Skip to content

BabylonJS - Mesh

This chapter explores creating various 3D shapes, known as meshes, in BabylonJS. We’ll primarily use the modern MeshBuilder API, which offers a convenient and powerful way to generate standard geometric shapes.

MeshBuilder provides methods like CreateBox, CreateSphere, CreateCylinder, etc., allowing you to easily define dimensions, subdivisions, and other properties. It also simplifies applying materials and textures compared to older methods.

Let’s start by creating a simple box (cube) mesh using MeshBuilder.CreateBox.

import * as BABYLON from '@babylonjs/core';
import '@babylonjs/loaders'; // If loading assets
const canvas = document.getElementById('renderCanvas');
const engine = new BABYLON.Engine(canvas, true);
const createScene = async function() {
const scene = new BABYLON.Scene(engine);
scene.clearColor = new BABYLON.Color4(0.1, 0.1, 0.2, 1.0); // Dark blue background
// Create a camera
const camera = new BABYLON.ArcRotateCamera("camera", -Math.PI / 2, Math.PI / 2.5, 10, BABYLON.Vector3.Zero(), scene);
camera.attachControl(canvas, true);
// Create a light
const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(1, 1, 0), scene);
light.intensity = 0.8;
// Create a default PBR material
const boxMaterial = new BABYLON.PBRMaterial("boxMat", scene);
boxMaterial.albedoColor = new BABYLON.Color3(0, 1, 0); // Green color
boxMaterial.metallic = 0.1;
boxMaterial.roughness = 0.7;
// Define box options (optional)
const boxOptions = {
size: 2 // Creates a 2x2x2 box
// width: 1.5,
// height: 2,
// depth: 1,
};
// Create the box mesh
const box = BABYLON.MeshBuilder.CreateBox("myBox", boxOptions, scene);
box.material = boxMaterial;
box.position.y = 1; // Position it slightly above the origin
// Add a ground plane (optional)
const ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 10, height: 10}, scene);
const groundMaterial = new BABYLON.PBRMaterial("groundMat", scene);
groundMaterial.albedoColor = new BABYLON.Color3(0.5, 0.5, 0.5);
groundMaterial.metallic = 0;
groundMaterial.roughness = 0.9;
ground.material = groundMaterial;
return scene;
};
createScene().then(scene => {
engine.runRenderLoop(function() {
if (scene) {
scene.render();
}
});
});
// Handle window resize
window.addEventListener('resize', function() {
engine.resize();
});

This code sets up a basic scene with a camera, light, and then creates a green box using MeshBuilder.CreateBox. We use a PBRMaterial (Physically Based Rendering), which is the modern standard for realistic materials, providing properties like metallic and roughness. The async function pattern is used for createScene to handle potential asynchronous operations like loading assets in the future.

To apply an image texture to a mesh, you need to define how the 2D image coordinates (U, V) map to the 3D surface of the mesh. MeshBuilder handles basic UV mapping automatically for standard shapes.

Let’s modify the previous example to add a texture:

// Inside createScene function...
// Create a PBR material for the textured box
const texturedBoxMat = new BABYLON.PBRMaterial("texturedBoxMat", scene);
texturedBoxMat.metallic = 0.1;
texturedBoxMat.roughness = 0.8;
// Load the texture
const texture = new BABYLON.Texture("path/to/your/texture.jpg", scene);
texturedBoxMat.albedoTexture = texture;
// Box options (size is often sufficient)
const boxOptions = {
size: 2
};
// Create the box mesh
const texturedBox = BABYLON.MeshBuilder.CreateBox("texturedBox", boxOptions, scene);
texturedBox.material = texturedBoxMat;
texturedBox.position.y = 1;
// ... rest of the scene setup ...

Here, we create a BABYLON.Texture object pointing to our image file and assign it to the albedoTexture property of the PBRMaterial. This property defines the base color map for the material.

Advanced UV Mapping (Sprite Sheets / Atlas)

Section titled “Advanced UV Mapping (Sprite Sheets / Atlas)”

Sometimes, you might use a single image file containing multiple smaller images (a sprite sheet or texture atlas). To map specific parts of this atlas to different faces of a mesh (like a box), you can use the faceUV option.

faceUV expects an array of BABYLON.Vector4 objects, one for each face of the mesh (6 for a box). Each Vector4(x, y, z, w) defines the rectangular region of the texture to use for that face, where:

  • x: U coordinate of the bottom-left corner (0.0 to 1.0)
  • y: V coordinate of the bottom-left corner (0.0 to 1.0)
  • z: U coordinate of the top-right corner (0.0 to 1.0)
  • w: V coordinate of the top-right corner (0.0 to 1.0)

Imagine your texture atlas is divided into a grid. If you have a 3x2 grid (3 columns, 2 rows):

// Inside createScene function...
const atlasTexture = new BABYLON.Texture("path/to/your/atlas.png", scene);
const atlasMat = new BABYLON.PBRMaterial("atlasMat", scene);
atlasMat.albedoTexture = atlasTexture;
atlasMat.metallic = 0;
atlasMat.roughness = 1.0; // Less reflective for clear texture view
const hSpriteNb = 3; // Number of horizontal sprites (columns)
const vSpriteNb = 2; // Number of vertical sprites (rows)
const faceUV = new Array(6);
// Example: Map the first 6 sprites from the 3x2 grid to the 6 faces of the box
// Face 0: Top-left sprite (col 0, row 0)
faceUV[0] = new BABYLON.Vector4(0 / hSpriteNb, 0 / vSpriteNb, (0 + 1) / hSpriteNb, (0 + 1) / vSpriteNb);
// Face 1: Middle-top sprite (col 1, row 0)
faceUV[1] = new BABYLON.Vector4(1 / hSpriteNb, 0 / vSpriteNb, (1 + 1) / hSpriteNb, (0 + 1) / vSpriteNb);
// Face 2: Right-top sprite (col 2, row 0)
faceUV[2] = new BABYLON.Vector4(2 / hSpriteNb, 0 / vSpriteNb, (2 + 1) / hSpriteNb, (0 + 1) / vSpriteNb);
// Face 3: Bottom-left sprite (col 0, row 1)
faceUV[3] = new BABYLON.Vector4(0 / hSpriteNb, 1 / vSpriteNb, (0 + 1) / hSpriteNb, (1 + 1) / vSpriteNb);
// Face 4: Bottom-middle sprite (col 1, row 1)
faceUV[4] = new BABYLON.Vector4(1 / hSpriteNb, 1 / vSpriteNb, (1 + 1) / hSpriteNb, (1 + 1) / vSpriteNb);
// Face 5: Bottom-right sprite (col 2, row 1)
faceUV[5] = new BABYLON.Vector4(2 / hSpriteNb, 1 / vSpriteNb, (2 + 1) / hSpriteNb, (1 + 1) / vSpriteNb);
const atlasBoxOptions = {
width: 2,
height: 2,
depth: 2,
faceUV: faceUV
};
const atlasBox = BABYLON.MeshBuilder.CreateBox("atlasBox", atlasBoxOptions, scene);
atlasBox.material = atlasMat;
atlasBox.position.y = 1;
// ... rest of scene setup ...

This technique requires careful calculation of UV coordinates based on your atlas layout. Tools exist to help generate these coordinates automatically from texture packing software.

Creating other shapes like cylinders is similar, using the corresponding MeshBuilder method.

// Inside createScene function...
// Material for the cylinder (e.g., wireframe)
const cylinderMat = new BABYLON.PBRMaterial("cylinderMat", scene);
cylinderMat.albedoColor = new BABYLON.Color3(0.8, 0.8, 1.0); // Light blue
cylinderMat.wireframe = true; // Show wireframe
// Cylinder options
const cylinderOptions = {
height: 4,
diameterTop: 1,
diameterBottom: 2,
tessellation: 24 // Number of segments around the circumference
};
const cylinder = BABYLON.MeshBuilder.CreateCylinder("myCylinder", cylinderOptions, scene);
cylinder.material = cylinderMat;
cylinder.position = new BABYLON.Vector3(-3, 2, 0); // Position it
// ... rest of scene setup ...

Here, CreateCylinder takes options like height, diameterTop, diameterBottom (allowing for cones if diameters differ), and tessellation (controlling smoothness). We’ve applied a wireframe material for visualization.

Explore the BabylonJS documentation for other MeshBuilder methods like CreateSphere, CreatePlane, CreateTorus, CreateIcoSphere, etc., and their respective options.

Detecting when meshes overlap (intersect) is crucial for game logic, physics, and interactions. BabylonJS provides the intersectsMesh method.

// Inside createScene function...
// Create a target mesh (e.g., a static box)
const targetBox = BABYLON.MeshBuilder.CreateBox("target", {size: 3}, scene);
targetBox.position = new BABYLON.Vector3(5, 1.5, 0);
const targetMat = new BABYLON.PBRMaterial("targetMat", scene);
targetMat.albedoColor = new BABYLON.Color3(1, 0.5, 0); // Orange
targetBox.material = targetMat;
// Create moving spheres
const sphereMat = new BABYLON.PBRMaterial("sphereMat", scene);
sphereMat.albedoColor = new BABYLON.Color3(0, 1, 1); // Cyan
const sphere1 = BABYLON.MeshBuilder.CreateSphere("sphere1", {diameter: 1}, scene);
sphere1.material = sphereMat;
sphere1.position = new BABYLON.Vector3(0, 1, 0);
// Animation / Movement Logic
let angle = 0;
scene.onBeforeRenderObservable.add(() => {
// Move sphere1 towards the target box
sphere1.position.x += 0.02;
// Check for intersection
if (sphere1.intersectsMesh(targetBox, false)) {
// Precise check = false (uses bounding boxes first, faster)
// Precise check = true (uses geometry, slower but more accurate)
sphere1.material.emissiveColor = new BABYLON.Color3(1, 0, 0); // Glow red on intersect
targetBox.material.emissiveColor = new BABYLON.Color3(1, 0, 0);
} else {
sphere1.material.emissiveColor = BABYLON.Color3.Black(); // Reset glow
targetBox.material.emissiveColor = BABYLON.Color3.Black();
}
// Simple rotation example (unrelated to intersection)
targetBox.rotation.y += 0.01;
});
// ... rest of scene setup ...

In this example, sphere1 moves towards targetBox. Inside the scene.onBeforeRenderObservable, which runs every frame before rendering, we call sphere1.intersectsMesh(targetBox, false). The second parameter (false) indicates a less precise but faster check using bounding boxes. Setting it to true performs a more accurate check based on the actual mesh geometry. We change the emissiveColor (making the objects glow) to visually indicate intersection.

You can also check if a specific point in 3D space lies within a mesh’s bounds using intersectsPoint.

// Inside scene.onBeforeRenderObservable...
const checkPoint = new BABYLON.Vector3(5.1, 1.6, 0.1); // A point near the target box
// Visualize the point (optional)
// const pointMarker = BABYLON.MeshBuilder.CreateSphere("marker", {diameter: 0.2}, scene);
// pointMarker.position = checkPoint;
// pointMarker.material = new BABYLON.StandardMaterial("markerMat", scene);
// pointMarker.material.emissiveColor = BABYLON.Color3.Yellow();
if (targetBox.intersectsPoint(checkPoint)) {
console.log("Point is inside the target box!");
// You could trigger an action here
}

mesh.intersectsPoint(vector3) returns true if the given BABYLON.Vector3 is inside the mesh’s bounding box, false otherwise.

Picking allows you to determine which mesh (if any) is under the mouse cursor. This is fundamental for user interaction, like selecting objects or placing items in a scene.

BabylonJS uses the scene.onPointerObservable for handling pointer events (mouse clicks, taps, moves).

// Inside createScene function...
// Create a ground plane to click on
const ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 10, height: 10}, scene);
const groundMat = new BABYLON.PBRMaterial("groundMat", scene);
groundMat.albedoColor = new BABYLON.Color3(0.3, 0.5, 0.3); // Greenish
ground.material = groundMat;
// Create a small sphere to place on click
const placementSphere = BABYLON.MeshBuilder.CreateSphere("placementSphere", {diameter: 0.5}, scene);
const placementMat = new BABYLON.PBRMaterial("placementMat", scene);
placementMat.albedoColor = new BABYLON.Color3(1, 0, 0); // Red
placementSphere.material = placementMat;
placementSphere.position = new BABYLON.Vector3(0, -10, 0); // Initially hidden
// Pointer observable for clicks
scene.onPointerObservable.add((pointerInfo) => {
switch (pointerInfo.type) {
case BABYLON.PointerEventTypes.POINTERDOWN:
console.log("Pointer Down");
// Perform a pick operation
const pickResult = scene.pick(scene.pointerX, scene.pointerY);
if (pickResult.hit) {
console.log("Hit mesh:", pickResult.pickedMesh.name);
console.log("Hit point coordinates:", pickResult.pickedPoint);
// If we hit the ground, move the placement sphere to the hit point
if (pickResult.pickedMesh.name === "ground") {
placementSphere.position = pickResult.pickedPoint.clone(); // Use clone()
placementSphere.position.y += 0.25; // Place it slightly above the ground
}
}
break;
case BABYLON.PointerEventTypes.POINTERUP:
console.log("Pointer Up");
break;
case BABYLON.PointerEventTypes.POINTERMOVE:
// Can be used for hover effects, drag and drop, etc.
break;
}
});
// ... rest of scene setup ...

We add an observer to scene.onPointerObservable. When a POINTERDOWN event occurs, we call scene.pick(scene.pointerX, scene.pointerY) to get information about what’s under the cursor at that screen coordinate. The pickResult object contains:

  • hit: A boolean indicating if a mesh was hit.
  • pickedMesh: The mesh that was hit (if hit is true).
  • pickedPoint: A Vector3 representing the exact 3D coordinate on the surface of the pickedMesh where the hit occurred.

In the example, if the ground is clicked (pickResult.pickedMesh.name === "ground"), we move the red placementSphere to the pickedPoint. Note the use of .clone() when assigning the position to avoid modifying the original pickedPoint vector indirectly.

Raycasting involves projecting an invisible line (a ray) from a point in a specific direction and detecting which meshes it intersects. It’s used for things like AI line-of-sight, simulated projectiles, or more complex picking scenarios.

You create a BABYLON.Ray object and use scene.pickWithRay.

// Inside createScene function...
// A central box that will cast the ray
const casterBox = BABYLON.MeshBuilder.CreateBox("casterBox", {size: 1}, scene);
casterBox.position.y = 0.5;
const casterMat = new BABYLON.PBRMaterial("casterMat", scene);
casterMat.albedoColor = new BABYLON.Color3(0.5, 0.5, 1); // Blue
casterBox.material = casterMat;
casterBox.isPickable = false; // Usually, the caster itself shouldn't be picked by its own ray
// Some target boxes
const targetMatRed = new BABYLON.PBRMaterial("targetRed", scene);
targetMatRed.albedoColor = new BABYLON.Color3(1, 0, 0);
const targetMatGreen = new BABYLON.PBRMaterial("targetGreen", scene);
targetMatGreen.albedoColor = new BABYLON.Color3(0, 1, 0);
const target1 = BABYLON.MeshBuilder.CreateBox("target1", {size: 2}, scene);
target1.position = new BABYLON.Vector3(5, 1, 0);
target1.material = targetMatRed;
const target2 = BABYLON.MeshBuilder.CreateBox("target2", {size: 2}, scene);
target2.position = new BABYLON.Vector3(-5, 1, 0);
target2.material = targetMatGreen;
// Ray and RayHelper for visualization
const ray = new BABYLON.Ray();
const rayHelper = new BABYLON.RayHelper(ray);
rayHelper.show(scene, new BABYLON.Color3(1, 1, 0)); // Yellow ray visualization
scene.onBeforeRenderObservable.add(() => {
// Make the caster box rotate
casterBox.rotation.y += 0.01;
// Define the ray properties relative to the caster box
const origin = casterBox.position;
// Get the forward direction based on the caster's rotation
const forward = new BABYLON.Vector3(0, 0, 1); // Local forward Z
const worldMatrix = casterBox.getWorldMatrix();
const direction = BABYLON.Vector3.TransformNormal(forward, worldMatrix).normalize();
const length = 10; // How far the ray should travel
// Update the ray object
ray.origin = origin;
ray.direction = direction;
ray.length = length;
// Perform the raycast
// Optional predicate function to filter meshes:
// const predicate = function(mesh) {
// return mesh.isPickable && mesh.isEnabled() && mesh.name !== "ground";
// }
// const pickInfo = scene.pickWithRay(ray, predicate);
const pickInfo = scene.pickWithRay(ray);
// Reset target colors/effects
target1.scaling.y = 1;
target2.scaling.y = 1;
target1.material.emissiveColor = BABYLON.Color3.Black();
target2.material.emissiveColor = BABYLON.Color3.Black();
if (pickInfo.hit && pickInfo.pickedMesh) {
//console.log("Ray hit:", pickInfo.pickedMesh.name);
// Apply an effect to the hit mesh
pickInfo.pickedMesh.scaling.y = 1.5;
pickInfo.pickedMesh.material.emissiveColor = new BABYLON.Color3(0.8, 0.8, 0); // Yellow glow
}
});
// ... rest of scene setup ...

Key elements:

  • BABYLON.Ray(origin, direction, length): Defines the ray. origin and direction are Vector3, length is a number.
  • BABYLON.RayHelper: A utility to visualize the ray in the scene (useful for debugging).
  • BABYLON.Vector3.TransformNormal(localVector, worldMatrix): Used here to convert the caster’s local forward direction (0,0,1) into world space direction based on its current rotation.
  • scene.pickWithRay(ray, predicate): Performs the cast. It returns a PickingInfo object similar to scene.pick. The optional predicate function receives each potential mesh hit by the ray and should return true if the mesh should be considered pickable by this ray, false otherwise. This allows filtering (e.g., ignoring the ground or invisible objects).

In the example, the central box rotates, and the ray points forward from it. When the ray hits target1 or target2, that target scales up slightly and glows yellow.

Shadows add realism by showing how objects block light. Setting up basic shadows involves a light source capable of casting shadows and a ShadowGenerator.

// Inside createScene function...
// Create a light capable of casting shadows (Directional or SpotLight)
const light = new BABYLON.DirectionalLight("dirLight", new BABYLON.Vector3(-0.5, -1, -0.5), scene);
light.position = new BABYLON.Vector3(10, 20, 10);
light.intensity = 0.8;
// Create a Shadow Generator
// The first parameter is the shadow map size (power of 2, e.g., 1024, 2048)
// Higher values mean sharper shadows but use more resources.
const shadowGenerator = new BABYLON.ShadowGenerator(1024, light);
// Configure shadow properties (optional)
shadowGenerator.useBlurExponentialShadowMap = true; // Softer shadows (other options exist)
// shadowGenerator.usePoissonSampling = true;
shadowGenerator.blurKernel = 32;
shadowGenerator.darkness = 0.5; // How dark the shadows are
// shadowGenerator.bias = 0.01; // Helps prevent shadow acne (self-shadowing artifacts)
// Create a ground that receives shadows
const ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 10, height: 10}, scene);
const groundMat = new BABYLON.PBRMaterial("groundMat", scene);
groundMat.albedoColor = new BABYLON.Color3(0.5, 0.5, 0.5);
// *** Crucial: Enable shadow receiving on the ground material ***
// ground.receiveShadows = true; // For StandardMaterial
// PBRMaterial handles receiving automatically if lighting is enabled.
// If you are using PBR, ensure scene environment texture or lights are set up correctly.
// For simplicity here, we might force it with StandardMaterial or ensure PBR setup is complete.
// Let's use StandardMaterial for a clear receiveShadows example:
const groundStdMat = new BABYLON.StandardMaterial("groundStdMat", scene);
groundStdMat.diffuseColor = new BABYLON.Color3(0.5, 0.5, 0.5);
ground.material = groundStdMat;
ground.receiveShadows = true; // Enable shadow receiving
// Create a box that casts shadows
const shadowBox = BABYLON.MeshBuilder.CreateBox("shadowBox", {size: 2}, scene);
shadowBox.position = new BABYLON.Vector3(0, 1.1, 0);
const boxMat = new BABYLON.PBRMaterial("boxMat", scene);
boxMat.albedoColor = new BABYLON.Color3(1, 0, 0); // Red
shadowBox.material = boxMat;
// *** Add the box to the shadow caster list ***
shadowGenerator.addShadowCaster(shadowBox);
// Or add multiple meshes: shadowGenerator.getShadowMap().renderList.push(mesh1, mesh2);
// Create another object casting shadow (e.g., sphere)
const shadowSphere = BABYLON.MeshBuilder.CreateSphere("shadowSphere", {diameter: 1.5}, scene);
shadowSphere.position = new BABYLON.Vector3(3, 0.8, 2);
const sphereMat = new BABYLON.PBRMaterial("sphereMat", scene);
sphereMat.albedoColor = new BABYLON.Color3(0, 0, 1); // Blue
shadowSphere.material = sphereMat;
shadowGenerator.addShadowCaster(shadowSphere);
// Animate the box (optional)
scene.onBeforeRenderObservable.add(() => {
shadowBox.rotation.y += 0.01;
shadowBox.rotation.x += 0.005;
});
// ... rest of scene setup (camera needed) ...
const camera = new BABYLON.ArcRotateCamera("camera", -Math.PI / 2.2, Math.PI / 2.5, 15, new BABYLON.Vector3(0,1,0), scene);
camera.attachControl(canvas, true);

Shadow Setup Steps:

  1. Create a suitable light (DirectionalLight or SpotLight). PointLight can cast shadows but requires cube textures (more complex). HemisphericLight cannot cast shadows.
  2. Instantiate BABYLON.ShadowGenerator, passing the shadow map size and the light.
  3. For meshes that should receive shadows (like the ground), set mesh.receiveShadows = true;. (Note: This is mainly for StandardMaterial. PBRMaterial typically handles this based on lighting setup, but explicit setting might be needed in some configurations).
  4. For meshes that should cast shadows, add them to the shadow generator’s list using shadowGenerator.addShadowCaster(mesh) or by pushing to shadowGenerator.getShadowMap().renderList.
  5. Adjust shadow generator properties (useBlurExponentialShadowMap, blurKernel, darkness, bias) to fine-tune the appearance and performance.

Beyond basic creation and interaction, BabylonJS offers many advanced mesh features:

  • Advanced Materials: Explore PBRMaterial properties (metallic, roughness, clearcoat, subsurface scattering), NodeMaterial for custom visual shader creation, and ShaderMaterial for coding shaders directly in GLSL.
  • Level of Detail (LOD): Display simpler versions of a mesh when it’s far from the camera to improve performance (mesh.addLODLevel).
  • Instances: Efficiently render many identical copies of a mesh with variations in position, rotation, and scale (mesh.createInstance).
  • Morph Targets: Deform a mesh smoothly between different shapes, often used for facial animation or shape-shifting effects.
  • Asset Loading: Import complex meshes created in 3D modeling software (like Blender, Maya) using formats like glTF/GLB (SceneLoader.ImportMeshAsync).
  • Solid Particle System (SPS): Manage and animate thousands of meshes (particles) efficiently.
  • Highlight Layer: Easily add outlines or highlights to meshes for selection or emphasis (HighlightLayer).

These topics represent the next steps in mastering mesh manipulation and rendering in BabylonJS. Refer to the official BabylonJS documentation for in-depth explanations and examples.