Material not applied after loading mesh from file


I have an issue with applying material to meshes loaded from a file; the material does not show.

I think it is related to timing, because the mesh shows visually before console.log outputs information about the mesh and upon inspecting it in the console, the material is set.

Since this is an integration with Maplibre, I cannot use the Playground, sorry for that. Here is the code:

<!DOCTYPE html>
<meta charset="utf-8" />
<title>Add a 3D model with babylon.js</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<script src=""></script>
<link href="" rel="stylesheet" />
<script src=''></script>
	body { margin: 0; padding: 0; }
	#map { position: absolute; top: 0; bottom: 0; width: 100%; }
<script src=""></script>
<script src=""></script>
<style type="text/css">
    #info {
    display: block;
    position: relative;
    margin: 0px auto;
    width: 50%;
    padding: 10px;
    border: none;
    border-radius: 3px;
    font-size: 12px;
    text-align: center;
    color: #222;
    background: #fff;
<div id="map"></div>
<pre id="info">abc</pre>
    var BABYLON = window.BABYLON;

    // Define the map syle (OpenStreetMap raster tiles)
    const style = {
    "version": 8,
        "sources": {
        "osm": {
                "type": "raster",
                "tiles": ["{z}/{x}/{y}.png"],
                "tileSize": 256,
        "attribution": "&copy; OpenStreetMap Contributors",
        "maxzoom": 19
    "layers": [
        "id": "osm",
        "type": "raster",
        "source": "osm" // This must match the source key above
    var map = ( = new maplibregl.Map({
        container: 'map',
        zoom: 18,
        center: [148.9819, -35.3981],
        pitch: 60,
        antialias: true // create the gl context with MSAA antialiasing, so custom layers are antialiased

    // World matrix parameters
    var worldOrigin = [148.9819, -35.39847];
    var worldAltitude = 0;

    // Maplibre.js default coordinate system (no rotations)
    // +x east, -y north, +z up
    //var worldRotate = [0, 0, 0];

    // Babylon.js default coordinate system
    // +x east, +y up, +z north
    var worldRotate = [Math.PI / 2, 0, 0];

    // Calculate mercator coordinates and scale
    var worldOriginMercator = maplibregl.MercatorCoordinate.fromLngLat(
    const worldScale = worldOriginMercator.meterInMercatorCoordinateUnits();

    // Calculate world matrix
    var worldMatrix = BABYLON.Matrix.Compose(
        new BABYLON.Vector3(worldScale, worldScale, worldScale),
        new BABYLON.Vector3(

    // configuration of the custom layer for a 3D model per the CustomLayerInterface
    var customLayer = {
        id: '3d-model',
        type: 'custom',
        renderingMode: '3d',
        onAdd: function (map, gl) {
            this.engine = new BABYLON.Engine(
                    useHighPrecisionMatrix: true // Important to prevent jitter at mercator scale
            this.scene = new BABYLON.Scene(this.engine);
            this.scene.autoClear = false;

            this.scene.beforeRender = () => {

            // create simple camera (will have its project matrix manually calculated)
   = new BABYLON.Camera(
                new BABYLON.Vector3(0, 0, 0),

            // create simple light
            const light = new BABYLON.HemisphericLight(
                new BABYLON.Vector3(0, 0, 100),
            light.intensity = 0.7;

            // Add debug axes viewer, positioned at origin, 10 meter axis lengths
            new BABYLON.AxesViewer(this.scene, 10);
            const files = [
                    fileName : "34M_17.gltf",
                    positionX : "0",
                    positionY : "0"
                    fileName : "byggnad.obj",
                    positionX : "25",
                    positionY : "0"
            for (let i = 0; i < files.length; i++)
                // load GLTF model in to the scene
                ).then((modelContainer) => {                                        
                    const rootMesh = modelContainer.createRootMesh();
           = "min mesh " + i;                    
                    rootMesh.position.x = files[i].positionX;
                    rootMesh.position.y = files[i].positionY; 
                    var mat = new BABYLON.StandardMaterial("mat", this.scene);
                    mat.emissiveColor = BABYLON.Color3.White();
                    mat.wireframe = true;
                    rootMesh.material = mat;                                                         
   = map;  
        render: function (gl, matrix) {
            const cameraMatrix = BABYLON.Matrix.FromArray(matrix);

            // world-view-projection matrix
            const wvpMatrix = worldMatrix.multiply(cameraMatrix);


        raycast: function (point, isClick) {
            var ray = this.scene.createPickingRay(point.x, point.y, BABYLON.Matrix.Identity(),, false);	
            var hit = this.scene.pickWithRay(ray);

            if (hit.pickedMesh){

    map.on('style.load', function () {

    map.on('click', function(e) {
        customLayer.raycast(e.point, true);        


Thanks for all help!

1 Like

Welcome aboard!

The rootMesh is probably not a displayable mesh (probably a TransformNode), so changing its material will have no effect.

Try to use the inspector to see the hierarchy of meshes in your scene.

Are you sure this mesh in the inspector is not a simple node with no rendering geometry? Because if it’s a regular mesh, if you can see that the material is applied to it, it should definitely work… Just to be sure, using the inspector, try to change the material to another one and set it back to your own material.


You are 100% correct, it had no geometry. Thanks for pointing me in the right direction!

Everything went as expected if I changed to this:

rootMesh._children[0].material = mat;

What is the difference between LoadAssetContainerAsync and ImportMesh? I tried to use ImportMesh and found it easier to use because i don’t have to to work with children and parent.

My scenario is to load obj files with a 3D representation of real world objects, one per file. So the files will only contain meshes. No camera, material or anything else, just a geometry. What is the “best” way to load these? In my demo application it may be up to 300.

1 Like

LoadAssetContainer will load the data into an asset container and not directly in the scene. It can be handy if you don’t want to have all the objects directly loaded in the scene, but add them/some later on.

If all your objects must be in the scene right from the start, then ImportMesh is easier to use (or Append to import everything from the file).

1 Like

Thank you very much!