@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
428 lines • 21.4 kB
JavaScript
import { Vector3 } from "../../Maths/math.vector.js";
import { Color3 } from "../../Maths/math.color.js";
import { Mesh } from "../mesh.js";
import { VertexData } from "../mesh.vertexData.js";
import { GroundMesh } from "../groundMesh.js";
import { Tools } from "../../Misc/tools.js";
import { EngineStore } from "../../Engines/engineStore.js";
import { Epsilon } from "../../Maths/math.constants.js";
import { useOpenGLOrientationForUV } from "../../Compat/compatibilityOptions.js";
/**
* Creates the VertexData for a Ground
* @param options an object used to set the following optional parameters for the Ground, required but can be empty
* @param options.width the width (x direction) of the ground, optional, default 1
* @param options.height the height (z direction) of the ground, optional, default 1
* @param options.subdivisions the number of subdivisions per side, optional, default 1
* @param options.subdivisionsX the number of subdivisions in the x direction, overrides options.subdivisions, optional, default undefined
* @param options.subdivisionsY the number of subdivisions in the y direction, overrides options.subdivisions, optional, default undefined
* @returns the VertexData of the Ground
*/
export function CreateGroundVertexData(options) {
const indices = [];
const positions = [];
const normals = [];
const uvs = [];
let row, col;
const width = options.width || options.size || 1;
const height = options.height || options.size || 1;
const subdivisionsX = (options.subdivisionsX || options.subdivisions || 1) | 0;
const subdivisionsY = (options.subdivisionsY || options.subdivisions || 1) | 0;
for (row = 0; row <= subdivisionsY; row++) {
for (col = 0; col <= subdivisionsX; col++) {
const position = new Vector3((col * width) / subdivisionsX - width / 2.0, 0, ((subdivisionsY - row) * height) / subdivisionsY - height / 2.0);
const normal = new Vector3(0, 1.0, 0);
positions.push(position.x, position.y, position.z);
normals.push(normal.x, normal.y, normal.z);
uvs.push(col / subdivisionsX, useOpenGLOrientationForUV ? row / subdivisionsY : 1.0 - row / subdivisionsY);
}
}
for (row = 0; row < subdivisionsY; row++) {
for (col = 0; col < subdivisionsX; col++) {
indices.push(col + 1 + (row + 1) * (subdivisionsX + 1));
indices.push(col + 1 + row * (subdivisionsX + 1));
indices.push(col + row * (subdivisionsX + 1));
indices.push(col + (row + 1) * (subdivisionsX + 1));
indices.push(col + 1 + (row + 1) * (subdivisionsX + 1));
indices.push(col + row * (subdivisionsX + 1));
}
}
// Result
const vertexData = new VertexData();
vertexData.indices = indices;
vertexData.positions = positions;
vertexData.normals = normals;
vertexData.uvs = uvs;
return vertexData;
}
/**
* Creates the VertexData for a TiledGround by subdividing the ground into tiles
* @param options an object used to set the following optional parameters for the Ground
* @param options.xmin ground minimum X coordinate, default -1
* @param options.zmin ground minimum Z coordinate, default -1
* @param options.xmax ground maximum X coordinate, default 1
* @param options.zmax ground maximum Z coordinate, default 1
* @param options.subdivisions a javascript object {w: positive integer, h: positive integer}, `w` and `h` are the numbers of subdivisions on the ground width and height creating 'tiles', default {w: 6, h: 6}
* @param options.subdivisions.w positive integer, default 6
* @param options.subdivisions.h positive integer, default 6
* @param options.precision a javascript object {w: positive integer, h: positive integer}, `w` and `h` are the numbers of subdivisions on the tile width and height, default {w: 2, h: 2}
* @param options.precision.w positive integer, default 2
* @param options.precision.h positive integer, default 2
* @returns the VertexData of the TiledGround
*/
export function CreateTiledGroundVertexData(options) {
const xmin = options.xmin !== undefined && options.xmin !== null ? options.xmin : -1.0;
const zmin = options.zmin !== undefined && options.zmin !== null ? options.zmin : -1.0;
const xmax = options.xmax !== undefined && options.xmax !== null ? options.xmax : 1.0;
const zmax = options.zmax !== undefined && options.zmax !== null ? options.zmax : 1.0;
const subdivisions = options.subdivisions || { w: 1, h: 1 };
const precision = options.precision || { w: 1, h: 1 };
const indices = [];
const positions = [];
const normals = [];
const uvs = [];
let row, col, tileRow, tileCol;
subdivisions.h = subdivisions.h < 1 ? 1 : subdivisions.h;
subdivisions.w = subdivisions.w < 1 ? 1 : subdivisions.w;
precision.w = precision.w < 1 ? 1 : precision.w;
precision.h = precision.h < 1 ? 1 : precision.h;
const tileSize = {
w: (xmax - xmin) / subdivisions.w,
h: (zmax - zmin) / subdivisions.h,
};
function applyTile(xTileMin, zTileMin, xTileMax, zTileMax) {
// Indices
const base = positions.length / 3;
const rowLength = precision.w + 1;
for (row = 0; row < precision.h; row++) {
for (col = 0; col < precision.w; col++) {
const square = [base + col + row * rowLength, base + (col + 1) + row * rowLength, base + (col + 1) + (row + 1) * rowLength, base + col + (row + 1) * rowLength];
indices.push(square[1]);
indices.push(square[2]);
indices.push(square[3]);
indices.push(square[0]);
indices.push(square[1]);
indices.push(square[3]);
}
}
// Position, normals and uvs
const position = Vector3.Zero();
const normal = new Vector3(0, 1.0, 0);
for (row = 0; row <= precision.h; row++) {
position.z = (row * (zTileMax - zTileMin)) / precision.h + zTileMin;
for (col = 0; col <= precision.w; col++) {
position.x = (col * (xTileMax - xTileMin)) / precision.w + xTileMin;
position.y = 0;
positions.push(position.x, position.y, position.z);
normals.push(normal.x, normal.y, normal.z);
uvs.push(col / precision.w, row / precision.h);
}
}
}
for (tileRow = 0; tileRow < subdivisions.h; tileRow++) {
for (tileCol = 0; tileCol < subdivisions.w; tileCol++) {
applyTile(xmin + tileCol * tileSize.w, zmin + tileRow * tileSize.h, xmin + (tileCol + 1) * tileSize.w, zmin + (tileRow + 1) * tileSize.h);
}
}
// Result
const vertexData = new VertexData();
vertexData.indices = indices;
vertexData.positions = positions;
vertexData.normals = normals;
vertexData.uvs = uvs;
return vertexData;
}
/**
* Creates the VertexData of the Ground designed from a heightmap
* @param options an object used to set the following parameters for the Ground, required and provided by CreateGroundFromHeightMap
* @param options.width the width (x direction) of the ground
* @param options.height the height (z direction) of the ground
* @param options.subdivisions the number of subdivisions per side
* @param options.minHeight the minimum altitude on the ground, optional, default 0
* @param options.maxHeight the maximum altitude on the ground, optional default 1
* @param options.colorFilter the filter to apply to the image pixel colors to compute the height, optional Color3, default (0.3, 0.59, 0.11)
* @param options.buffer the array holding the image color data
* @param options.bufferWidth the width of image
* @param options.bufferHeight the height of image
* @param options.alphaFilter Remove any data where the alpha channel is below this value, defaults 0 (all data visible)
* @param options.heightBuffer a array of floats where the height data can be saved, if its length is greater than zero.
* @returns the VertexData of the Ground designed from a heightmap
*/
export function CreateGroundFromHeightMapVertexData(options) {
const indices = [];
const positions = [];
const normals = [];
const uvs = [];
let row, col;
const filter = options.colorFilter || new Color3(0.3, 0.59, 0.11);
const alphaFilter = options.alphaFilter || 0.0;
let invert = false;
if (options.minHeight > options.maxHeight) {
invert = true;
const temp = options.maxHeight;
options.maxHeight = options.minHeight;
options.minHeight = temp;
}
// Vertices
for (row = 0; row <= options.subdivisions; row++) {
for (col = 0; col <= options.subdivisions; col++) {
const position = new Vector3((col * options.width) / options.subdivisions - options.width / 2.0, 0, ((options.subdivisions - row) * options.height) / options.subdivisions - options.height / 2.0);
// Compute height
const heightMapX = (((position.x + options.width / 2) / options.width) * (options.bufferWidth - 1)) | 0;
const heightMapY = ((1.0 - (position.z + options.height / 2) / options.height) * (options.bufferHeight - 1)) | 0;
const pos = (heightMapX + heightMapY * options.bufferWidth) * 4;
let r = options.buffer[pos] / 255.0;
let g = options.buffer[pos + 1] / 255.0;
let b = options.buffer[pos + 2] / 255.0;
const a = options.buffer[pos + 3] / 255.0;
if (invert) {
r = 1.0 - r;
g = 1.0 - g;
b = 1.0 - b;
}
const gradient = r * filter.r + g * filter.g + b * filter.b;
// If our alpha channel is not within our filter then we will assign a 'special' height
// Then when building the indices, we will ignore any vertex that is using the special height
if (a >= alphaFilter) {
position.y = options.minHeight + (options.maxHeight - options.minHeight) * gradient;
}
else {
position.y = options.minHeight - Epsilon; // We can't have a height below minHeight, normally.
}
if (options.heightBuffer) {
// set the height buffer information in row major order.
options.heightBuffer[row * (options.subdivisions + 1) + col] = position.y;
}
// Add vertex
positions.push(position.x, position.y, position.z);
normals.push(0, 0, 0);
uvs.push(col / options.subdivisions, 1.0 - row / options.subdivisions);
}
}
// Indices
for (row = 0; row < options.subdivisions; row++) {
for (col = 0; col < options.subdivisions; col++) {
// Calculate Indices
const idx1 = col + 1 + (row + 1) * (options.subdivisions + 1);
const idx2 = col + 1 + row * (options.subdivisions + 1);
const idx3 = col + row * (options.subdivisions + 1);
const idx4 = col + (row + 1) * (options.subdivisions + 1);
// Check that all indices are visible (based on our special height)
// Only display the vertex if all Indices are visible
// Positions are stored x,y,z for each vertex, hence the * 3 and + 1 for height
const isVisibleIdx1 = positions[idx1 * 3 + 1] >= options.minHeight;
const isVisibleIdx2 = positions[idx2 * 3 + 1] >= options.minHeight;
const isVisibleIdx3 = positions[idx3 * 3 + 1] >= options.minHeight;
if (isVisibleIdx1 && isVisibleIdx2 && isVisibleIdx3) {
indices.push(idx1);
indices.push(idx2);
indices.push(idx3);
}
const isVisibleIdx4 = positions[idx4 * 3 + 1] >= options.minHeight;
if (isVisibleIdx4 && isVisibleIdx1 && isVisibleIdx3) {
indices.push(idx4);
indices.push(idx1);
indices.push(idx3);
}
}
}
// Normals
VertexData.ComputeNormals(positions, indices, normals);
// Result
const vertexData = new VertexData();
vertexData.indices = indices;
vertexData.positions = positions;
vertexData.normals = normals;
vertexData.uvs = uvs;
return vertexData;
}
/**
* Creates a ground mesh
* @param name defines the name of the mesh
* @param options defines the options used to create the mesh
* @param options.width set the width size (float, default 1)
* @param options.height set the height size (float, default 1)
* @param options.subdivisions sets the number of subdivision per side (default 1)
* @param options.subdivisionsX sets the number of subdivision on the X axis (overrides subdivisions)
* @param options.subdivisionsY sets the number of subdivision on the Y axis (overrides subdivisions)
* @param options.updatable defines if the mesh must be flagged as updatable (default false)
* @param scene defines the hosting scene
* @returns the ground mesh
* @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/set#ground
*/
export function CreateGround(name, options = {}, scene) {
const ground = new GroundMesh(name, scene);
ground._setReady(false);
ground._subdivisionsX = options.subdivisionsX || options.subdivisions || 1;
ground._subdivisionsY = options.subdivisionsY || options.subdivisions || 1;
ground._width = options.width || 1;
ground._height = options.height || 1;
ground._maxX = ground._width / 2;
ground._maxZ = ground._height / 2;
ground._minX = -ground._maxX;
ground._minZ = -ground._maxZ;
const vertexData = CreateGroundVertexData(options);
vertexData.applyToMesh(ground, options.updatable);
ground._setReady(true);
return ground;
}
/**
* Creates a tiled ground mesh
* @param name defines the name of the mesh
* @param options defines the options used to create the mesh
* @param options.xmin ground minimum X coordinate (float, default -1)
* @param options.zmin ground minimum Z coordinate (float, default -1)
* @param options.xmax ground maximum X coordinate (float, default 1)
* @param options.zmax ground maximum Z coordinate (float, default 1)
* @param options.subdivisions a javascript object `{w: positive integer, h: positive integer}` (default `{w: 6, h: 6}`). `w` and `h` are the numbers of subdivisions on the ground width and height. Each subdivision is called a tile
* @param options.subdivisions.w positive integer, default 6
* @param options.subdivisions.h positive integer, default 6
* @param options.precision a javascript object `{w: positive integer, h: positive integer}` (default `{w: 2, h: 2}`). `w` and `h` are the numbers of subdivisions on the ground width and height of each tile
* @param options.precision.w positive integer, default 2
* @param options.precision.h positive integer, default 2
* @param options.updatable boolean, default false, true if the mesh must be flagged as updatable
* @param scene defines the hosting scene
* @returns the tiled ground mesh
* @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/set#tiled-ground
*/
export function CreateTiledGround(name, options, scene = null) {
const tiledGround = new Mesh(name, scene);
const vertexData = CreateTiledGroundVertexData(options);
vertexData.applyToMesh(tiledGround, options.updatable);
return tiledGround;
}
/**
* Creates a ground mesh from a height map. The height map download can take some frames,
* so the mesh is not immediately ready. To wait for the mesh to be completely built,
* you should use the `onReady` callback option.
* @param name defines the name of the mesh
* @param url sets the URL of the height map image resource.
* @param options defines the options used to create the mesh
* @param options.width sets the ground width size (positive float, default 10)
* @param options.height sets the ground height size (positive float, default 10)
* @param options.subdivisions sets the number of subdivision per side (positive integer, default 1)
* @param options.minHeight is the minimum altitude on the ground (float, default 0)
* @param options.maxHeight is the maximum altitude on the ground (float, default 1)
* @param options.colorFilter is the filter to apply to the image pixel colors to compute the height (optional Color3, default (0.3, 0.59, 0.11) )
* @param options.alphaFilter will filter any data where the alpha channel is below this value, defaults 0 (all data visible)
* @param options.updatable defines if the mesh must be flagged as updatable
* @param options.onReady is a javascript callback function that will be called once the mesh is just built (the height map download can last some time)
* @param options.onError is a javascript callback function that will be called if there is an error
* @param options.passHeightBufferInCallback a boolean that indicates if the calculated height data will be passed in the onReady callback. Useful if you need the height data for physics, for example.
* @param scene defines the hosting scene
* @returns the ground mesh
* @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/set/height_map
* @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/set#ground-from-a-height-map
*/
export function CreateGroundFromHeightMap(name, url, options = {}, scene = null) {
const width = options.width || 10.0;
const height = options.height || 10.0;
const subdivisions = options.subdivisions || 1 | 0;
const minHeight = options.minHeight || 0.0;
const maxHeight = options.maxHeight || 1.0;
const filter = options.colorFilter || new Color3(0.3, 0.59, 0.11);
const alphaFilter = options.alphaFilter || 0.0;
const updatable = options.updatable;
const onReady = options.onReady;
scene = scene || EngineStore.LastCreatedScene;
const ground = new GroundMesh(name, scene);
ground._subdivisionsX = subdivisions;
ground._subdivisionsY = subdivisions;
ground._width = width;
ground._height = height;
ground._maxX = ground._width / 2.0;
ground._maxZ = ground._height / 2.0;
ground._minX = -ground._maxX;
ground._minZ = -ground._maxZ;
ground._setReady(false);
let heightBuffer;
if (options.passHeightBufferInCallback) {
heightBuffer = new Float32Array((subdivisions + 1) * (subdivisions + 1));
}
const onBufferLoaded = (buffer, bufferWidth, bufferHeight) => {
const vertexData = CreateGroundFromHeightMapVertexData({
width: width,
height: height,
subdivisions: subdivisions,
minHeight: minHeight,
maxHeight: maxHeight,
colorFilter: filter,
buffer: buffer,
bufferWidth: bufferWidth,
bufferHeight: bufferHeight,
alphaFilter: alphaFilter,
heightBuffer,
});
vertexData.applyToMesh(ground, updatable);
//execute ready callback, if set
if (onReady) {
onReady(ground, heightBuffer);
}
ground._setReady(true);
};
if (typeof url === "string") {
const onload = (img) => {
const bufferWidth = img.width;
const bufferHeight = img.height;
if (scene.isDisposed) {
return;
}
const buffer = scene?.getEngine().resizeImageBitmap(img, bufferWidth, bufferHeight);
onBufferLoaded(buffer, bufferWidth, bufferHeight);
};
Tools.LoadImage(url, onload, options.onError ? options.onError : () => { }, scene.offlineProvider);
}
else {
onBufferLoaded(url.data, url.width, url.height);
}
return ground;
}
/**
* Class containing static functions to help procedurally build meshes
* @deprecated use the functions directly from the module
*/
export const GroundBuilder = {
// eslint-disable-next-line @typescript-eslint/naming-convention
CreateGround,
// eslint-disable-next-line @typescript-eslint/naming-convention
CreateGroundFromHeightMap,
// eslint-disable-next-line @typescript-eslint/naming-convention
CreateTiledGround,
};
VertexData.CreateGround = CreateGroundVertexData;
VertexData.CreateTiledGround = CreateTiledGroundVertexData;
VertexData.CreateGroundFromHeightMap = CreateGroundFromHeightMapVertexData;
Mesh.CreateGround = (name, width, height, subdivisions, scene, updatable) => {
const options = {
width,
height,
subdivisions,
updatable,
};
return CreateGround(name, options, scene);
};
Mesh.CreateTiledGround = (name, xmin, zmin, xmax, zmax, subdivisions, precision, scene, updatable) => {
const options = {
xmin,
zmin,
xmax,
zmax,
subdivisions,
precision,
updatable,
};
return CreateTiledGround(name, options, scene);
};
Mesh.CreateGroundFromHeightMap = (name, url, width, height, subdivisions, minHeight, maxHeight, scene, updatable, onReady, alphaFilter) => {
const options = {
width,
height,
subdivisions,
minHeight,
maxHeight,
updatable,
onReady,
alphaFilter,
};
return CreateGroundFromHeightMap(name, url, options, scene);
};
//# sourceMappingURL=groundBuilder.js.map