Chapter 01Understanding Meshes

Meshes are renderable 2D and 3D objects in a scene. We’ll see in subsequent chapters that Babylon.js has a variety of methods that we can use to create standard meshes (e.g. Sphere, Plane, Polygon, and Ribbon) and methods that import assets from files created by modeling software such as Blender. In this section we’re going to investigate how Babylon.js defines meshes as objects and look at how the data in a mesh object is used by the Babylon.js engine to render the object, color it, and light it.

Coordinate Systems

A Babylon.js scene models a 3-dimensional space with the vertical axis as the y-axis and the x and z axis on the horizontal plane. If we position ourselves in the system so that the x-axis’ positive direction is to the right, we have two choices as to how to orient the z-axis; it can be defined so that the direction of +z points away from us or toward us. These two systems are called left-handed and right-handed coordinate systems, respectively. By default, Babylon.js uses a left-handed coordinate system.

left-handed
right-handed

A Babylon.js scene uses a world coordinate system when determining how elements in the scene are projected to a 2D screen. Each mesh in the scene also uses its own local coordinate system when defining its geometry and other data describing the mesh. In the demonstration below, the coordinate system having its origin in the center of the square is the local coordinate system for the square. The other coordinate system is the global coordinate system.

<script>
  var canvas1 = document.getElementById("canvas1");
  var engine1 = new BABYLON.Engine(canvas1, true);

  var createScene1 = function(engine, canvas) {
    var scene = new BABYLON.Scene(engine);
    scene.clearColor = new BABYLON.Color3(1,1,1);
    var camera = new BABYLON.ArcRotateCamera("camera1", -Math.PI/3, 15*Math.PI/32, 16, new BABYLON.Vector3(2,3,4), scene);
    camera.attachControl(canvas, true);
    let light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(1, 1, 0), scene);
    light.intensity = 0.8;

    // create and position the box
    box = BABYLON.MeshBuilder.CreateBox("box", {"size": 2}, scene);
    let mat = new BABYLON.StandardMaterial("mat", scene);
    mat.emissiveColor = new BABYLON.Color3(0.2,0.2,0.2);
    box.material = mat;

    // create local axis and position them to where the position of the box
    let axis_origin = createLocalAxes(4, scene);
    axis_origin.parent = box;

    // translate the position of the box to (2,3,4)
    box.position = new BABYLON.Vector3(2, 3, 4);

    // create world axis
    createAxis(8, scene);

    scene.registerAfterRender(function () {
      rotateCube();
    });

    return scene;
  };

  function rotateCube() {
    let children = box.getChildMeshes(false, (node) => {
        return node.name == "TextPlane";
    });

    for (child of children) {
      let a = Math.PI / 1000;
      let pos = child.position;
      let newX = pos.z *Math.sin(a) + pos.x * Math.cos(a);
      let newZ = pos.z * Math.cos(a) - pos.x * Math.sin(a);
      child.position = new BABYLON.Vector3(newX, pos.y, newZ);
    }

    box.rotate(BABYLON.Axis.Y, Math.PI / 1000, BABYLON.Space.LOCAL);
  }

  function createLocalAxes(size, scene) {
    let [local_axisX, local_axisY, local_axisZ] = createAxis(size, scene);

    let origin = new BABYLON.TransformNode("origin");

    local_axisX.parent = origin;
    local_axisY.parent = origin;
    local_axisZ.parent = origin;

    return origin;
  }

  function createAxis(size, scene) {
    let makeTextPlane = function(text, color, size, camera, billboardMode) {
      let dynamicTexture = new BABYLON.DynamicTexture("DynamicTexture", 50, scene, true);
      dynamicTexture.hasAlpha = true;
      dynamicTexture.drawText(text, 5, 40, "bold 36px Arial", color , "transparent", true);

      let plane = new BABYLON.MeshBuilder.CreatePlane("TextPlane", {size: size, updatable: true}, scene);
      plane.material = new BABYLON.StandardMaterial("TextPlaneMaterial", scene);
      plane.material.specularColor = new BABYLON.Color3(0, 0, 0);
      plane.material.diffuseTexture = dynamicTexture;
      plane.billboardMode = BABYLON.Mesh.BILLBOARDMODE_ALL;

      return plane;
    };

    let axisX = BABYLON.Mesh.CreateLines("axisX", [
      new BABYLON.Vector3.Zero(),
      new BABYLON.Vector3(size, 0, 0),
      new BABYLON.Vector3(size * 0.95, 0.05 * size, 0),
      new BABYLON.Vector3(size, 0, 0),
      new BABYLON.Vector3(size * 0.95, -0.05 * size, 0)
      ], scene);
    axisX.color = new BABYLON.Color3(1, 0, 0);

    let xChar = makeTextPlane("X", "red", size / 8);
    xChar.position = new BABYLON.Vector3(0.9 * size, -0.05 * size, 0);
    xChar.parent = axisX;

    let axisY = BABYLON.Mesh.CreateLines("axisY", [
      new BABYLON.Vector3.Zero(),
      new BABYLON.Vector3(0, size, 0),
      new BABYLON.Vector3( -0.05 * size, size * 0.95, 0),
      new BABYLON.Vector3(0, size, 0),
      new BABYLON.Vector3( 0.05 * size, size * 0.95, 0)
      ], scene);
    axisY.color = new BABYLON.Color3(0, 1, 0);

    let yChar = makeTextPlane("Y", "green", size / 8);
    yChar.position = new BABYLON.Vector3(0, 0.9 * size, -0.05 * size);
    yChar.parent = axisY;

    let axisZ = BABYLON.Mesh.CreateLines("axisZ", [
      new BABYLON.Vector3.Zero(),
      new BABYLON.Vector3(0, 0, size),
      new BABYLON.Vector3( 0 , -0.05 * size, size * 0.95),
      new BABYLON.Vector3(0, 0, size),
      new BABYLON.Vector3( 0, 0.05 * size, size * 0.95)
      ], scene);
    axisZ.color = new BABYLON.Color3(0, 0, 1);

    let zChar = makeTextPlane("Z", "blue", size / 8);
    zChar.position = new BABYLON.Vector3(0, 0.05 * size, 0.9 * size);
    zChar.parent = axisZ;

    return [axisX, axisY, axisZ];
  }

  var scene1 = createScene1(engine1, canvas1);

  engine1.runRenderLoop(function () {
    scene1.render();
  });

  window.addEventListener("resize", function () {
    engine1.resize();
  });

</script>

<canvas id="canvas1" height="300px" width="500px"></canvas>

Facets and Vertices

A mesh is defined by of a set of triangular facets, as shown by the wireframe of the polyhedron shown below. In this example, the polyhedron is constructed with 48 facets. Each one of the facets (triangles) of a mesh is defined by 3 vertices (points). In some cases, facets will share vertices. In the case of the polyhedron shown below, 144 separate vertices are created; 3 vertices for each facet.

In order to render a facet on the screen, Babylon.js needs to know the location of the facet’s vertices, which side of the facet is the front side, and how to color, texture, and shade the facet. All of this information is stored in the mesh object.

<script>
  var canvas2 = document.getElementById("canvas2");
  var engine2 = new BABYLON.Engine(canvas2, true);

  function createScene2 (engine, canvas) {
    let scene = new BABYLON.Scene(engine);
    scene.clearColor = new BABYLON.Color3(0, 0.5, 1);

    let camera = new BABYLON.ArcRotateCamera("Camera", 0, Math.PI/2, 1.5, BABYLON.Vector3.Zero(), scene);
    camera.attachControl(canvas, true);
    let light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(1, 1, 0), scene);

    let sphere = BABYLON.MeshBuilder.CreateSphere("sphere", {"segments": 2}, scene);
    let mat = new BABYLON.StandardMaterial("shereMaterial", scene);
    mat.emissiveColor = BABYLON.Color3.White();
    mat.wireframe = true;
    sphere.material = mat;

    return scene;
  };

  var scene2 = createScene2(engine2, canvas2);

  engine2.runRenderLoop(function () {
    scene2.render();
  });

  window.addEventListener("resize", function () {
    engine2.resize();
  });
</script>

<canvas id="canvas2"></canvas>

Constructing Meshes using VertexData

Consider for example, the 20 x 20 square that sits on the xy-plane, shown below. The square consists of two facets which share two vertices (0 and 2), thus requiring only four vertices in total.

In this example, we construct the mesh by specifying the positions of the vertices, define the vertices used by each facet, assign a color to each vertex, and give Babylon.js information on how to light the pixels in the mesh. After defining this information we’ll store the information in a BABYLON.VertexData object and apply this information to a custom mesh object.

In future sections we’ll see that Babylon.js has a variety of classes and methods that allow us to construct meshes without requiring this low level programming, but for now, lets use VertexData so that we can see the information used by Babylon.js to render a mesh to the screen.

<script>
  var canvas3 = document.getElementById("canvas3");
  var engine3 = new BABYLON.Engine(canvas3, true, { preserveDrawingBuffer: true, stencil: true });

  var createScene = function(engine, canvas) {
      let scene = new BABYLON.Scene(engine);
      scene.clearColor = new BABYLON.Color3(0, 0.5, 1);

      let cam = new BABYLON.ArcRotateCamera("cam", -Math.PI/3, 15*Math.PI/32, 50, new BABYLON.Vector3(0,0,0), scene);
      cam.attachControl(canvas, true);
      scene.activeCameras.push(cam);

      let light = new BABYLON.DirectionalLight("light", new BABYLON.Vector3(0, 0, 1), scene);

      let positions = [
        10, 10, 0,      // (x,y,z) for vertex 0
        -10, 10, 0,     // (x,y,z) for vertex 1
        -10, -10, 0,    // (x,y,z) for vertex 2
        10, -10, 0      // (x,y,z) for vertex 3
      ];

      let indices = [
        0, 1, 2,        // facet 0
        2, 3, 0         // facet 1
      ];

      let colors = [
        1, 0, 0, 1,     // red for vertex 0
        1, 0, 0, 1,     // red for vertex 1
        1, 0, 0, 1,     // red for vertex 2
        0, 1, 0, 1      // green for vertex 3
      ];

      let normals = [
        0, 0, -1,     // <0,0,-1> for vertex 0
        0, 0, -1,     // <0,0,-1> for vertex 1
        0, 0, -1,     // <0,0,-1> for vertex 2
        0, 0, -1      // <0,0,-1> for vertex 3
      ];

      let vertexData = new BABYLON.VertexData();
      vertexData.positions = positions;
      vertexData.indices = indices;
      vertexData.colors = colors;
      vertexData.normals = normals;

      let customMesh = new BABYLON.Mesh("custom", scene);
      vertexData.applyToMesh(customMesh);

      //customMesh.flipFaces(true);

      customMesh.material = new BABYLON.StandardMaterial('mat', scene);
      customMesh.material.specularColor = new BABYLON.Color3(0, 0, 0);
      //customMesh.material.sideOrientation = BABYLON.Material.ClockWiseSideOrientation;

      createPositionableLabels(scene);
      createVertexNormals(scene, customMesh);
      createFacetNormals(scene, customMesh);
      createAxis(16, scene);

      return scene;
  };

  function createPositionableLabels(scene) {
    createPositionableLabel(scene, "0", new BABYLON.Vector3(12,12,0));
    createPositionableLabel(scene, "1", new BABYLON.Vector3(-12,12,0));
    createPositionableLabel(scene, "2", new BABYLON.Vector3(-12,-12,0));
    createPositionableLabel(scene, "3", new BABYLON.Vector3(12,-12,0));
  }

  function createPositionableLabel(scene, text, position) {
    let texture = new BABYLON.DynamicTexture("dtex", 512, scene, true);
    texture.drawText(text, 200, 320, "bold 200px Segoe UI", "white", "transparent", true);
    texture.hasAlpha = true;

    let plane = BABYLON.Mesh.CreatePlane("plane", 10.0, scene, false, BABYLON.Mesh.DEFAULTSIDE);
    plane.position = position;
    plane.billboardMode = BABYLON.Mesh.BILLBOARDMODE_ALL;
    plane.material = new BABYLON.StandardMaterial("mat", scene);
    plane.material.diffuseTexture = texture;
   }

  function createVertexNormals(scene, mesh) {
    let positions = mesh.getVerticesData(BABYLON.VertexBuffer.PositionKind);
  	let normals = mesh.getVerticesData(BABYLON.VertexBuffer.NormalKind);

    for (let p = 0; p < positions.length; p += 3) {
      let start = new BABYLON.Vector3(positions[p], positions[p+1], positions[p+2]);
      let end = new BABYLON.Vector3(positions[p] + normals[p] * 10,
                                    positions[p+1] + normals[p+1] * 10,
                                    positions[p+2] + normals[p+2] * 10);

      BABYLON.MeshBuilder.CreateLines("normal", {points:[start,end], updatable:true}, scene);
    }
  }

  function createFacetNormals(scene, mesh) {
    mesh.updateFacetData();
    var positions = mesh.getFacetLocalPositions();
    var normals = mesh.getFacetLocalNormals();

    var lines = [];
    for (var i = 0; i < positions.length; i++) {
      var line = [ positions[i], positions[i].add(normals[i].scale(5)) ];
      lines.push(line);
    }
    var lineSystem = BABYLON.MeshBuilder.CreateLineSystem("ls", {lines: lines}, scene);
    lineSystem.color = BABYLON.Color3.Black();
  }

  var scene3 = createScene(engine3, canvas3);

  engine3.runRenderLoop(function() {
      scene3.render();
  });

  window.addEventListener("resize", function() {
      engine3.resize();
  });
</script>

<canvas id="canvas3" height="300px" width="500px"></canvas>

Vertex Positions

The initial position of a vertex is defined in the mesh’s local coordinate system and is represented by a triple, denoted (x,y,z). By default, these coordinates do not change, even if the mesh is rotated, scaled, or translated. Transformation vectors are kept track of in the mesh’s world matrix which we’ll discuss in the next section.

The location of the vertices of the square are (10,10,0), (-10,10,0), (-10,-10,0), and (10,-10,0), and are labeled 0 through 3, respectively. When the vertices are defined, their coordinates are stored in an array called a vertex buffer. In the above demonstration, the coordinates of the vertices are stored in the vertex buffer named positions.

let positions = [
  10, 10, 0,      // (x,y,z) for vertex 0
  -10, 10, 0,     // (x,y,z) for vertex 1
  -10, -10, 0,    // (x,y,z) for vertex 2
  10, -10, 0      // (x,y,z) for vertex 3
];

Each vertex is assigned an index, starting at 0, based on where its coordinates are stored in the vertex buffer. For example, the vertex assigned index 0 has its x-coordinate at positions[0], its y-coordinate at positions[1], and its z-coordinate at positions[2]. In general, the vertex assigned index i has its x-coordinate at positions[3i], its y-coordinate at positions[3i+1], and its z-coordinate at positions[3i+2].

Facet Positions

The position of a facet is specified by a triple of vertex indices (v1, v2, v3). In our demonstration there are two facets and the position of one of the facets is specified using the triple (0,1,2) and the position of the other facet is specified using the triple (2,3,0). In the code you’ll see that we specify the positions of the 2 facets in an array named indices as shown below.

let indices = [
  0, 1, 2,        // facet 0
  2, 3, 0         // facet 1
];

Facet Colors

One way to specify how each facet is to be colored is to define a color value for each vertex. Here, a color value is defined by a 4-tuple (r,g,b,a) where r, g, b, and a are values between 0 and 1, representing the red, green, blue, and alpha (transparency) components of the color. In the code we define a color for each vertex in the array named colors. Like the positions array, the colors array holds the color components for each vertex, with the vertex having index 0 occupying the first four elements in the array, the vertex having index 1 occupying the next four elements in the array, and so on.

let colors = [
  1, 0, 0, 1,     // red for vertex 0
  1, 0, 0, 1,     // red for vertex 1
  1, 0, 0, 1,     // red for vertex 2
  0, 1, 0, 1      // green for vertex 3
];

In our square example, the first facet is defined by indices 0, 1, and 2, and the corresponding colors for the vertices are all red. Since each vertex is assigned the color red, Babylon.js uniformly colors the facet red. The second facet is defined by the indices 2, 3, and 0, and the colors assigned to those vertices are red, green, and red, respectively. Since two of the vertices are assigned the color red, and the third is assigned the color green, the facet is rendered with a gradient from the red vertices to the green vertex.

Facet Faces

Each facet has two faces (sides), a front face and a back face. By default, Babylon.js only renders the front faces of the facets. This can be seen, by rotating the square. When you rotate the square to view the other side of the facets you see that they’re not visible.

By default, Babylon.js determines the front face of a facet to be the face where the facet’s vertices, v1, v2 and v3 (as ordered in the indices array), are positioned counterclockwise about the facet. The back face, on the other hand, is the face where v1, v2 and v3 are positioned clockwise about the facet. Since the order of the indices in the indices array determines which side of a facet is the front face, it is important to order them correctly. In the demonstration above, if you follow the vertices of a facet, in the order defined in the indices array, you’ll find that the front faces of the facets face the -z direction.

Facet Normals

A vector defines a direction in a coordinate system, and in a 3D space it is denoted by <x,y,z>. Each facet has a vector associated with it called a normal. Babylon.js computes a normal for each facet in a mesh. Each facet normal is perpendicular to the faces of the facet and points in the direction that the front face of the facet is facing. Below we discuss briefly how the facet normals are computed.

Given the vertices of a facet, v1, v2 and v3, we can compute vectors along the edges of the facet to indicate the direction of the edges. To compute an edge vector from one vertex to another we simple subtract the first vector from the second. The normal of a facet is computed by taking the cross product of two edge vectors, denoted A X B where A and B are the edge vectors. Babylon.js computes facet normals by computing A X B where A is the edge vector from v1 to v2 and B is the edge vector from v3 to v2.

When viewing the square demonstration, you’ll notice two back lines starting at the centers of the facets and projecting in the -z direction. These lines are drawn in the direction defined by the facet normals.

If you’re intrigued by the math and are interested in the computation, please see the method ComputeNormals in Babylon.js’ mesh.vertexData class.

Facet Position

The position of a facet is the location of the facet’s barycenter (a.k.a. centroid). Wikipedia describes it as “the point at which a cutout of the shape could be perfectly balanced on the tip of a pin.” Given the three vertices of a facet (x1,y1,z1), (x2,y2,z2), and (x3,y3,z3), the barycenter is the point at ( (x1+x2+x3)/3, (y1+y2+y3)/3, (z1+z2+z3)/3).

The back lines in the demonstration representing the facet normals are drawn starting at the facets’ barycenters.

Vertex Normals

Each vertex also has a normal vector that can be used by the pixel shaders (see below). In our example, the normals assigned to our vertices are defined in an array named normals as shown below. All of the normals in this example define the same vector, <0,0,-1> which is parallel with the z-axis and point toward -z.

let normals = [
  0, 0, -1,     // <0,0,-1> for vertex 0
  0, 0, -1,     // <0,0,-1> for vertex 1
  0, 0, -1,     // <0,0,-1> for vertex 2
  0, 0, -1      // <0,0,-1> for vertex 3
];

The white lines in the demonstration that are drawn starting at each vertex point are drawn in the direction defined by the vertex normals.

Shaders

Shaders are pieces of code that the GPU executes to project a 3D scene to a 2D screen. BJS uses two types of shaders to render a scene: vertex shaders and pixel (or fragment) shaders. The primary purpose of a vertex shader is to assign to each vertex in a scene a pixel in the 2D screen. The pixel shader uses color, normal, and texture data to determine the colors and luminosity of the pixels.

The pixel shader used in our demonstration uses the angle between a vertex normal and the vector defining the direction of the light source to determine how luminous the pixel associated with the vertex should be. If the angle is 0, (i.e. the vertex normal and the light vector point directly toward one another) the luminosity is maximal - all of the light is reflected back at the light. If the angle is greater or equal to PI radians, then the luminosity is 0 - all of the light is reflected away from the light source.

In our demo, the direction of the light is <0,0,1> and the normals for each vertex are <0,0,-1>. Thus the angle between these vectors is 0 radians, resulting in vivid colors for all of the pixels chosen for the mesh.

Creating Custom Meshes

With the positions of the vertices defined in the array named positions, the indices of the facets defined in the array named indices, the colors assigned to the vertices in the array named colors, and the normals for each vertex defined in the array named normals, we are ready to create the custom square mesh.

First we create an instance of VertexData and set the positions, indices, colors, and normals properties as shown below.

let vertexData = new BABYLON.VertexData();
vertexData.positions = positions;
vertexData.indices = indices;
vertexData.colors = colors;
vertexData.normals = normals;

Next, we call the Mesh constructor and, to wrap it all up, we apply the vertex data to the mesh.

let customMesh = new BABYLON.Mesh("custom", scene);
vertexData.applyToMesh(customMesh);

Explore

This section contained a lot of information and understandably, it may take some time to sink in. We find that our understanding becomes better when experimenting with code, so we hope you take advantage of this online text’s sandbox, before moving on, to experiment with the square mesh demonstration to see the effects of modifying the data in the positions, indices, colors, and normals arrays.

References