This sample gives an overview of what can be done with 3D Mesh geometry primitives. It also introduces some computer graphics terminology that is helpful to understand when working with meshes. Mesh primitives are easy to create and can be modified to create more complex geometries.
When creating a new Mesh you need to describe its vertex attributes, vertex space and components. Here's a brief explanation of the terms (for more details check Read more
section of Mesh):
Vertex: a point in 3D space that defines the position of a 3D point on the surface of the mesh, represented by its x, y, and z coordinates.
Vertex Space: a coordinate system in which the vertices of the mesh are defined. This space is important for defining how the mesh is positioned and oriented in 3D space and where is its origin.
Components: allow defining different materials for different parts of the mesh as they separate the mesh by reusing vertices across different faces.
Faces: triangles that make up the mesh, which are specified by specifying which 3 vertices should be connected in a closed loop.
UV: coordinates representing the texture mapping on a mesh, where each pair corresponds to a vertex on the mesh. They range from 0 to 1 (as they are normalized), with (0,0) representing the lower left corner and (1,1) the upper right corner of the texture.
1. Create
There are several ways to create the Mesh. Mesh provides functionality to create basic shapes such as a box, cylinder, plane, or sphere, as shown in the box example below. Just like point or polygon geometries, a mesh is displayed by using it as the geometry of a Graphic.
// Box
const boxMesh = Mesh.createBox(point, {
size: { width: 1, depth: 1, height: 10 },
material: {
color: [58, 38, 0, 1]
}
});
const box = new Graphic({
geometry: boxMesh,
symbol: emptyMeshSymbol // symbology is explained in other section below
});
If the shape you want to create is more complex you can specify the shape by declaring the origin of the vertexSpace and then the relative position of all the vertices.
function createPyramid(location, { material, size }) {
const { height, width, depth } = size;
const halfWidth = width / 2;
const halfDepth = depth / 2;
// adding 10 to the placement position to create the pyramid next to the box
const origin = [location.x + 10, location.y, location.z];
// specifying vertices, where each 3 numbers are one point
const position = [
0, 0, height, // top vertex of the pyramid
-halfWidth, -halfDepth, 0, // base vertex of the pyramid
halfWidth, -halfDepth, 0, // base vertex of the pyramid
halfWidth, halfDepth, 0, // base vertex of the pyramid
-halfWidth, halfDepth, 0 // base vertex of the pyramid
];
// coordinates representing the texture mapping on a mesh
const uv = [0.5, 0, 0, 1, 1, 1, 0, 1, 1, 1];
const pyramid = new Mesh({
vertexSpace: new MeshLocalVertexSpace({ origin }),
vertexAttributes: { position, uv},
components: [
{ faces: [0, 1, 2], material },
{ faces: [0, 2, 3], material },
{ faces: [0, 3, 4], material },
{ faces: [0, 4, 1], material }
],
spatialReference: location.spatialReference
});
return pyramid;
}
// creating a new mesh using a function defined above
const pyramidMesh = createPyramid(point, {
size: { width: 7, depth: 7, height: 6 },
material: new MeshMaterial({
color: [60, 87, 49, 1]
})
});
To learn how to create meshes by loading external files, have a look at the Import glTF 3D Models sample.
2. Modify
Meshes have a couple of methods to manipulate them, like offset, rotate or scale. These operations modify the existing mesh geometry, so you need to clone the original mesh first if you want to keep it unchanged. You can build more complex geometries by merging simple meshes into a single new geometry. In the sample, it is used to create a Mesh representing a tree.
const treeMesh = meshUtils.merge([
boxMesh.clone().offset(10, 0, 10),
pyramidMesh.clone().offset(0, 0, 20),
pyramidMesh.clone().offset(0, 0, 22).scale(0.75),
pyramidMesh.clone().offset(0, 0, 24).scale(0.5)
]);
Once you have your target object, such as a tree in this case, you may want to reuse it in several locations. To place instances of the same mesh multiple times, you can clone it and offset it at random locations in the given extent. You could also specify placements using points with fixed coordinates. Note that this duplication approach should be used for millions of meshes.
function duplicateModels(mesh, amount, extentX, extentY) {
graphicsLayer.removeAll(); // clear existing trees
for (let i = 0; i < amount; i++) {
let tree = new Graphic({
geometry: mesh
.clone()
.offset(
-(Math.floor(Math.random() * extentX) - Math.random() * extentX),
-(Math.floor(Math.random() * extentY) - Math.random() * extentY),
0
), // generate random offset within given extent
symbol: emptyMeshSymbol
});
graphicsLayer.add(tree);
}
}
3. Style
Finally, you can change the style of the mesh and it's components. You can adjust various aspects such as colors, textures, and edge styles. Some changes can be applied directly to the mesh material, but it is also possible to use symbols to control coloring and edge properties.
// getting HTML switch component
const meshEdges = document.getElementById("meshEdges");
let edges;
// checking if the switch is on and applying different types of edges based on that
if (meshEdges.checked) {
edges = new SketchEdges3D({
color: [35, 47, 32, 1],
size: 2
});
} else {
edges = new SolidEdges3D({
color: [35, 47, 32, 1],
size: 2
});
}
let meshSymbol = new MeshSymbol3D({
symbolLayers: [
new FillSymbol3DLayer({
edges: edges
})
]
});
graphicsLayer.graphics.forEach((graphic) => {
graphic.symbol = meshSymbol;
});
If you want to keep the material assignment on the mesh, you can use an empty FillSymbol3DLayer. Otherwise, the default symbol (with an orange color and white edges) will blend with any style changes you make to the mesh.
// Empty symbol
const emptyMeshSymbol = new MeshSymbol3D({
symbolLayers: [
new FillSymbol3DLayer({})
]
});
When it comes to applying textures, there are also many ways to do it. Here, creating custom patterns on a canvas is used. The texture mapping is defined in the UV table during mesh creation, ensuring that the texture is properly applied to each component.
// creating a canvas using a function where the pattern and color are defined
const needleCanvas = drawNeedleTexture();
graphicsLayer.graphics.forEach((graphic) => {
const mesh = graphic.geometry;
// Apply the texture to each component of the mesh
mesh.components.forEach((meshComponent) => {
if (meshTexture.checked) {
meshComponent.material.colorTexture = new MeshTexture({ data: needleCanvas });
} else {
meshComponent.material.colorTexture = null;
}
});
// Notify that the vertex attributes values have to be recalculated
mesh.vertexAttributesChanged();
});
When making styling adjustments, it is a good practice to clone the object. This prevents unintended changes from affecting the original graphic or mesh structure.
graphicsLayer.add(tree.clone());