Skip to content

Instantly share code, notes, and snippets.

@danielstenson
Last active October 28, 2025 16:57
Show Gist options
  • Select an option

  • Save danielstenson/07bf127136cd7730dfcfedb4e97a9649 to your computer and use it in GitHub Desktop.

Select an option

Save danielstenson/07bf127136cd7730dfcfedb4e97a9649 to your computer and use it in GitHub Desktop.
ES6 Compatible Dynamic Terrain Babylon Extension

ES6 Compatible Dynamic Terrain Babylon Extension

How to use

  • Place the babylon.dynamicTerrain_modular.ts file inside the src/externals/DynamicTerrain folder.
  • In your script that you'd like to generate the terrain from, import the module with:
import { DynamicTerrain } from "../externals/DynamicTerrain/babylon.dynamicTerrain_modular";
  • And use it like such:
terrain = new DynamicTerrain("t", params, scene);
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/camelcase */
/* eslint-disable no-var */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { SolidParticleSystem, Mesh, Scene, Camera, IndicesArray, VertexBuffer, Vector3, Vector2, Color4, Quaternion, MeshBuilder, VertexData, Color3, Tools, SolidParticle } from "@babylonjs/core";
export class DynamicTerrain {
public name: string;
private _terrainSub: number; // terrain number of subdivisions per axis
private _mapData: number[] | Float32Array; // data of the map
private _terrainIdx: number; // actual terrain vertex number per axis
private _mapSubX: number; // map number of subdivisions on X axis
private _mapSubZ: number; // map number of subdivisions on Z axis
private _mapUVs: number[] | Float32Array; // UV data of SPmapDatahe map
private _mapColors: number[] | Float32Array; // Color data of the map
private _mapNormals: number[] | Float32Array; // Normal data of the map
private _SPmapData: number[][] | Float32Array[]; // Solid particle data (position, rotation, scaling) of the object map : array of arrays, one per particle type
private _SPcolorData: number[][] | Float32Array[]; // Solid particle color data : array of arrays, one per particle type
private _SPuvData: number[][] | Float32Array[]; // Solid particle uv data : array of arrays, one per particle type
private _sps: SolidParticleSystem; // SPS used to manage the particles
private _spsTypeStartIndexes: number[] = []; // type start indexes in the SPS
// type start indexes in the SPS
private _nbAvailableParticlesPerType: number[]; // per type of used particle counter
private _spsNbPerType: number[] = []; // number of particles available per type in the SPS
// number of particles available per type in the SPS
private _particleDataStride = 9; // data stride : position, rotation, scaling : 9 floats
private _particleColorStride = 4; // color stride : color4 : r, g, b, a : 4 floats
private _particleUVStride = 4; // uv stride : vector4 : x, y, z, w : 4 floats
private _instanceMapData: number[][] | Float32Array[]; // Instance data (position, rotation, scaling) of the object map : array of arrays, one per instance type
private _instanceColorData: number[][] | Float32Array[]; // Instance color data : array of arrays, one per instance type
private _nbAvailableInstancesPerType: number[] = []; // per type of used instance counter
// per type of used instance counter
private _sourceMeshes: Mesh[]; // Source meshes used to hold the instances
private _typeSPS = 0; // Constant to define the SPS in the mapQuads
private _typeInstance = 1; // Constant to define the SPS in the mapQuads
private _scene: Scene; // current scene
private _subToleranceX: number = 1 | 0; // how many cells flought over thy the camera on the terrain x axis before update
private _subToleranceZ: number = 1 | 0; // how many cells flought over thy the camera on the terrain z axis before update
private _LODLimits: number[] = []; // array of LOD limits
private _initialLOD: number = 1 | 0; // initial LOD value (integer > 0)
private _LODValue: number = 1 | 0; // current LOD value : initial + camera correction
private _cameraLODCorrection: number = 0 | 0; // LOD correction (integer) according to the camera altitude
private _LODPositiveX = true; // Does LOD apply to the terrain right edge ?
private _LODNegativeX = true; // Does LOD apply to the terrain left edge ?
private _LODPositiveZ = true; // Does LOD apply to the terrain upper edge ?
private _LODNegativeZ = true; // Does LOD apply to the terrain lower edge ?
private _terrainCamera: Camera; // camera linked to the terrain
private _inverted = false; // is the terrain mesh inverted upside down ?
public shiftFromCamera: { x: number; z: number } = { // terrain center shift from camera position
x: 0.0,
z: 0.0
};
private _indices: IndicesArray;
private _positions: Float32Array | number[];
private _normals: Float32Array | number[];
private _colors: Float32Array | number[];
private _uvs: Float32Array | number[];
private _deltaSubX: number = 0 | 0; // map x subdivision delta : variation in number of map subdivisions
private _deltaSubZ: number = 0 | 0; // map z subdivision delta
private _refreshEveryFrame = false; // boolean : to force the terrain computation every frame
private _useCustomVertexFunction = false; // boolean : to allow the call to updateVertex()
private _computeNormals = false; // boolean : to skip or not the normal computation
private _datamap = false; // boolean : true if an data map is passed as parameter
private _uvmap = false; // boolean : true if an UV map is passed as parameter
private _colormap = false; // boolean : true if an color map is passed as parameter
private _mapSPData = false; // boolean : true if a SPmapData array is passed as parameter
private _colorSPData = false; // boolean : true if a SPcolorData array is passed as parameter
private _uvSPData = false; // boolean : true if a SPuvData array is passed as parameter
private _mapInstanceData = false; // boolean : true if a instanceMapData array is passed as parameter
private _colorInstanceData = false; // boolean : true if a instanceColorData array is passed as parameter
private _colorBuffers: VertexBuffer[] = []; // Reference to the created Color Buffers for the instances
// Reference to the created Color Buffers for the instances
private _mapQuads: number[][][][]; // map quads of types of particle/instance index in the SPmapData/instanceMapData array mapQuads[mapIndex]["sps" | "instances"][partType] = [pIndex1, pIndex2, ...] (particle/instance indexes in SPmapData/instanceMapData)
private _instanceWM: Float32Array[] = []; // precomputed world matrices of instances per type
// precomputed world matrices of instances per type
private _precomputeInstances = true; // if the instance WM must be precomputed once before
private static _vertex: any = { // current vertex object passed to the user custom function
position: Vector3.Zero(), // vertex position in the terrain space (Vector3)
uvs: Vector2.Zero(), // vertex uv
color: new Color4(1.0, 1.0, 1.0, 1.0), // vertex color (Color4)
lodX: 1 | 0, // vertex LOD value on X axis
lodZ: 1 | 0, // vertex LOD value on Z axis
worldPosition: Vector3.Zero(), // vertex World position
mapIndex: 0 | 0 // current map index
};
private _averageSubSizeX = 0.0; // map cell average x size
private _averageSubSizeZ = 0.0; // map cell average z size
private _terrainSizeX = 0.0; // terrain x size
private _terrainSizeZ = 0.0; // terrain y size
private _terrainHalfSizeX = 0.0;
private _terrainHalfSizeZ = 0.0;
private _centerWorld: Vector3 = Vector3.Zero(); // terrain world center position
private _centerLocal: Vector3 = Vector3.Zero(); // terrain local center position
private _mapSizeX = 0.0; // map x size
private _mapSizeZ = 0.0; // map z size
private _terrain: Mesh; // reference to the ribbon
private _isAlwaysVisible = false; // is the terrain mesh always selected for rendering
private _precomputeNormalsFromMap = false; // if the normals must be precomputed from the map data when assigning a new map to the existing terrain
// tmp vectors
private static _v1: Vector3 = Vector3.Zero();
private static _v2: Vector3 = Vector3.Zero();
private static _v3: Vector3 = Vector3.Zero();
private static _v4: Vector3 = Vector3.Zero();
private static _vAvB: Vector3 = Vector3.Zero();
private static _vAvC: Vector3 = Vector3.Zero();
private static _norm: Vector3 = Vector3.Zero();
private static _bbMin: Vector3 = Vector3.Zero();
private static _bbMax: Vector3 = Vector3.Zero();
private static _pos: Vector3 = Vector3.Zero();
private static _scl: Vector3 = Vector3.Zero();
// tmp quaternion and matrix or arrays
private static _quat: Quaternion = Quaternion.Identity();
private static _mat: Float32Array = new Float32Array(16);
private static _matZero: Float32Array = new Float32Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
private static _col: Float32Array = new Float32Array(4);
/**
* constructor
* @param name
* @param options
* @param scene
* @param {*} mapData the array of the map 3D data : x, y, z successive float values
* @param {*} mapSubX the data map number of x subdivisions : integer
* @param {*} mapSubZ the data map number of z subdivisions : integer
* @param {*} terrainSub the wanted terrain number of subdivisions : integer, multiple of 2.
* @param {*} mapUVs the array of the map UV data (optional) : u,v successive values, each between 0 and 1.
* @param {*} mapColors the array of the map Color data (optional) : r,g,b successive values, each between 0 and 1.
* @param {*} mapNormals the array of the map normal data (optional) : x, y, z successive float values.
* @param {*} invertSide boolean, to invert the terrain mesh upside down. Default false.
* @param {*} camera the camera to link the terrain to. Optional, by default the scene active camera
* @param {*} SPmapData an array of arrays or Float32Arrays (one per particle type) of object data (position, rotation, scaling) on the map. Optional.
* @param {*} sps the Solid Particle System used to manage the particles. Required when used with SPmapData.
* @param {*} SPcolorData an optional array of arrays or Float32Arrays (one per particle type) of object colors on the map. One series of r, g, b, a floats per object. Optional, requires a SPmapData and a sps to be passed.
* @param {*} SPuvData an optional array of arrays or Float32Arrays (one per particle type) of object uvs on the map. One series of x, y, z, w floats per object. Optional, requires a SPmapData and a sps to be passed.
* @param {*} instanceMapData an array of arrays or Float32Arrays (one per instance type) of object data (position, rotation, scaling) on the map. Optional.
* @param {*} sourceMeshes an array of source meshes. Required when used with InstanceMapdata.
* @param {*} instanceColorData an optional array of arrays or Float32Arrays (one per instance type) of object colors on the map. One series of r, g, b, a floats per object. Optional, requires a InstanceMapData and an sourceMeshes array to be passed.
* @param {*} precomputeInstances an optional boolean (default true) to precompute all the instance world matrices (faster, but more memory used)
*/
constructor(name: string, options: {
terrainSub?: number;
mapData?: number[] | Float32Array;
mapSubX?: number; mapSubZ?: number;
mapUVs?: number[] | Float32Array;
mapColors?: number[] | Float32Array;
mapNormals?: number[] | Float32Array;
invertSide?: boolean;
camera?: Camera;
SPmapData?: number[][] | Float32Array[];
sps?: SolidParticleSystem;
SPcolorData?: number[][] | Float32Array[];
SPuvData?: number[][] | Float32Array[];
instanceMapData?: number[][] | Float32Array[];
sourceMeshes?: Mesh[];
instanceColorData?: number[][] | Float32Array[];
precomputeInstances?: boolean;
}, scene: Scene) {
this.name = name;
this._terrainSub = options.terrainSub || 60;
this._mapData = options.mapData!!;
this._terrainIdx = this._terrainSub + 1;
this._mapSubX = options.mapSubX || this._terrainIdx;
this._mapSubZ = options.mapSubZ || this._terrainIdx;
this._mapUVs = options.mapUVs!!; // if not defined, it will be still populated by default values
this._mapColors = options.mapColors!!;
this._scene = scene;
this._terrainCamera = options.camera || scene.activeCamera!!;
this._inverted = options.invertSide!!;
this._SPmapData = options.SPmapData!!;
this._SPcolorData = options.SPcolorData!!;
this._SPuvData = options.SPuvData!!;
this._sps = options.sps!!;
this._instanceMapData = options.instanceMapData!!;
this._instanceColorData = options.instanceColorData!!;
this._sourceMeshes = options.sourceMeshes!!;
this._precomputeInstances = (options.precomputeInstances) ? (options.precomputeInstances) : true;
// initialize the map arrays if not passed as parameters
this._datamap = (this._mapData) ? true : false;
this._uvmap = (this._mapUVs) ? true : false;
this._colormap = (this._mapColors) ? true : false;
this._mapSPData = (this._SPmapData) ? true : false;
this._colorSPData = (this._mapSPData && this._SPcolorData) ? true : false;
this._uvSPData = (this._mapSPData && this._SPuvData) ? true : false;
this._mapInstanceData = (this._instanceMapData) ? true : false;
this._colorInstanceData = (this._mapInstanceData && this._instanceColorData) ? true : false;
this._mapData = (this._datamap) ? this._mapData : new Float32Array(this._terrainIdx * this._terrainIdx * 3);
this._mapUVs = (this._uvmap) ? this._mapUVs : new Float32Array(this._terrainIdx * this._terrainIdx * 2);
if (this._datamap) {
this._mapNormals = options.mapNormals || new Float32Array(this._mapSubX * this._mapSubZ * 3);
}
else {
this._mapNormals = new Float32Array(this._terrainIdx * this._terrainIdx * 3);
}
this._mapQuads = [];
// Ribbon creation
let index = 0; // current vertex index in the map array
let posIndex = 0; // current position (coords) index in the map array
let colIndex = 0; // current color index in the color array
let uvIndex = 0; // current uv index in the uv array
let color; // current color
let uv; // current uv
let terIndex = 0; // current index in the terrain array
let y = 0.0; // current y coordinate
let terrainPath; // current path
let u = 0.0; // current u of UV
let v = 0.0; // current v of UV
const lg = this._terrainIdx + 1; // augmented length for the UV to finish before
const terrainData = [];
const terrainColor = [];
const terrainUV = [];
const mapData = this._mapData;
const mapColors = this._mapColors;
const mapUVs = this._mapUVs;
const nbAvailableParticlesPerType: number[] = [];
this._nbAvailableParticlesPerType = nbAvailableParticlesPerType;
for (let j = 0; j <= this._terrainSub; j++) {
terrainPath = [];
for (let i = 0; i <= this._terrainSub; i++) {
index = this._mod(j * 3, this._mapSubZ) * this._mapSubX + this._mod(i * 3, this._mapSubX);
posIndex = index * 3;
colIndex = index * 3;
uvIndex = index * 2;
terIndex = j * this._terrainIdx + i;
// geometry
if (this._datamap) {
y = mapData[posIndex + 1];
}
else {
y = 0.0;
mapData[3 * terIndex] = i;
mapData[3 * terIndex + 1] = y;
mapData[3 * terIndex + 2] = j;
}
terrainPath.push(new Vector3(i, y, j));
// color
if (this._colormap) {
color = new Color4(mapColors[colIndex], mapColors[colIndex + 1], mapColors[colIndex + 2], 1.0);
}
else {
color = new Color4(1.0, 1.0, 1.0, 1.0);
}
terrainColor.push(color);
// uvs
if (this._uvmap) {
uv = new Vector2(mapUVs[uvIndex], mapUVs[uvIndex + 1]);
}
else {
u = 1.0 - Math.abs(1.0 - 2.0 * i / lg);
v = 1.0 - Math.abs(1.0 - 2.0 * j / lg);
mapUVs[2 * terIndex] = u;
mapUVs[2 * terIndex + 1] = v;
uv = new Vector2(u, v);
}
terrainUV.push(uv);
}
terrainData.push(terrainPath);
}
this._mapSizeX = Math.abs(mapData[(this._mapSubX - 1) * 3] - mapData[0]);
this._mapSizeZ = Math.abs(mapData[(this._mapSubZ - 1) * this._mapSubX * 3 + 2] - mapData[2]);
this._averageSubSizeX = this._mapSizeX / this._mapSubX;
this._averageSubSizeZ = this._mapSizeZ / this._mapSubZ;
const ribbonOptions = {
pathArray: terrainData,
sideOrientation: (options.invertSide) ? Mesh.FRONTSIDE : Mesh.BACKSIDE,
colors: terrainColor,
uvs: terrainUV,
updatable: true
};
this._terrain = MeshBuilder.CreateRibbon("terrain", ribbonOptions, this._scene);
this._indices = this._terrain.getIndices()!!;
this._positions = this._terrain.getVerticesData(VertexBuffer.PositionKind)!!;
this._normals = this._terrain.getVerticesData(VertexBuffer.NormalKind)!!;
this._uvs = this._terrain.getVerticesData(VertexBuffer.UVKind)!!;
this._colors = this._terrain.getVerticesData(VertexBuffer.ColorKind)!!;
this.computeNormalsFromMap();
// update it immediatly and register the update callback function in the render loop
this.update(true);
this._terrain.position.x = this._terrainCamera.globalPosition.x - this._terrainHalfSizeX + this.shiftFromCamera.x;
this._terrain.position.z = this._terrainCamera.globalPosition.z - this._terrainHalfSizeZ + this.shiftFromCamera.z;
// initialize deltaSub to make on the map
const deltaNbSubX = (this._terrain.position.x - mapData[0]) / this._averageSubSizeX;
const deltaNbSubZ = (this._terrain.position.z - mapData[2]) / this._averageSubSizeZ
this._deltaSubX = (deltaNbSubX > 0) ? Math.floor(deltaNbSubX) : Math.ceil(deltaNbSubX);
this._deltaSubZ = (deltaNbSubZ > 0) ? Math.floor(deltaNbSubZ) : Math.ceil(deltaNbSubZ);
this._scene.onBeforeRenderObservable.add(() => {
const refreshEveryFrame = this._refreshEveryFrame;
this.beforeUpdate(refreshEveryFrame);
this.update(refreshEveryFrame);
this.afterUpdate(refreshEveryFrame);
});
// Solid Particles or Instances in the map
const SPmapData = this._SPmapData;
const instanceMapData = this._instanceMapData;
const dataStride = this._particleDataStride;
const typeSPS = this._typeSPS;
const typeInstance = this._typeInstance;
const mapSizeX = this._mapSizeX;
const mapSizeZ = this._mapSizeZ;
const mapSubX = this._mapSubX;
const mapSubZ = this._mapSubZ;
const quads = this._mapQuads;
// if SP data, populate the map quads
// mapQuads[mapIndex][typeSPS][partType] = [partIdx1 , partIdx2 ...] partIdx are particle indexes in SPmapData
if (this._mapSPData) {
const x0 = mapData[0];
const z0 = mapData[2];
for (let t = 0; t < SPmapData.length; t++) {
const data = SPmapData[t];
const nb = (data.length / dataStride) | 0;
for (let pIdx = 0; pIdx < nb; pIdx++) {
// particle position x, z in the map
const dIdx = pIdx * dataStride;
let x = data[dIdx];
let z = data[dIdx + 2];
x = x - Math.floor((x - x0) / mapSizeX) * mapSizeX;
z = z - Math.floor((z - z0) / mapSizeZ) * mapSizeZ;
const col = Math.floor((x - x0) * mapSubX / mapSizeX);
const row = Math.floor((z - z0) * mapSubZ / mapSizeZ);
const quadIdx = row * mapSubX + col;
if (quads[quadIdx] === undefined) {
quads[quadIdx] = [];
quads[quadIdx][typeSPS] = [];
}
if (quads[quadIdx][typeSPS][t] === undefined) {
quads[quadIdx][typeSPS][t] = [];
}
const quad = quads[quadIdx][typeSPS][t];
// push the particle index from the SPmapData array into the quads array
quad.push(pIdx);
}
}
// update the sps
const sps = this._sps;
sps.computeBoundingBox = true;
sps.isAlwaysVisible = true;
if (this._colorSPData) {
sps.computeParticleColor = true;
}
if (this._uvSPData) {
sps.computeParticleTexture = true;
}
// store particle types
const spsTypeStartIndexes: number[] = [];
this._spsTypeStartIndexes = spsTypeStartIndexes;
const spsNbPerType: number[] = [];
this._spsNbPerType = spsNbPerType;
const nbParticles = sps.nbParticles;
const particles = sps.particles;
let type = 0;
spsTypeStartIndexes.push(type);
nbAvailableParticlesPerType.push(0);
let count = 1;
for (let p = 1; p < nbParticles; p++) {
particles[p].isVisible = false;
if (type != particles[p].shapeId) {
type++;
spsTypeStartIndexes.push(p);
spsNbPerType.push(count);
nbAvailableParticlesPerType.push(count);
count = 0;
}
count++;
}
spsNbPerType.push(count);
}
// if instance data, populate the map quads
// mapQuads[mapIndex][typeInstances][instanceType] = [instanceIdx1 , instanceIdx2 ...] instanceIdx are instance indexes in instanceMapData
if (this._mapInstanceData) {
const x0 = mapData[0];
const z0 = mapData[2];
this._colorBuffers = [];
this._instanceWM = [];
const posVct = DynamicTerrain._pos;
const sclVct = DynamicTerrain._scl;
const mat = DynamicTerrain._mat;
const quat = DynamicTerrain._quat;
const composeToRef = DynamicTerrain._ComposeToRef;
for (let t = 0; t < instanceMapData.length; t++) {
const data = instanceMapData[t];
const nb = (data.length / dataStride) | 0;
var instanceWM: Float32Array = new Float32Array(0);
if (this._precomputeInstances) {
this._instanceWM[t] = new Float32Array(nb * 16); // 16 floats per instance WM
instanceWM = this._instanceWM[t];
}
for (let pIdx = 0; pIdx < nb; pIdx++) {
// instance position x, z in the map
const dIdx = pIdx * dataStride;
let x = data[dIdx];
const y = data[dIdx + 1];
let z = data[dIdx + 2];
// precompute all the instance WM and store them
if (this._precomputeInstances) {
posVct.copyFromFloats(x, y, z);
const rx = data[dIdx + 3];
const ry = data[dIdx + 4];
const rz = data[dIdx + 5];
Quaternion.RotationYawPitchRollToRef(ry, rx, rz, quat);
sclVct.copyFromFloats(data[dIdx + 6], data[dIdx + 7], data[dIdx + 8]);
composeToRef(sclVct, quat, posVct, mat);
const wmIndex = 16 * pIdx;
instanceWM.set(mat, wmIndex);
}
x = x - Math.floor((x - x0) / mapSizeX) * mapSizeX;
z = z - Math.floor((z - z0) / mapSizeZ) * mapSizeZ;
const col = Math.floor((x - x0) * mapSubX / mapSizeX);
const row = Math.floor((z - z0) * mapSubZ / mapSizeZ);
const quadIdx = row * mapSubX + col;
if (quads[quadIdx] === undefined) {
quads[quadIdx] = [];
quads[quadIdx][typeInstance] = [];
}
if (quads[quadIdx][typeInstance] === undefined) {
quads[quadIdx][typeInstance] = [];
}
if (quads[quadIdx][typeInstance][t] === undefined) {
quads[quadIdx][typeInstance][t] = [];
}
const quad = quads[quadIdx][typeInstance][t];
// push the instance index from the instanceMapData array into the quads array
quad.push(pIdx);
}
}
// store instance types and init instance buffers
const nbAvailableInstancesPerType: number[] = [];
this._nbAvailableInstancesPerType = nbAvailableInstancesPerType;
const typeNb = this._sourceMeshes.length;
const engine = this._scene.getEngine();
for (let t = 0; t < typeNb; t++) {
const mesh = this._sourceMeshes[t];
mesh.alwaysSelectAsActiveMesh = true;
const nb = mesh.instances.length;
nbAvailableInstancesPerType[t] = nb;
mesh.manualUpdateOfWorldMatrixInstancedBuffer = true;
for (let i = 0; i < mesh.instances.length; i++) {
const instance = mesh.instances[i];
instance.freezeWorldMatrix();
instance.alwaysSelectAsActiveMesh = true;
instance.doNotSyncBoundingInfo = true;
}
if (this._colorInstanceData) {
const colorArray = new Float32Array(4 * (mesh.instances.length + 1));
for (let c = 0; c < colorArray.length; c++) {
colorArray[c] = 1;
}
const colorBuffer = new VertexBuffer(engine, colorArray, VertexBuffer.ColorKind, true, false, 4, true);
mesh.setVerticesBuffer(colorBuffer);
this._colorBuffers.push(colorBuffer);
}
}
}
this.update(true); // recompute everything once the initial deltas are calculated
}
/**
* Updates the terrain position and shape according to the camera position.
* `force` : boolean, forces the terrain update even if no camera position change.
* Returns the terrain.
*/
public update(force: boolean): DynamicTerrain {
let needsUpdate = false;
let updateLOD = false;
const updateForced = (force) ? true : false;
const terrainPosition = this._terrain.position;
const cameraPosition = this._terrainCamera.globalPosition;
const shiftFromCamera = this.shiftFromCamera;
let terrainHalfSizeX = this._terrainHalfSizeX;
let terrainHalfSizeZ = this._terrainHalfSizeZ;
const deltaX = terrainHalfSizeX + terrainPosition.x - cameraPosition.x - shiftFromCamera.x;
const deltaZ = terrainHalfSizeZ + terrainPosition.z - cameraPosition.z - shiftFromCamera.z;
const subToleranceX = this._subToleranceX;
const subToleranceZ = this._subToleranceZ;
const mod = this._mod;
// current LOD
const oldCorrection = this._cameraLODCorrection;
this._cameraLODCorrection = (this.updateCameraLOD(this._terrainCamera)) | 0;
updateLOD = (oldCorrection == this._cameraLODCorrection) ? false : true;
let LODValue = this._initialLOD + this._cameraLODCorrection;
LODValue = (LODValue > 0) ? LODValue : 1;
this._LODValue = LODValue;
// threshold sizes on each axis to trigger the terrain update
const mapShiftX = this._averageSubSizeX * subToleranceX * LODValue;
const mapShiftZ = this._averageSubSizeZ * subToleranceZ * LODValue;
let mapFlgtNb = 0 | 0; // number of map cells flought over by the camera in the delta shift
let deltaSubX = this._deltaSubX;
let deltaSubZ = this._deltaSubZ;
if (Math.abs(deltaX) > mapShiftX) {
const signX = (deltaX > 0.0) ? -1 : 1;
mapFlgtNb = Math.abs(deltaX / mapShiftX) | 0;
terrainPosition.x += mapShiftX * signX * mapFlgtNb;
deltaSubX += (subToleranceX * signX * LODValue * mapFlgtNb);
needsUpdate = true;
}
if (Math.abs(deltaZ) > mapShiftZ) {
const signZ = (deltaZ > 0.0) ? -1 : 1;
mapFlgtNb = Math.abs(deltaZ / mapShiftZ) | 0;
terrainPosition.z += mapShiftZ * signZ * mapFlgtNb;
deltaSubZ += (subToleranceZ * signZ * LODValue * mapFlgtNb);
needsUpdate = true;
}
const updateSize = updateLOD || updateForced; // must the terrain size be updated ?
if (needsUpdate || updateSize) {
this._deltaSubX = mod(deltaSubX, this._mapSubX);
this._deltaSubZ = mod(deltaSubZ, this._mapSubZ);
this._updateTerrain(updateSize);
}
terrainHalfSizeX = this._terrainHalfSizeX;
terrainHalfSizeZ = this._terrainHalfSizeZ;
this.centerLocal.copyFromFloats(terrainHalfSizeX, 0.0, terrainHalfSizeZ);
this._centerWorld.copyFromFloats(terrainPosition.x + terrainHalfSizeX, terrainPosition.y, terrainPosition.z + terrainHalfSizeZ);
return this;
}
// private : updates the underlying ribbon
private _updateTerrain(updateSize: boolean): void {
let stepJ = 0 | 0;
let stepI = 0 | 0;
let LODLimitDown = 0 | 0;
let LODLimitUp = 0 | 0;
const LODValue = this._LODValue; // terrain LOD value
let axisLODValue = LODValue; // current axis computed LOD value
let lodI = LODValue; // LOD X
let lodJ = LODValue; // LOD Z
const bbMin = DynamicTerrain._bbMin;
const bbMax = DynamicTerrain._bbMax;
const terrain = this._terrain;
const positions = this._positions;
const normals = this._normals;
const colors = this._colors;
const uvs = this._uvs;
const mapColors = this._mapColors;
const mapNormals = this._mapNormals;
const mapData = this._mapData;
const mapUVs = this._mapUVs;
const mapSPData = this._mapSPData;
const quads = this._mapQuads;
const nbPerType = this._spsNbPerType;
const SPmapData = this._SPmapData;
const SPcolorData = this._SPcolorData;
const SPuvData = this._SPuvData;
const mapInstanceData = this._mapInstanceData;
const instanceMapData = this._instanceMapData;
const instanceColorData = this._instanceColorData;
const dataStride = this._particleDataStride;
const colorStride = this._particleColorStride;
const uvStride = this._particleUVStride;
const LODLimits = this._LODLimits;
const terrainSub = this._terrainSub;
const mod = this._mod;
const terrainIdx = this._terrainIdx;
const mapSubX = this._mapSubX;
const mapSubZ = this._mapSubZ;
const deltaSubX = this._deltaSubX;
const deltaSubZ = this._deltaSubZ;
const datamap = this._datamap;
const uvmap = this._uvmap;
const colormap = this._colormap;
const useCustomVertexFunction = this._useCustomVertexFunction;
const updateVertex = this.updateVertex;
const dontComputeNormals = !this._computeNormals;
const LODpstvX = this._LODPositiveX;
const LODngtvX = this._LODNegativeX;
const LODpstvZ = this._LODPositiveZ;
const LODngtvZ = this._LODNegativeZ;
const mapSizeX = this._mapSizeX;
const mapSizeZ = this._mapSizeZ;
const averageSubSizeX = this._averageSubSizeX;
const averageSubSizeZ = this._averageSubSizeZ;
const particleMap = (mapSPData && quads);
const particleColorMap = (particleMap && this._colorSPData);
const particleUVMap = (particleMap && this._uvSPData);
const typeSPS = this._typeSPS;
const typeInstance = this._typeInstance;
const instanceMap = (mapInstanceData && quads);
const instanceColorMap = (instanceMap && this._colorInstanceData);
const precomputeInstances = this._precomputeInstances;
const sourceMeshes = this._sourceMeshes;
const nbAvailableInstancesPerType = this._nbAvailableInstancesPerType;
const composeToRef = DynamicTerrain._ComposeToRef;
const copyArrayValuesFromToRef = DynamicTerrain._CopyArrayValuesFromToRef;
const instanceWM = this._instanceWM;
const sclVct = DynamicTerrain._scl;
const posVct = DynamicTerrain._pos;
const quat = DynamicTerrain._quat;
const matZero = DynamicTerrain._matZero;
let l = 0 | 0;
let index = 0 | 0; // current vertex index in the map data array
let posIndex1 = 0 | 0; // current position index in the map data array
let posIndex2 = 0 | 0;
let posIndex3 = 0 | 0;
let colIndex = 0 | 0; // current index in the map color array
let uvIndex = 0 | 0; // current index in the map uv array
let terIndex = 0 | 0; // current vertex index in the terrain map array when used as a data map
let ribbonInd = 0 | 0; // current ribbon vertex index
let ribbonPosInd = 0 | 0; // current ribbon position index (same than normal index)
let ribbonUVInd = 0 | 0; // current ribbon UV index
let ribbonColInd = 0 | 0; // current ribbon color index
let ribbonColInd1 = 0 | 0;
let ribbonColInd2 = 0 | 0;
let ribbonColInd3 = 0 | 0;
let ribbonColInd4 = 0 | 0;
let ribbonPosInd1 = 0 | 0;
let ribbonPosInd2 = 0 | 0;
let ribbonPosInd3 = 0 | 0;
// note : all the indexes are explicitly set as integers for the js optimizer (store them all in the stack)
if (updateSize) {
this.updateTerrainSize();
}
Vector3.FromFloatsToRef(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE, bbMin);
Vector3.FromFloatsToRef(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE, bbMax);
// Object (solid particles or instances) map management
const x0 = mapData[0];
const z0 = mapData[2];
const terrainPos = terrain.position;
var spsTypeStartIndexes: number[] = [];
var nbAvailableParticlesPerType: number[] = [];
var particles: SolidParticle[] = [];
// if solid particle data
var sps: SolidParticleSystem;
if (particleMap) {
sps = this._sps;
particles = sps.particles;
spsTypeStartIndexes = this._spsTypeStartIndexes;
nbAvailableParticlesPerType = this._nbAvailableParticlesPerType;
// reset all the particles to invisible
const nbParticles = sps.nbParticles;
for (let p = 0; p < nbParticles; p++) {
particles[p].isVisible = false;
}
}
// if instance data
var mat: Float32Array = new Float32Array(0);
if (instanceMap) {
mat = DynamicTerrain._mat;
for (let t = 0; t < sourceMeshes.length; t++) {
const sourceMesh = sourceMeshes[t];
const instancedBuffer = sourceMesh.worldMatrixInstancedBuffer;
if (instancedBuffer) {
const instances = sourceMesh.instances;
let offset = 0;
for (let i = 0; i < instances.length; i++) {
instancedBuffer.set(matZero, offset);
offset += 16;
}
}
}
}
// Test map seam within the terrain
let seamX = false;
let seamZ = false;
let seamXIndex = 0 | 0;
let seamZIndex = 0 | 0;
let prevXIndex = mod(deltaSubX, mapSubX);
let prevZIndex = mod(deltaSubZ, mapSubZ);
let axisZLODValue = 0 | 0;
let axisXLODValue = 0 | 0;
let curXIndex = 0 | 0;
let curZIndex = 0 | 0;
const positionsLength = positions.length;
const uvsLength = uvs.length;
const colorsLength = colors.length;
for (let j = 1 | 0; j <= terrainSub; j++) {
axisZLODValue = LODValue;
axisXLODValue = LODValue;
for (l = 0; l < LODLimits.length; l++) {
LODLimitDown = LODLimits[l];
LODLimitUp = terrainSub - LODLimitDown - 1;
if ((LODngtvZ && j < LODLimitDown) || (LODpstvZ && j > LODLimitUp)) {
axisZLODValue = l + 1 + LODValue;
}
if ((LODngtvX && j < LODLimitDown) || (LODpstvX && j > LODLimitUp)) {
axisXLODValue = l + 1 + LODValue;
}
lodJ = axisZLODValue;
lodI = axisXLODValue;
}
stepJ += lodJ;
stepI += lodI;
// seam test
if (!seamX) {
curXIndex = mod(deltaSubX + stepI, mapSubX);
if (Math.abs(curXIndex - prevXIndex) > lodI) {
seamX = true;
seamXIndex = stepI;
}
else {
prevXIndex = curXIndex;
}
}
if (!seamZ) {
curZIndex = mod(deltaSubZ + stepJ, mapSubZ);
if (Math.abs(curZIndex - prevZIndex) > lodJ) {
seamZ = true;
seamZIndex = stepJ;
}
else {
prevZIndex = curZIndex;
}
}
if (seamZ && seamX) {
break;
}
}
stepI = 0 | 0;
stepJ = 0 | 0;
lodI = LODValue;
lodJ = LODValue;
let zIndex = 0 | 0;
let xIndex = 0 | 0;
for (let j = 0 | 0; j <= terrainSub; j++) {
// LOD Z
axisLODValue = LODValue;
for (l = 0; l < LODLimits.length; l++) {
LODLimitDown = LODLimits[l];
LODLimitUp = terrainSub - LODLimitDown - 1;
if ((LODngtvZ && j < LODLimitDown) || (LODpstvZ && j > LODLimitUp)) {
axisLODValue = l + 1 + LODValue;
}
lodJ = axisLODValue;
}
zIndex = mod(deltaSubZ + stepJ, mapSubZ);
for (let i = 0 | 0; i <= terrainSub; i++) {
// LOD X
axisLODValue = LODValue;
for (l = 0; l < LODLimits.length; l++) {
LODLimitDown = LODLimits[l];
LODLimitUp = terrainSub - LODLimitDown - 1;
if ((LODngtvX && i < LODLimitDown) || (LODpstvX && i > LODLimitUp)) {
axisLODValue = l + 1 + LODValue;
}
lodI = axisLODValue;
}
// map current index
xIndex = mod(deltaSubX + stepI, mapSubX);
index = zIndex * mapSubX + xIndex;
terIndex = mod(deltaSubZ + stepJ, terrainIdx) * terrainIdx + mod(deltaSubX + stepI, terrainIdx);
// related index in the array of positions (data map)
if (datamap) {
posIndex1 = 3 * index;
}
else {
posIndex1 = 3 * terIndex;
}
// related index in the UV map
if (uvmap) {
uvIndex = 2 * index;
}
else {
uvIndex = 2 * terIndex;
}
// related index in the color map
if (colormap) {
colIndex = 3 * index;
}
else {
colIndex = 3 * terIndex;
}
//map indexes
posIndex2 = posIndex1 + 1;
posIndex3 = posIndex1 + 2;
// ribbon indexes
ribbonPosInd = 3 * ribbonInd;
ribbonColInd = 4 * ribbonInd;
ribbonUVInd = 2 * ribbonInd;
ribbonPosInd1 = ribbonPosInd;
ribbonPosInd2 = ribbonPosInd + 1;
ribbonPosInd3 = ribbonPosInd + 2;
ribbonColInd1 = ribbonColInd;
ribbonColInd2 = ribbonColInd + 1;
ribbonColInd3 = ribbonColInd + 2;
ribbonColInd4 = ribbonColInd + 3;
ribbonInd += 1;
// geometry
positions[ribbonPosInd1] = averageSubSizeX * stepI;
positions[ribbonPosInd2] = mapData[posIndex2];
positions[ribbonPosInd3] = averageSubSizeZ * stepJ;
if (dontComputeNormals) {
normals[ribbonPosInd1] = mapNormals[posIndex1];
normals[ribbonPosInd2] = mapNormals[posIndex2];
normals[ribbonPosInd3] = mapNormals[posIndex3];
}
// uv : the array _mapUVs is always populated
uvs[ribbonUVInd] = mapUVs[uvIndex];
uvs[ribbonUVInd + 1] = mapUVs[uvIndex + 1];
// color
if (colormap) {
colors[ribbonColInd1] = mapColors[colIndex];
colors[ribbonColInd2] = mapColors[colIndex + 1];
colors[ribbonColInd3] = mapColors[colIndex + 2];
}
// seam test on Z
if (seamZ && (seamZIndex == stepJ || stepJ == seamZIndex + 1)) {
const back3 = 3 * terrainSub + 3;
const ind1 = mod(ribbonPosInd1 - back3, positionsLength);
const ind2 = ind1 + 1;
const ind3 = ind1 + 2;
positions[ribbonPosInd1] = positions[ind1];
positions[ribbonPosInd2] = positions[ind2];
positions[ribbonPosInd3] = positions[ind3];
if (dontComputeNormals) {
normals[ribbonPosInd1] = normals[ind1];
normals[ribbonPosInd2] = normals[ind2];
normals[ribbonPosInd3] = normals[ind3];
}
const back2 = 2 * terrainSub + 2;
const back4 = 2 * back2;
if (stepJ == seamZIndex + 1) {
const induv = mod(ribbonUVInd - back2, uvsLength);
uvs[ribbonUVInd] = uvs[induv];
uvs[ribbonUVInd + 1] = uvs[induv + 1];
if (colormap) {
const indcol = mod(ribbonColInd - back4, colorsLength);
colors[ribbonColInd1] = colors[indcol];
colors[ribbonColInd2] = colors[indcol + 1];
colors[ribbonColInd3] = colors[indcol + 2];
}
}
}
// seam test on X
if (seamX && (seamXIndex == stepI || stepI == seamXIndex + 1)) {
const back3 = 3;
const ind1 = mod(ribbonPosInd1 - back3, positionsLength);
const ind2 = ind1 + 1;
const ind3 = ind1 + 2;
positions[ribbonPosInd1] = positions[ind1];
positions[ribbonPosInd2] = positions[ind2];
positions[ribbonPosInd3] = positions[ind3];
if (dontComputeNormals) {
normals[ribbonPosInd1] = normals[ind1];
normals[ribbonPosInd2] = normals[ind2];
normals[ribbonPosInd3] = normals[ind3];
}
const back2 = 2;
const back4 = 4;
if (stepI == seamXIndex + 1) {
const induv = mod(ribbonUVInd - back2, uvsLength);
uvs[ribbonUVInd] = uvs[induv];
uvs[ribbonUVInd + 1] = uvs[induv + 1];
if (colormap) {
const indcol = mod(ribbonColInd - back4, colorsLength);
colors[ribbonColInd1] = colors[indcol];
colors[ribbonColInd2] = colors[indcol + 1];
colors[ribbonColInd3] = colors[indcol + 2];
}
}
}
// bbox internal update
if (positions[ribbonPosInd1] < bbMin.x) {
bbMin.x = positions[ribbonPosInd1];
}
if (positions[ribbonPosInd1] > bbMax.x) {
bbMax.x = positions[ribbonPosInd1];
}
if (positions[ribbonPosInd2] < bbMin.y) {
bbMin.y = positions[ribbonPosInd2];
}
if (positions[ribbonPosInd2] > bbMax.y) {
bbMax.y = positions[ribbonPosInd2];
}
if (positions[ribbonPosInd3] < bbMin.z) {
bbMin.z = positions[ribbonPosInd3];
}
if (positions[ribbonPosInd3] > bbMax.z) {
bbMax.z = positions[ribbonPosInd3];
}
// call to user custom function with the current updated vertex object
if (useCustomVertexFunction) {
const vertex = DynamicTerrain._vertex;
const vertexPosition = vertex.position;
const vertexWorldPosition = vertex.worldPosition;
const vertexColor = vertex.color;
const vertexUvs = vertex.uvs;
vertexPosition.copyFromFloats(positions[ribbonPosInd1], positions[ribbonPosInd2], positions[ribbonPosInd3]);
vertexWorldPosition.copyFromFloats(mapData[posIndex1], vertexPosition.y, mapData[posIndex3]);
vertex.lodX = lodI;
vertex.lodZ = lodJ;
vertexColor.copyFromFloats(colors[ribbonColInd1], colors[ribbonColInd2], colors[ribbonColInd3], colors[ribbonColInd4]);
vertexUvs.copyFromFloats(uvs[ribbonUVInd], uvs[ribbonUVInd + 1]);
vertex.mapIndex = index;
updateVertex(vertex, i, j); // the user can modify the array values here
colors[ribbonColInd1] = vertexColor.r;
colors[ribbonColInd2] = vertexColor.g;
colors[ribbonColInd3] = vertexColor.b;
colors[ribbonColInd4] = vertexColor.a;
uvs[ribbonUVInd] = vertexUvs.x;
uvs[ribbonUVInd + 1] = vertexUvs.y;
positions[ribbonPosInd1] = vertexPosition.x;
positions[ribbonPosInd2] = vertexPosition.y;
positions[ribbonPosInd3] = vertexPosition.z;
}
// SPS management
if (particleMap) {
// if a quad contains some objects in the map
if (quads[index]) {
const quad = quads[index][typeSPS];
for (let t = 0; t < quad.length; t++) {
const data = SPmapData[t];
const partIndexes = quad[t];
var sp_colorData: number[] | Float32Array = [];
if (particleColorMap) {
sp_colorData = SPcolorData[t];
}
var sp_uvData: number[] | Float32Array = [];
if (particleUVMap) {
sp_uvData = SPuvData[t];
}
if (partIndexes) {
const typeStartIndex = spsTypeStartIndexes[t]; // particle start index for a given type in the SPS
const nbQuadParticles = partIndexes.length;
const nbInSPS = nbPerType[t];
let available = nbAvailableParticlesPerType[t];
const rem = nbInSPS - available;
var used = (rem > 0) ? rem : 0;
let min = (available < nbQuadParticles) ? available : nbQuadParticles; // don't iterate beyond possible
for (let pIdx = 0; pIdx < min; pIdx++) {
const px = partIndexes[pIdx]
const idm = px * dataStride;
// set successive available particles of this type
const particle = particles[typeStartIndex + pIdx + used];
const pos = particle.position;
const rot = particle.rotation;
const scl = particle.scaling;
const x = data[idm];
pos.x = x + Math.floor((terrainPos.x - x - x0) / mapSizeX) * mapSizeX;
pos.y = data[idm + 1];
const z = data[idm + 2];
pos.z = z + Math.floor((terrainPos.z - z - z0) / mapSizeZ) * mapSizeZ;
rot.x = data[idm + 3];
rot.y = data[idm + 4];
rot.z = data[idm + 5];
scl.x = data[idm + 6];
scl.y = data[idm + 7];
scl.z = data[idm + 8];
if (particleColorMap) {
const idc = px * colorStride;
const col = particle.color!!;
col.r = sp_colorData[idc];
col.g = sp_colorData[idc + 1];
col.b = sp_colorData[idc + 2];
col.a = sp_colorData[idc + 3];
}
if (particleUVMap) {
const iduv = px * uvStride;
const uvs = particle.uvs;
uvs.x = sp_uvData[iduv];
uvs.y = sp_uvData[iduv + 1];
uvs.z = sp_uvData[iduv + 2];
uvs.w = sp_uvData[iduv + 3];
}
particle.isVisible = true;
available = available - 1;
used = used + 1;
min = (available < nbQuadParticles) ? available : nbQuadParticles;
}
available = (available > 0) ? available : 0;
nbAvailableParticlesPerType[t] = available;
}
}
}
}
// Instance management
if (instanceMap) {
// are there objects in this quad ?
if (quads[index]) {
const quad = quads[index][typeInstance];
const colorBuffers = this._colorBuffers;
const tmpCol = DynamicTerrain._col;
for (let t = 0; t < quad.length; t++) {
const sourceMesh = this._sourceMeshes[t];
const instances = sourceMesh.instances;
const instancedBuffer = sourceMesh.worldMatrixInstancedBuffer;
const data = instanceMapData[t];
const instanceIndexes = quad[t];
const instWM = instanceWM[t];
var instance_colorData: number[] | Float32Array = [];
var colorBuffer: VertexBuffer;
if (instanceColorMap) {
instance_colorData = instanceColorData[t];
colorBuffer = colorBuffers[t];
}
if (instanceIndexes && instancedBuffer) {
const nbQuadInstances = instanceIndexes.length;
const nbInstances = instances.length;
let available = nbAvailableInstancesPerType[t];
const rem = nbInstances - available;
var used = (rem > 0) ? rem : 0;
let min = (available < nbQuadInstances) ? available : nbQuadInstances; // don't iterate beyond possible
for (let iIdx = 0; iIdx < min; iIdx++) {
const ix = instanceIndexes[iIdx];
const idm = ix * dataStride;
// set successive instance of this type
const nextFree = iIdx + used;
const bufferIndex = nextFree * 16; // the world matrix instanced buffer offset is 16
if (precomputeInstances) {
copyArrayValuesFromToRef(instWM, ix * 16, 16, mat);
}
else {
let x = data[idm];
let y = data[idm + 1];
let z = data[idm + 2];
x = x + Math.floor((terrainPos.x - x - x0) / mapSizeX) * mapSizeX;
z = z + Math.floor((terrainPos.z - z - z0) / mapSizeZ) * mapSizeZ;
posVct.copyFromFloats(x, y, z);
x = data[idm + 3];
y = data[idm + 4];
z = data[idm + 5];
Quaternion.RotationYawPitchRollToRef(y, x, z, quat);
sclVct.copyFromFloats(data[idm + 6], data[idm + 7], data[idm + 8]);
composeToRef(sclVct, quat, posVct, mat);
}
instancedBuffer.set(mat, bufferIndex);
if (instanceColorData) {
const idc = ix * colorStride;
const colorBufferIndex = nextFree * 4; // the color instanced buffet offset is 4
tmpCol[0] = instance_colorData[idc]
tmpCol[1] = instance_colorData[idc + 1];
tmpCol[2] = instance_colorData[idc + 2];
tmpCol[3] = instance_colorData[idc + 3];
colorBuffer!!.updateDirectly(tmpCol, colorBufferIndex);
}
available = available - 1;
used = used + 1;
min = (available < nbQuadInstances) ? available : nbQuadInstances;
}
available = (available > 0) ? available : 0;
this._nbAvailableInstancesPerType[t] = available;
}
}
}
}
stepI += lodI;
}
if (seamX && seamXIndex + 1 == stepI) {
seamX = false;
}
if (seamZ && seamZIndex + 1 == stepJ) {
seamZ = false;
}
stepJ += lodJ;
stepI = 0;
}
if (particleMap) {
sps!!.setParticles();
for (let c = 0; c < nbAvailableParticlesPerType.length; c++) {
nbAvailableParticlesPerType[c] = nbPerType[c];
}
}
if (instanceMap && nbAvailableInstancesPerType) {
for (let c = 0; c < nbAvailableInstancesPerType.length; c++) {
nbAvailableInstancesPerType[c] = this._sourceMeshes[c].instances.length;
}
}
// ribbon update
terrain.updateVerticesData(VertexBuffer.PositionKind, positions, false, false);
if (this._computeNormals) {
VertexData.ComputeNormals(positions, this._indices, normals);
}
terrain.updateVerticesData(VertexBuffer.NormalKind, normals, false, false);
terrain.updateVerticesData(VertexBuffer.UVKind, uvs, false, false);
terrain.updateVerticesData(VertexBuffer.ColorKind, colors, false, false);
terrain.getBoundingInfo().reConstruct(bbMin, bbMax, terrain._worldMatrix);
}
// private modulo, for dealing with negative indexes
private _mod(a: number, b: number): number {
return ((a % b) + b) % b;
}
/**
* Updates the mesh terrain size according to the LOD limits and the camera position.
* Returns the terrain.
*/
public updateTerrainSize(): DynamicTerrain {
let remainder = this._terrainSub; // the remaining cells at the general current LOD value
let nb = 0 | 0; // nb of cells in the current LOD limit interval
let next = 0 | 0; // next cell index, if it exists
const LODValue = this._LODValue;
let lod = LODValue + 1; // lod value in the current LOD limit interval
let tsx = 0.0; // current sum of cell sizes on x
let tsz = 0.0; // current sum of cell sizes on z
const LODLimits = this._LODLimits;
const averageSubSizeX = this._averageSubSizeX;
const averageSubSizeZ = this._averageSubSizeZ;
for (let l = 0 | 0; l < LODLimits.length; l++) {
lod = LODValue + l + 1;
next = (l >= LODLimits.length - 1) ? 0 : LODLimits[l + 1];
nb = 2 * (LODLimits[l] - next);
tsx += averageSubSizeX * lod * nb;
tsz += averageSubSizeZ * lod * nb;
remainder -= nb;
}
tsx += remainder * averageSubSizeX * LODValue;
tsz += remainder * averageSubSizeZ * LODValue;
this._terrainSizeX = tsx;
this._terrainSizeZ = tsz;
this._terrainHalfSizeX = tsx * 0.5;
this._terrainHalfSizeZ = tsz * 0.5;
return this;
}
/**
* Returns the altitude (float) at the coordinates (x, z) of the map.
* @param x
* @param z
* @param {normal: Vector3} (optional)
* If the optional object {normal: Vector3} is passed, then its property "normal" is updated with the normal vector value at the coordinates (x, z).
*/
public getHeightFromMap(x: number, z: number, options?: { normal: Vector3 }): number {
return DynamicTerrain._GetHeightFromMap(x, z, this._mapData, this._mapSubX, this._mapSubZ, this._mapSizeX, this._mapSizeZ, options, this._inverted);
}
/**
* Static : Returns the altitude (float) at the coordinates (x, z) of the passed map.
* @param x
* @param z
* @param mapSubX the number of points along the map width
* @param mapSubX the number of points along the map height
* @param {normal: Vector3} (optional)
* @param inverted (optional boolean) is the terrain inverted
* If the optional object {normal: Vector3} is passed, then its property "normal" is updated with the normal vector value at the coordinates (x, z).
*/
public static GetHeightFromMap(x: number, z: number, mapData: number[] | Float32Array, mapSubX: number, mapSubZ: number, options?: { normal: Vector3 }, inverted?: boolean): number {
const mapSizeX = Math.abs(mapData[(mapSubX - 1) * 3] - mapData[0]);
const mapSizeZ = Math.abs(mapData[(mapSubZ - 1) * mapSubX * 3 + 2] - mapData[2]);
return DynamicTerrain._GetHeightFromMap(x, z, mapData, mapSubX, mapSubZ, mapSizeX, mapSizeZ, options, inverted);
}
// Computes the height and optionnally the normal at the coordinates (x ,z) from the passed map
private static _GetHeightFromMap(x: number, z: number, mapData: number[] | Float32Array, mapSubX: number, mapSubZ: number, mapSizeX: number, mapSizeZ: number, options?: { normal: Vector3 }, inverted?: boolean): number {
const x0 = mapData[0];
const z0 = mapData[2];
// reset x and z in the map space so they are between 0 and the map size
x = x - Math.floor((x - x0) / mapSizeX) * mapSizeX;
z = z - Math.floor((z - z0) / mapSizeZ) * mapSizeZ;
const col1 = Math.floor((x - x0) * mapSubX / mapSizeX);
const row1 = Math.floor((z - z0) * mapSubZ / mapSizeZ);
const col2 = (col1 + 1) % mapSubX;
const row2 = (row1 + 1) % mapSubZ;
// starting indexes of the positions of 4 vertices defining a quad on the map
const idx1 = 3 * (row1 * mapSubX + col1);
const idx2 = 3 * (row1 * mapSubX + col2);
const idx3 = 3 * ((row2) * mapSubX + col1);
const idx4 = 3 * ((row2) * mapSubX + col2);
const v1 = DynamicTerrain._v1;
const v2 = DynamicTerrain._v2;
const v3 = DynamicTerrain._v3;
const v4 = DynamicTerrain._v4;
v1.copyFromFloats(mapData[idx1], mapData[idx1 + 1], mapData[idx1 + 2]);
v2.copyFromFloats(mapData[idx2], mapData[idx2 + 1], mapData[idx2 + 2]);
v3.copyFromFloats(mapData[idx3], mapData[idx3 + 1], mapData[idx3 + 2]);
v4.copyFromFloats(mapData[idx4], mapData[idx4 + 1], mapData[idx4 + 2]);
const vAvB = DynamicTerrain._vAvB;
const vAvC = DynamicTerrain._vAvC;
const norm = DynamicTerrain._norm;
const vA = v1;
let vB;
let vC;
let v;
const xv4v1 = v4.x - v1.x;
const zv4v1 = v4.z - v1.z;
if (xv4v1 == 0 || zv4v1 == 0) {
return v1.y;
}
const cd = zv4v1 / xv4v1;
const h = v1.z - cd * v1.x;
if (z < cd * x + h) {
vB = v4;
vC = v2;
v = vA;
}
else {
vB = v3;
vC = v4;
v = vB;
}
vB.subtractToRef(vA, vAvB);
vC.subtractToRef(vA, vAvC);
Vector3.CrossToRef(vAvB, vAvC, norm);
norm.normalize();
if (inverted) {
norm.scaleInPlace(-1.0);
}
if (options && options.normal) {
options.normal.copyFrom(norm);
}
const d = -(norm.x * v.x + norm.y * v.y + norm.z * v.z);
let y = v.y;
if (norm.y != 0.0) {
y = -(norm.x * x + norm.z * z + d) / norm.y;
}
return y;
}
/**
* Static : Computes all the normals from the terrain data map and stores them in the passed Float32Array reference.
* This passed array must have the same size than the mapData array.
*/
public static ComputeNormalsFromMapToRef(mapData: number[] | Float32Array, mapSubX: number, mapSubZ: number, normals: number[] | Float32Array, inverted: boolean): void {
const mapIndices = [];
const tmp1 = { normal: Vector3.Zero() };
const tmp2 = { normal: Vector3.Zero() };
const normal1 = tmp1.normal;
const normal2 = tmp2.normal;
const l = mapSubX * (mapSubZ - 1);
let i = 0;
for (i = 0; i < l; i++) {
mapIndices.push(i + 1, i + mapSubX, i);
mapIndices.push(i + mapSubX, i + 1, i + mapSubX + 1);
}
VertexData.ComputeNormals(mapData, mapIndices, normals);
// seam process
const lastIdx = (mapSubX - 1) * 3;
let colStart = 0;
let colEnd = 0;
const getHeightFromMap = DynamicTerrain.GetHeightFromMap;
for (i = 0; i < mapSubZ; i++) {
colStart = i * mapSubX * 3;
colEnd = colStart + lastIdx;
getHeightFromMap(mapData[colStart], mapData[colStart + 2], mapData, mapSubX, mapSubZ, tmp1);
getHeightFromMap(mapData[colEnd], mapData[colEnd + 2], mapData, mapSubX, mapSubZ, tmp2);
normal1.addInPlace(normal2).scaleInPlace(0.5);
normals[colStart] = normal1.x;
normals[colStart + 1] = normal1.y;
normals[colStart + 2] = normal1.z;
normals[colEnd] = normal1.x;
normals[colEnd + 1] = normal1.y;
normals[colEnd + 2] = normal1.z;
}
// inverted terrain
if (inverted) {
for (i = 0; i < normals.length; i++) {
normals[i] = -normals[i];
}
}
}
/**
* Computes all the map normals from the current terrain data map and sets them to the terrain.
* Returns the terrain.
*/
public computeNormalsFromMap(): DynamicTerrain {
DynamicTerrain.ComputeNormalsFromMapToRef(this._mapData, this._mapSubX, this._mapSubZ, this._mapNormals, this._inverted);
return this;
}
/**
* Returns true if the World coordinates (x, z) are in the current terrain.
* @param x
* @param z
*/
public contains(x: number, z: number): boolean {
const positions = this._positions;
const meshPosition = this.mesh.position;
const terrainIdx = this._terrainIdx;
if (x < positions[0] + meshPosition.x || x > positions[3 * terrainIdx] + meshPosition.x) {
return false;
}
if (z < positions[2] + meshPosition.z || z > positions[3 * terrainIdx * terrainIdx + 2] + meshPosition.z) {
return false;
}
return true;
}
/**
* Static : Returns a new data map from the passed heightmap image file.
The parameters `width` and `height` (positive floats, default 300) set the map width and height sizes.
* `subX` is the wanted number of points along the map width (default 100).
* `subZ` is the wanted number of points along the map height (default 100).
* The parameter `minHeight` (float, default 0) is the minimum altitude of the map.
* The parameter `maxHeight` (float, default 1) is the maximum altitude of the map.
* The parameter `colorFilter` (optional Color3, default (0.3, 0.59, 0.11) ) is the filter to apply to the image pixel colors to compute the height.
* `onReady` is an optional callback function, called once the map is computed. It's passed the computed map.
* `scene` is the Scene object whose database will store the downloaded image.
*/
public static CreateMapFromHeightMap(heightmapURL: string, options: { width: number; height: number; subX: number; subZ: number; minHeight: number; maxHeight: number; offsetX: number; offsetZ: number; onReady?: (map: number[] | Float32Array, subX: number, subZ: number) => void; colorFilter?: Color3 }, scene: Scene): Float32Array {
const subX = options.subX || 100;
const subZ = options.subZ || 100;
const data = new Float32Array(subX * subZ * 3);
DynamicTerrain.CreateMapFromHeightMapToRef(heightmapURL, options, data, scene);
return data;
}
/**
* Static : Updates the passed array or Float32Array with a data map computed from the passed heightmap image file.
* The parameters `width` and `height` (positive floats, default 300) set the map width and height sizes.
* `subX` is the wanted number of points along the map width (default 100).
* `subZ` is the wanted number of points along the map height (default 100).
* The parameter `minHeight` (float, default 0) is the minimum altitude of the map.
* The parameter `maxHeight` (float, default 1) is the maximum altitude of the map.
* The parameter `colorFilter` (optional Color3, default (0.3, 0.59, 0.11) ) is the filter to apply to the image pixel colors to compute the height.
* `onReady` is an optional callback function, called once the map is computed. It's passed the computed map.
* `scene` is the Scene object whose database will store the downloaded image.
* The passed Float32Array must be the right size : 3 x subX x subZ.
*/
public static CreateMapFromHeightMapToRef(heightmapURL: string, options: { width: number; height: number; subX: number; subZ: number; minHeight: number; maxHeight: number; offsetX: number; offsetZ: number; onReady?: (map: number[] | Float32Array, subX: number, subZ: number) => void; colorFilter?: Color3 }, data: number[] | Float32Array, scene: Scene): void {
const width = options.width || 300;
const height = options.height || 300;
const subX = options.subX || 100;
const subZ = options.subZ || 100;
const minHeight = options.minHeight || 0.0;
const maxHeight = options.maxHeight || 10.0;
const offsetX = options.offsetX || 0.0;
const offsetZ = options.offsetZ || 0.0;
const filter = options.colorFilter || new Color3(0.3, 0.59, 0.11);
const onReady = options.onReady;
const onload = (img: CanvasImageSource) => {
// Getting height map data
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const bufferWidth = img.width as number;
const bufferHeight = img.height as number;
canvas.width = bufferWidth;
canvas.height = bufferHeight;
context!!.drawImage(img, 0, 0);
// Cast is due to wrong definition in lib.d.ts from ts 1.3 - https://github.com/Microsoft/TypeScript/issues/949
const buffer = (context!!.getImageData(0, 0, bufferWidth, bufferHeight).data as unknown) as Uint8Array;
let x = 0.0;
let y = 0.0;
let z = 0.0;
for (let row = 0; row < subZ; row++) {
for (let col = 0; col < subX; col++) {
x = col * width / subX - width * 0.5;
z = row * height / subZ - height * 0.5;
const heightmapX = ((x + width * 0.5) / width * (bufferWidth - 1)) | 0;
const heightmapY = (bufferHeight - 1) - ((z + height * 0.5) / height * (bufferHeight - 1)) | 0;
const pos = (heightmapX + heightmapY * bufferWidth) * 4;
const gradient = (buffer[pos] * filter.r + buffer[pos + 1] * filter.g + buffer[pos + 2] * filter.b) / 255.0;
y = minHeight + (maxHeight - minHeight) * gradient;
const idx = (row * subX + col) * 3;
data[idx] = x + offsetX;
data[idx + 1] = y;
data[idx + 2] = z + offsetZ;
}
}
// callback function if any
if (onReady) {
onReady(data, subX, subZ);
}
}
Tools.LoadImage(heightmapURL, onload, () => { }, scene.offlineProvider)
}
/**
* Static : Updates the passed arrays with UVs values to fit the whole map with subX points along its width and subZ points along its height.
* The passed array must be the right size : subX x subZ x 2.
*/
public static CreateUVMapToRef(subX: number, subZ: number, mapUVs: number[] | Float32Array): void {
for (let h = 0; h < subZ; h++) {
for (let w = 0; w < subX; w++) {
mapUVs[(h * subX + w) * 2] = w / (subX - 1);
mapUVs[(h * subX + w) * 2 + 1] = h / (subZ - 1);
}
}
}
/**
* Static : Returns a new UV array with values to fit the whole map with subX points along its width and subZ points along its height.
*/
public static CreateUVMap(subX: number, subZ: number): Float32Array {
const mapUVs = new Float32Array(subX * subZ * 2);
DynamicTerrain.CreateUVMapToRef(subX, subZ, mapUVs);
return mapUVs;
}
/**
* Computes and sets the terrain UV map with values to fit the whole map.
* Returns the terrain.
*/
public createUVMap(): DynamicTerrain {
this.mapUVs = DynamicTerrain.CreateUVMap(this._mapSubX, this._mapSubZ);
return this;
}
/**
* Internal reimplementation of Matrix.ComposeToRef() in order to skip the former call to result._markAsUpdated(), so faster.
*/
private static _ComposeToRef(scale: Vector3, rotation: Quaternion, translation: Vector3, m: Float32Array): void {
const x = rotation.x, y = rotation.y, z = rotation.z, w = rotation.w;
const x2 = x + x, y2 = y + y, z2 = z + z;
const xx = x * x2, xy = x * y2, xz = x * z2;
const yy = y * y2, yz = y * z2, zz = z * z2;
const wx = w * x2, wy = w * y2, wz = w * z2;
const sx = scale.x, sy = scale.y, sz = scale.z;
m[0] = (1 - (yy + zz)) * sx;
m[1] = (xy + wz) * sx;
m[2] = (xz - wy) * sx;
m[3] = 0;
m[4] = (xy - wz) * sy;
m[5] = (1 - (xx + zz)) * sy;
m[6] = (yz + wx) * sy;
m[7] = 0;
m[8] = (xz + wy) * sz;
m[9] = (yz - wx) * sz;
m[10] = (1 - (xx + yy)) * sz;
m[11] = 0;
m[12] = translation.x;
m[13] = translation.y;
m[14] = translation.z;
m[15] = 1;
}
/**
*
* @param source Internal : copies a subpart of the source array to the target array
* @param start
* @param nb
* @param target
*/
private static _CopyArrayValuesFromToRef(source: Float32Array | Array<number>, start: number, nb: number, target: Float32Array | Array<number>) {
for (let i = 0; i < nb; i++) {
target[i] = source[start + i];
}
}
// Getters / Setters
/**
* boolean : if the terrain must be recomputed every frame.
*/
public get refreshEveryFrame(): boolean {
return this._refreshEveryFrame;
}
public set refreshEveryFrame(val: boolean) {
this._refreshEveryFrame = val;
}
/**
* Mesh : the logical terrain underlying mesh
*/
public get mesh(): Mesh {
return this._terrain;
}
/**
* The camera the terrain is linked to
*/
public get camera(): Camera {
return this._terrainCamera;
}
public set camera(val: Camera) {
this._terrainCamera = val;
}
/**
* Number of cells flought over by the cam on the X axis before the terrain is updated.
* Integer greater or equal to 1.
*/
public get subToleranceX(): number {
return this._subToleranceX;
}
public set subToleranceX(val: number) {
this._subToleranceX = (val > 0) ? val : 1;
}
/**
* Number of cells flought over by the cam on the Z axis before the terrain is updated.
* Integer greater or equal to 1. Default 1.
*/
public get subToleranceZ(): number {
return this._subToleranceZ;
}
public set subToleranceZ(val: number) {
this._subToleranceZ = (val > 0) ? val : 1;
}
/**
* Initial LOD factor value.
* Integer greater or equal to 1. Default 1.
*/
public get initialLOD(): number {
return this._initialLOD;
}
public set initialLOD(val: number) {
this._initialLOD = (val > 0) ? val : 1;
}
/**
* Current LOD factor value : the lower factor in the terrain.
* The LOD value is the sum of the initialLOD and the current cameraLODCorrection.
* Integer greater or equal to 1. Default 1.
*/
public get LODValue(): number {
return this._LODValue;
}
/**
* Camera LOD correction : the factor to add to the initial LOD according to the camera position, movement, etc.
* Positive integer (default 0)
*/
public get cameraLODCorrection(): number {
return this._cameraLODCorrection;
}
public set cameraLODCorrection(val: number) {
this._cameraLODCorrection = (val >= 0) ? val : 0;
}
/**
* Boolean : Does the LOD apply only to the terrain right edge ?
* Default : true
*/
public get LODPositiveX(): boolean {
return this._LODPositiveX;
}
public set LODPositiveX(val: boolean) {
this._LODPositiveX = val;
}
/**
* Boolean : Does the LOD apply only to the terrain left edge ?
* Default : true
*/
public get LODNegativeX(): boolean {
return this._LODNegativeX;
}
public set LODNegativeX(val: boolean) {
this._LODNegativeX = val;
}
/**
* Boolean : Does the LOD apply only to the terrain upper edge ?
* Default : true
*/
public get LODPositiveZ(): boolean {
return this._LODPositiveZ;
}
public set LODPositiveZ(val: boolean) {
this._LODPositiveZ = val;
}
/**
* Boolean : Does the LOD apply only to the terrain lower edge ?
* Default : true
*/
public get LODNegativeZ(): boolean {
return this._LODNegativeZ;
}
public set LODNegativeZ(val: boolean) {
this._LODNegativeZ = val;
}
/**
* Average map and terrain subdivision size on X axis.
* Returns a float.
*/
public get averageSubSizeX(): number {
return this._averageSubSizeX;
}
/**
* Average map and terrain subdivision size on Z axis.
* Returns a float.
*/
public get averageSubSizeZ(): number {
return this._averageSubSizeZ;
}
/**
* Current terrain size on the X axis.
* Returns a float.
*/
public get terrainSizeX(): number {
return this._terrainSizeX;
}
/**
* Current terrain half size on the X axis.
* Returns a float.
*/
public get terrainHalfSizeX(): number {
return this._terrainHalfSizeX;
}
/**
* Current terrain size on the Z axis.
* Returns a float.
*/
public get terrainSizeZ(): number {
return this._terrainSizeZ;
}
/**
* Current terrain half size on the Z axis.
* Returns a float.
*/
public get terrainHalfSizeZ(): number {
return this._terrainHalfSizeZ;
}
/**
* Current position of terrain center in its local space.
* Returns a Vector3.
*/
public get centerLocal(): Vector3 {
return this._centerLocal;
}
/**
* Current position of terrain center in the World space.
* Returns a Vector3.
*/
public get centerWorld(): Vector3 {
return this._centerWorld;
}
/**
* The array of the limit values to change the LOD factor.
* Returns an array of integers or an empty array.
* This array is always sorted in the descending order once set.
*/
public get LODLimits(): number[] {
return this._LODLimits;
}
public set LODLimits(ar: number[]) {
ar.sort((a, b) => {
return b - a;
});
this._LODLimits = ar;
}
/**
* The data of the map.
* A flat array (Float32Array recommeded) of successive 3D float coordinates (x, y, z).
* This property can be set only if a mapData array was passed at construction time.
*/
public get mapData(): Float32Array | number[] {
return this._mapData;
}
public set mapData(val: Float32Array | number[]) {
this._mapData = val;
this._datamap = true;
const mapSubX = this._mapSubX;
const mapSubZ = this._mapSubZ;
this._mapSizeX = Math.abs(val[(mapSubX - 1) * 3] - val[0]);
this._mapSizeZ = Math.abs(val[(mapSubZ - 1) * mapSubX * 3 + 2] - val[2]);
this._averageSubSizeX = this._mapSizeX / mapSubX;
this._averageSubSizeZ = this._mapSizeZ / mapSubZ;
if (this._precomputeNormalsFromMap) {
this.computeNormalsFromMap();
}
this.update(true);
}
/**
* The number of points on the map width.
* Positive Integer.
*/
public get mapSubX(): number {
return this._mapSubX;
}
public set mapSubX(val: number) {
this._mapSubX = val;
}
/**
* The number of points on the map height .
* Positive Integer.
*/
public get mapSubZ(): number {
return this._mapSubZ;
}
public set mapSubZ(val: number) {
this._mapSubZ = val;
}
/**
* The map of colors.
* A flat array of successive floats between 0 and 1 as r,g,b values.
* This property can be set only if a mapColors array was passed at construction time.
*/
public get mapColors(): Float32Array | number[] {
return this._mapColors;
}
public set mapColors(val: Float32Array | number[]) {
this._colormap = true;
this._mapColors = val;
}
/**
* The map of UVs.
* A flat array of successive floats between 0 and 1 as (u, v) values.
* This property can be set only if a mapUVs array was passed at construction time.
*/
public get mapUVs(): Float32Array | number[] {
return this._mapUVs;
}
public set mapUVs(val: Float32Array | number[]) {
this._uvmap = true;
this._mapUVs = val;
}
/**
* The map of normals.
* A flat array of successive floats as normal vector coordinates (x, y, z) on each map point.
*/
public get mapNormals(): Float32Array | number[] {
return this._mapNormals;
}
public set mapNormals(val: Float32Array | number[]) {
this._mapNormals = val;
}
/**
* Boolean : must the normals be recomputed on each terrain update (default : false).
* By default, all the map normals are pre-computed on terrain creation.
*/
public get computeNormals(): boolean {
return this._computeNormals;
}
public set computeNormals(val: boolean) {
this._computeNormals = val;
}
/**
* Boolean : will the custom function updateVertex() be called on each terrain update ?
* Default false
*/
public get useCustomVertexFunction(): boolean {
return this._useCustomVertexFunction;
}
public set useCustomVertexFunction(val: boolean) {
this._useCustomVertexFunction = val;
}
/**
* Boolean : is the terrain always directly selected for rendering ?
*/
public get isAlwaysVisible(): boolean {
return this._isAlwaysVisible;
}
public set isAlwaysVisible(val) {
this.mesh.alwaysSelectAsActiveMesh = val;
this._isAlwaysVisible = val;
}
/**
* Boolean : when assigning a new data map to the existing, shall the normals be automatically precomputed once ?
* Default false.
*/
public get precomputeNormalsFromMap(): boolean {
return this._precomputeNormalsFromMap;
}
public set precomputeNormalsFromMap(val) {
this._precomputeNormalsFromMap = val;
}
// ===============================================================
// User custom functions.
// These following can be overwritten bu the user to fit his needs.
/**
* Custom function called for each terrain vertex and passed the :
* - current vertex {position: Vector3, uvs: Vector2, color: Color4, lodX: integer, lodZ: integer, worldPosition: Vector3, mapIndex: integer}
* - i : the vertex index on the terrain x axis
* - j : the vertex index on the terrain x axis
* This function is called only if the property useCustomVertexFunction is set to true.
*/
public updateVertex(vertex: any, i: any, j: any): void {
return;
}
/**
* Custom function called each frame and passed the terrain camera reference.
* This should return a positive integer or zero.
* Returns zero by default.
*/
public updateCameraLOD(terrainCamera: Camera): number {
// LOD value increases with camera altitude
const camLOD = 0;
return camLOD;
}
/**
* Custom function called before each terrain update.
* The value of reference is passed.
* Does nothing by default.
*/
public beforeUpdate(refreshEveryFrame: boolean): void {
return;
}
/**
* Custom function called after each terrain update.
* The value of refreshEveryFrame is passed.
* Does nothing by default.
*/
public afterUpdate(refreshEveryFrame: boolean): void {
return;
}
}
@ilya-lopukhin
Copy link

this is golden

@danielstenson
Copy link
Author

this is golden

Thanks @ilya-lopukhin if you see any areas for improvement, please don't hesitate to let me know.

@fromage9747
Copy link

@danielstenson Thanks for this, just found it.

I am getting Property 'width' does not exist on type 'CanvasImageSource'. Property 'width' does not exist on type 'VideoFrame'. on line 1376 as well as a complaint about height. Will just tell VS Code to ignore for now, but just letting you know!

@danielstenson
Copy link
Author

@danielstenson Thanks for this, just found it.

I am getting Property 'width' does not exist on type 'CanvasImageSource'. Property 'width' does not exist on type 'VideoFrame'. on line 1376 as well as a complaint about height. Will just tell VS Code to ignore for now, but just letting you know!

Thanks for reporting @fromage9747. I haven't looked at this in some time. I'm sure it's a little outdated at this point. I'll try to set aside time in coming months to bring it up-to-date with the original extension and see if that addresses the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment