Scene with certain imported meshes (as .obj from 3DsMax) throws error when trying to serialize

I’m facing a problem where, when trying to run SceneSerializer.Serialize(scene) I’m sometimes getting the following error: Cannot read property 'length' of undefined at Mesh.push.wAMR.Mesh.serialize (with certain imported models). I think I’m doing the required ES6 imports (using this post to double check: Serializing mesh in ES6 Module has many problems):

import { SceneSerializer } from '@babylonjs/core/Misc/sceneSerializer';
import '@babylonjs/core/Physics/physicsEngineComponent';

What confuses me even more is that I have some other models exported from the same 3dsmax that work just fine during serialization. Moreover, the problematic models go successfully through the rest of my scene setup, except that I can’t seem to get the said troublesome models to receive shadows (or maybe cast, I didn’t figure that one yet). I’ve looked through the .mtl files extensively to see if something was different, all I found was that some Ke (emissive) values were set to 1.0 1.0 1.0, but that shouldn’t matter since I’m modifying the material on import and nulling any emissive/ambient texture and setting the values to a custom defined color on import (plus I tried setting them to 0 0 0 manually in the .mtl and that didn’t change anything). I also tried setting COMPUTE_NORMALS = true on the OBJFileLoader, but to no avail (and the .obj files in question define normals).

One thing I haven’t fully grasped yet is that the models causing the serialization error and not showing shadows trigger quite a few Setting vertex data kind 'position' with an empty array errors on import. I’ve done a quick test and I’m getting the same result even if my process is as simple as opening 3DsMax, creating a box and a sphere, assigning a standard material to each and exporting to OBJ.

I’m trying to clean up the scene as much as I can during setup and am also recentering the models since they often have a misplaced origin that ends up far away (although not always). Here’s a snippet of the kind of setup I’m running in the Scene Loaded callback:

DoStuffToSetupSceneHere(babylonScene: Scene) {
   this.engine.clearInternalTexturesCache();
   this.scene = babylonScene;
   this.scene.executeWhenReady( () => {
     this.setupScene();
   });
}

…where setupScene contains multiple functions such as:

configureSceneMeshes() {
this.scene.blockfreeActiveMeshesAndRenderingGroups = true;
// Clearing empty meshes
for ( let mesh of this.scene.meshes ) {
  if ( typeof mesh.subMeshes === undefined
    || mesh.subMeshes === undefined
    || mesh.subMeshes === null
    || typeof mesh.subMeshes[Symbol.iterator]!=='function'
    || mesh.getTotalVertices() == 0
    || !(mesh instanceof Mesh) ) {
    mesh.dispose();
  }
}
this.scene.blockfreeActiveMeshesAndRenderingGroups = false;  // Resetter à la bonne valeur

this.sceneExtent = this.scene.getWorldExtends();
this.sceneMiddle = Vector3.Center(this.sceneExtent.max, this.sceneExtent.min);
this.sceneSize = Vector3.Distance(this.sceneExtent.max, this.sceneExtent.min);

var rescaleFactor = 1;
if ( this.sceneSize > 2500 ) {
  rescaleFactor = Math.round(2000*100 / this.sceneSize) / 100;
}

for ( let material of this.scene.materials ) {
  if ( Object.keys(material.meshMap).length > 1 ) {
    const chunkSize = 500;
    var chunks = Object.values(material.meshMap).map((mesh, i) => {
      return i % chunkSize === 0 ? Object.values(material.meshMap).slice( i, i + chunkSize ) : null;
    }).filter((mesh) => { return mesh; });

    for (let chunk of chunks) {
      Mesh.MergeMeshes(chunk as Array<Mesh>, true, true);
    }
  }
  this.setupMaterialAsPbr(material);
  // this.setupMaterialAsStandard(material);
}

// Recentering remaining meshes
for ( let mesh of this.scene.meshes ) {
  // console.log(mesh.id,'\noriginal position: ',mesh.position,'\noriginal abs pivot: ',mesh.getAbsolutePivotPoint(),'\noriginal pivot: ',mesh.getPivotPoint());
  if ( mesh instanceof Mesh && mesh.getTotalVertices() > 0 ) {
    if ( rescaleFactor !== 1 ) {
      mesh.scaling = new Vector3(rescaleFactor, rescaleFactor, rescaleFactor);
    }
    mesh.translate(this.sceneMiddle, -1*rescaleFactor, Space.WORLD);
    mesh.bakeCurrentTransformIntoVertices();
    mesh.freezeWorldMatrix();
  }
  // console.log(mesh.id,'\nnew position: ',mesh.position,'\nnew abs pivot: ',mesh.getAbsolutePivotPoint(),'\nnew pivot: ',mesh.getPivotPoint());

  mesh.isPickable = false;
  mesh.doNotSyncBoundingInfo = true;

  if (!mesh.material || (mesh.material instanceof StandardMaterial && !mesh.material.diffuseTexture) || (mesh.material instanceof PBRMaterial && !mesh.material.albedoTexture)) {
    mesh.enableEdgesRendering();
    mesh.edgesRenderer.edgesWidthScalerForOrthographic = 10000;
  }
  mesh.edgesWidth = this.defaultEdgesWidth;
  mesh.edgesColor = new Color4(0, 0, 0, 1);
}

this.sceneExtent = this.scene.getWorldExtends();
this.sceneMiddle = Vector3.Center(this.sceneExtent.max, this.sceneExtent.min); // Devrait maintenant être équivalent à (0,0,0)
this.sceneSize = Vector3.Distance(this.sceneExtent.max, this.sceneExtent.min);
}

…and setting up the light/shadows:

setupLighting() {
// Lighting
this.sunTarget = new Vector3(this.sceneMiddle.x, this.sceneExtent.min.y, this.sceneMiddle.z);
this.sunDistance = Vector3.Distance(this.sunTarget, this.sceneExtent.max);
this.sunLight = new DirectionalLight("directionalLight", Vector3.Zero(), this.scene)
this.sunLight.diffuse = Color3.FromHexString(this.sunColor);
this.sunLight.specular = new Color3(1, 1, 1);

// Shadows
this.sunLight.shadowMinZ = this.sunDistance;
this.sunLight.shadowMaxZ = 3 * this.sceneSize + 100;
// this.sunLight.autoUpdateExtends = false;
this.sunLight.autoUpdateExtends = true;
// this.sunLight.autoCalcShadowZBounds = true;
this.sunLight.shadowOrthoScale = 0;
this.sunLight.shadowFrustumSize = 0;
this.sunShadows = new ShadowGenerator(1024, this.sunLight, true);
this.sunShadows.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
this.sunShadows.bias = 0.0025;
this.sunShadows.normalBias = 0.35;
this.sunShadows.useContactHardeningShadow = true;
this.sunShadows.contactHardeningLightSizeUVRatio = .04;
this.sunShadows.setDarkness(0.15);

for (let mesh of this.scene.meshes) {
  if (mesh.id !== 'skybox') {
    this.sunShadows.addShadowCaster(mesh, true);
    if (!mesh.material || (mesh.material instanceof StandardMaterial && !mesh.material.diffuseTexture) || (mesh.material instanceof PBRMaterial && !mesh.material.albedoTexture)) {
      this.sceneMeshesMap.get(mesh.uniqueId).receiveShadows = true;
      mesh.receiveShadows = true;
    }
  }
}

this.updateSun();
}

And here’s how we’re calling the Serialization:

sceneLoaded(sceneFile: File, babylonScene: Scene) {
   DoStuffToSetupSceneHere(babylonScene) ;
   this.scene.executeWhenReady(() => {
     const serializedScene = SceneSerializer.Serialize(this.scene);
     const strScene = JSON.stringify(serializedScene);
     broadcast resulting file(strScene);
   });
}

Any hint would be appreciated. I can provide some sample files too.

Pinging @Guillaume_Pelletier

@Evgeni_Popov i’m working on GLTF exporter code (Max and Maya). Max to Obj export is pure native from Autodesk. I have very few experience on Java Script Babylon serializer. :smile:
@emmbema consider to post a sample file to reproduce the issue, while this one could be easely trap by Exception catching

Sorry, will now know better!

1 Like

@emmbema would be awesome if you could repro in the playground ? if it does not repro there, sharing a github project might help.

Sure thing!

Here’s a PG in which I tried to showcase both problems (serialization & shadows) at once: https://playground.babylonjs.com/#1P3Q4Z#47

I commented the different FileLoads to avoid unwarranted downloads, but you can uncomment any (one at a time) to observe and compare the behaviors I’m talking about. Be warned, some files to import are quite large (~100MB).

I updated the PG with a small fix for calculation of the scene’s middle and sun’s position/zbounds, problem described below works better now.
Edit: In my project I’m setting the shadowMinZ and shadowMaxZ values manually using the scene’s size and sun’s position, but for some reason that wouldn’t work even with the good file in the PG so I had to use autoCalcShadowZBounds and when looking at its logged values after loading either of the bad files, I think there’s some empty mesh or some other thing that remains faaaaar off that could be the culprit for the shadows problem. It could be screwing the shadowMap since the Z limits appear to stretch from some e-300ish to e+300ish.

I don’t really trust my way of cleaning / recentering the imported meshes, but that’s all I could get working after trying a bunch of approches :frowning:

Thanks a lot for providing a repro !!! @Drigax will look into it next week after thanksgiving

Should be fixed in the next nightly

2 Likes

Oh wow, already!?!?

yup should be up in 30

I’m curious, what was causing it ?

a missing check for empty meshes

Ah ok. Any clue concerning the issues with the shadows? (I guess the check for the serialization is unrelated)

Could you provide a PG demonstrating just this problem?

Ahhh! I think I’ve put my hand on it!
https://playground.babylonjs.com/#NDP4XM#3

Without doing a check to exclude empty meshes, something goes wrong with the shadow generator, or maybe the material attribution (try removing the if (mesh.getTotalVertices() > 0) {...} check in sceneLoaded, shadows won’t work). Would this be handled by the check you added, does your update apply itself to the importation or the serialization step? (I got lost trying to figure it out on my own by going through the nightly’s diffs)

I’ll try doing the same thing in my project and let you know if the problem persists.
Either way, thanks for the help!

Ô 3dsMax, why you do this to me.

1 Like

nope it is only at serialization that I am not adding geo for meshes without data. but you ll still need your code to check after import

Oh kk. Do you think it’d be possible to add a boolean argument to the fileLoader constructor to specify if we want it to perform a check to ignore empty vertices data? (Though I’d totally understand if you think this is too much of an edge case to be worth implementing).