Engine-Only : Error deploying render/model containing entities loaded from scene json and glb files

Introduction:
bitbucket repo:
https://HamboGameJam@bitbucket.org/HamboGameJam/vue_demo.git

Work based on the repository from iso74:

However changed it from Vue2+VueX to Vue3+Pinia … Ionic and Capacitor were also added …

  • Vue - Javascript framework for building web applications. Popular choice for building complex, data-drive applications.
  • VueX - State management pattern and library for Vue.js, uses a single, centralized store to manage global state.
  • Pinia - Lightweight, modular state management library for Vue.js that stores state in separate module for automatic caching and reactivity.
  • Ionic - Framework for building hybrid mobile apps using web technologies such as: html, css and Javascript.
  • Capacitor - Cross-platform app runtime that allows you to build web apps that run on native platforms like Androis and iOs. Provides a set of APIs for accessing native device features like camera, gps and file system.

We use Vue to build our Web application,
Ionic for providing the mobile app framework and
Capacitor to access native device features and run the app on top of native platform…

The application is made from 2 vue objects:

  • Base - Contains the markup of the main page and also contains Stage object.
  • Stage - Contains the necessary canvas html object where Playcanvas app will mount.

Here is the Pinia store file; holder of all playcanvas code/logic

/* eslint-disable */

import { /*createPinia,*/ defineStore } from 'pinia';
import * as pc from 'playcanvas';
import settings from '@/components/settings.js';
import { useLoadModules }  from '@/components/useLoadModules.js';
//import { useRotate }       from '@/assets/scripts/useRotate.js';
import { CapacitorHttp }                   from '@capacitor/core';
//import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';



export const usePiniaStore = defineStore('piniaStore', {
    state: () => ({ 
      
      canvasStage: null,
      configJson: null,
      isConfigured: false,
      jsonEntitiesGLB: null,
      pc: null,      
      pcApp: null,
      sceneCurrent: "",     
      watchingAppConfigured: false,
      
    }),
    actions: { 

      configureCSS( canvas, fillMode, width, height ) {
        //const canvas = this.canvasStage

        if (canvas.classList) {
            canvas.classList.add('fill-mode-' + fillMode);
        }
    
          // Dynamic CSS media query for aspect ratio changes
        this.appWidth = width;
        this.appHeight = height;       
          
      },
      pcCreateInputDevices() {
        console.log('🍍🍍🍍 Pinia Store > pcCreateInputDevices');
        
        const canvas = this.canvasStage;
        const elementInput = new pc.ElementInput(canvas, {
            useMouse: settings.INPUT_SETTINGS.useMouse,
            useTouch: settings.INPUT_SETTINGS.useTouch,
        });
      
        const keyboard = settings.INPUT_SETTINGS.useKeyboard
            ? new pc.Keyboard(window)
            : null;
      
        console.log('🍍🍍 Pinia Store > createInputDevices > keyboard:', keyboard);
      
        const mouse = settings.INPUT_SETTINGS.useMouse 
            ? new pc.Mouse(canvas) 
            : null;
      
        console.log('🍍🍍 Pinia Store > createInputDevices > mouse:', mouse);
      
        const gamepads = settings.INPUT_SETTINGS.useGamepads
            ? new pc.GamePads()
            : null;
       
        console.log('🍍🍍 Pinia Store > createInputDevices > gamepads:', gamepads);
      
        const touch = settings.INPUT_SETTINGS.useTouch && pc.platform.touch
            ? new pc.TouchDevice(canvas)
            : null;
      
        console.log('🍍🍍 Pinia Store > createInputDevices > touch:', touch);
      
        const devices = {
          elementInput,
          keyboard,
          mouse,
          gamepads,
          touch,
        };
    
        return devices;
      },
      pcDisablePreloadingAssets() {
        console.log('🍍🍍🍍 Pinia Store > pcDisablePreloadingAssets');

        const app = this.pcApp;
       /* 
        app.assets.on("add", (asset) => {
            // Disable the default preloading of assets.
            // You can also just mark all assets to not preload from the editor.
            if (asset.type !== "script" && asset.type !== "wasm" && asset.type !== "folder") {
               console.log('🍍🍍🍍 Pinia Store > Asset '+asset+' Not Preloading!');
                asset.preload = false;
            }
        });
        */
      },

      pcEnableSceneEntityRenderers() {
        console.log('🍍🍍🍍  Pinia Store > pcEnableSceneEntityRenderers');


        const entityNames = ["Luigi", "QBert", "PacMan"];

        for (const entityName of entityNames) {
          for (const render of this.pcApp.root.findByName(entityName).findComponents('render')) {
            render.enabled = true;
          }
        }
      },
      pcKickstartApp() {
        console.log('🍍🍍🍍 kk Pinia Store > pcKickstartApp');

        const app = this.pcApp;
        
        app.fire('preload:end');
        

        app.start();

        this.pcUpdate();
      },        
      pcOnAppCreated() {
        console.log('🍍🍍🍍 Pinia Store > pcOnAppCreated');

        this.pcLoadModules();

      },
      pcOnAppPreconfigured() {
        console.log('🍍🍍🍍 Pinia Store > pcOnAppPreconfigured');

        this.pcAppConfigure( settings.CONFIG_FILENAME );
      },
      pcOnGLBFilesLoaded() {
        console.log('🍍🍍🍍 Pinia Store > pcOnGLBFilesLoaded');

        this.pcApp.off("app:glbFilesLoaded");

        this.pcSpawnGLBEntities( this.jsonEntitiesGLB );
        
      },
      pcOnGLBEntitiesSpawned() {
        console.log('🍍🍍🍍 Pinia Store > pcOnGLBEntitiesSpawned');

        this.pcApp.off("app:glbEntitiesSpawned");
    
        //this.pcSpawnEntitiesByCode();
        this.pcKickstartApp();        
      },
      pcOnModulesLoaded() {
        console.log('🍍🍍🍍 Pinia Store > pcOnModulesLoaded ');

        this.pcAppPreconfigure();        
      },
      pcOnSceneLoaded() {
        console.log('🍍🍍🍍 Pinia Store > pcOnSceneLoaded');

        this.pcApp.off("app:sceneLoaded");

        
         //this.pcLoadGLBEntities( this.sceneCurrent ); ////LOVE

        /* entities spawned by scene alone*/
         this.pcEnableSceneEntityRenderers();        
         this.pcKickstartApp();

                
        // errors for displaying enabled render objects pcSpawnEntitiesByCode_0.png
         
        /* entities spawned by json and code; code instantiation uses
           using loadedmaterial pcSpawnEntitiesByCode_2.png
        
        this.pcKickstartApp();       
        
        this.pcEnableSceneEntityRenderers();
        */  
              

        /* enable the renderers of the entities loaded from scene.json  */       
        //this.pcSpawnEntitiesByCode();
        //this.pcKickstartApp();
        

        /* load up only  the scene and GLB */
        //this.pcKickstartApp();
        //this.pcSpawnEntitiesByCode();
        //this.pcLoadGLBEntities( this.sceneCurrent ); 

        /**/

      },         
      pcSpawnCubeEntity( name ){
        console.log('🍍🍍🍍 Pinia Store > pcSpawnCubeEntity ',name );

        const box = new pc.Entity( name );

        box.addComponent('render', {
          type: 'box'
        });

        return box;

      },
      pcSpawnEntitiesByCode()
      {
        console.log('🍍🍍🍍 Pinia Store > pcSpawnEntitiesByCode');

        const entityLah = this.pcSpawnCubeEntity('Lah');
        const entityDee = this.pcSpawnCubeEntity('Dee');
        const entityDah = this.pcSpawnCubeEntity('Dah');

        const rootEntity = this.pcApp.root.findByGuid('Root');

        rootEntity.addChild( entityLah );
        rootEntity.addChild( entityDee );
        rootEntity.addChild( entityDah );

        entityLah.setPosition( new pc.Vec3( 3, 0,-7) );
        entityDee.setPosition( new pc.Vec3( 0, 0,-7) );
        entityDah.setPosition( new pc.Vec3(-3, 0,-7) );

      },
      pcSpawnGLBEntities( jsonEntitiesGLB ) {
        console.log('🍍🍍🍍 Pinia Store > pcSpawnGLBEntities( jsonEntitiesGLB ) '+jsonEntitiesGLB);

        for (const jsonEntityName in jsonEntitiesGLB) {
          const jsonEntityGLB = jsonEntitiesGLB[jsonEntityName];        

          const entityName     = jsonEntityGLB.name.split('_')[0];
          const glbFileName    = jsonEntityGLB.name.split('_')[2] + ".glb";

          const assetContainer = this.pcApp.assets.find(glbFileName);
          const resourceContainer = assetContainer.resource;

          const entityContainer     = resourceContainer.instantiateRenderEntity();


          const entityParent = this.pcApp.root.findByGuid(jsonEntityGLB.parent);
          const isEnabled    = jsonEntityGLB.enabled;

          console.log('🍍🍍 Pinia Store > pcSpawnGLBEntities > jsonEntityGLB '+jsonEntityGLB+", parent "+ parent);

          entityContainer.name = entityName;

          entityParent.addChild(entityContainer);

          entityContainer.enabled = isEnabled;
                                
          entityContainer.setPosition( jsonEntityGLB.position[0],
                                       jsonEntityGLB.position[1],
                                       jsonEntityGLB.position[2] );

          entityContainer.setRotation( jsonEntityGLB.rotation[0],
                                       jsonEntityGLB.rotation[1],
                                       jsonEntityGLB.rotation[2] );

          entityContainer.setLocalScale( jsonEntityGLB.scale[0],
                                         jsonEntityGLB.scale[1],
                                         jsonEntityGLB.scale[2] );

        }

        this.pcOnGLBEntitiesSpawned();
      },
      pcSpawnMaterialEntitiesByCode()
      {
        console.log('🍍🍍🍍 Pinia Store > pcSpawnEntitiesByCode');

        const entityLah = this.pcSpawnCubeEntity('Lah');
        const entityDee = this.pcSpawnCubeEntity('Dee');
        const entityDah = this.pcSpawnCubeEntity('Dah');

        const rootEntity = this.pcApp.root.findByGuid('Root');

        rootEntity.addChild( entityLah );
        rootEntity.addChild( entityDee );
        rootEntity.addChild( entityDah );

        entityLah.setPosition( new pc.Vec3( 3, 0,-7) );
        entityDee.setPosition( new pc.Vec3( 0, 0,-7) );
        entityDah.setPosition( new pc.Vec3(-3, 0,-7) );


        const assetMaterialOrange = this.pcApp.assets.find("materialOrange", "material");

        const entityNames = ["Lah", "Dee", "Dah"];

        for (const entityName of entityNames) {
          for (const render of this.pcApp.root.findByName(entityName).findComponents('render')) {
            for (const meshInstance of render.meshInstances) {
                meshInstance.material = assetMaterialOrange.resource;
            }
          }
        }
      },
      setCanvas(data) {
        console.log('🍍🍍🍍 Pinia Store > setCanvas this.canvasStage = '+ data);
        this.canvasStage = data;
      },
      async pcAppBaseCreate(canvas) {
        console.log('🍍🍍🍍 Pinia Store > pcAppBaseCreate');

        const gfxOptions = {
          deviceTypes: [pc.DEVICETYPE_WEBGPU],
          glslangUrl: settings.LIB_PATH+'glslang.js',
          twgslUrl: settings.LIB_PATH+'twgsl.js'
        };

        const device = await pc.createGraphicsDevice(canvas, gfxOptions);
        device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);

        const createOptions = new pc.AppOptions();
        createOptions.graphicsDevice = device;
        createOptions.mouse = new pc.Mouse(document.body);
        createOptions.touch = new pc.TouchDevice(document.body);
        createOptions.keyboard = new pc.Keyboard(document.body);

        createOptions.componentSystems = [
          pc.CameraComponentSystem,
          pc.LightComponentSystem,
          pc.ModelComponentSystem,
          pc.RenderComponentSystem,            
          pc.ScriptComponentSystem,
          pc.SpriteComponentSystem
        ];

        createOptions.resourceHandlers = [
          pc.AnimationHandler,
          pc.AudioHandler,
          pc.ContainerHandler,
          pc.CubemapHandler,
          pc.FontHandler,
          //pc.JsonHandler, ???
          pc.MaterialHandler,
          pc.ModelHandler,
          pc.RenderHandler,
          pc.SceneHandler,
          pc.ScriptHandler,
          pc.SpriteHandler,
          pc.TextureAtlasHandler,
          pc.TextureHandler
        ];
        
        const app = new pc.AppBase(canvas);

        app.init(createOptions);

        this.pcApp = app;

        this.pcOnAppCreated();
      },
      async pcAppPreconfigure() {
        console.log('🍍🍍🍍 Pinia Store > pcAppPreconfigure');

        //this event :preconfigured
        this.pcGetConfigJSON().then((configJson) => {
          this.configJson = configJson;
          this.pcOnAppPreconfigured();
        });
        
      },
      pcAppConfigure( configFilename ) {
        console.log('🍍1🍍1🍍 Pinia Store > pcAppConfigure');

        this.pcApp.on('app:configured', () => {
                
          console.log('🍍 Pinia Store > pcOnAppConfigured');
          this.pcOnAppConfigured();
        });

        const app = this.pcApp;

        app.configure( configFilename,
                       function(err) {
                         if(err) {
                           console.error(err)
                         }

                         const aFillMode = this.configJson.application_properties.fillMode;
                         const canvasStg = this.canvasStage;
                         const iWidth = app.graphicsDevice.width;
                         const iHeight = app.graphicsDevice.height;

                         console.log('🍍🍍 Pinia Store > pcAppConfigure > app.configure > aFillMode,canvasStg,iWidth,iHeight:', aFillMode,canvasStg,iWidth,iHeight);

                         this.configureCSS( canvasStg, aFillMode, iWidth, iHeight );

                         setTimeout(
                          function() {
                            // Reflow the canvas
                            this.reflow();
              
                            window.addEventListener('resize', this.reflow, false);
                            window.addEventListener('orientationchange', this.reflow, false);
              
                            this.pcPreloadAssets();
                          }.bind(this),
                          250
                         );
                       }.bind(this)
                     );

      },
      async pcGetConfigJSON() {        
        console.log('🍍w🍍w🍍 @@@@ Pinia Store > getConfig '+settings.CONFIG_FILENAME);
      
        const configFile = await fetch(settings.CONFIG_FILENAME);
        const configData = await configFile.json();
      
        return configData;
      },
      async pcKickstart() {
        console.log('🍍🍍🍍 KK Pinia Store > pcKickstart');

        this.pcAppBaseCreate(this.canvasStage);
        
      },
       
      async pcLoadGLBFiles(glbFileNames) {
        console.log('🍍🍍🍍 Pinia Store > pcLoadGLBFiles ');

        //Step 1 - Create the data to feed pc.assetListLoader
        const assets_ = {};
        for (let i = 0; i < glbFileNames.length; i++) {          
          const glbFileName = glbFileNames[i];
        
          if(!this.pcApp.assets.find(glbFileName))
          {                    
            let glbUrl = process.env.BASE_URL + settings.GLB_PATH + glbFileName;

            console.log('🍍🍍 Pinia Store > pcLoadGLBFiles > '+glbFileName +" "+glbUrl );

            assets_[glbFileName] = (  new pc.Asset( glbFileName, 'container', { url: glbUrl } )  );
          }          
        }
        /////////////////////////////////////////////////////////////
        //Step 2 - Load files... and trigger callback when all data is loaded
                
        const assetListLoader = new pc.AssetListLoader(Object.values(assets_), this.pcApp.assets);
        assetListLoader.load((err, failed) => {
          if (err) {
            console.error(`🍍🍍 Pinia Store > pcLoadGLBFiles > ${failed.length} assets failed to load`);
          } else {

            console.log(`🍍🍍 Pinia Store > pcLoadGLBFiles > assets loaded`);

            this.pcApp.fire("app:glbFilesLoaded");
          }
        });

      },


      async pcLoadModules() {
        console.log('🍍🍍🍍 Pinia Store > pcLoadModules');     
        
        pc.WasmModule.setConfig('Ammo', {
          glueUrl: '/assets/scripts/ammo.wasm.js',
          wasmUrl: '/assets/scripts/ammo.wasm.wasm',
          fallbackUrl: '/assets/scripts/ammo.js'
        });

        console.log('🍍🍍🍍 Pinia Store > pcLoadModules > pc.WasmModule.getConfig(\'Ammo\')', pc.WasmModule.getConfig('Ammo'));

        pc.WasmModule.getInstance('Ammo', this.pcOnModulesLoaded.bind(this));
      },



      async pcLoadModules2() {
        console.log('🍍🍍🍍 Pinia Store > pcLoadModules');
        // Using Mixins in Vue 3

        const { loadModules } = useLoadModules();

        loadModules(settings.PRELOAD_MODULES, settings.ASSET_PREFIX, function() {
          console.log('🍍🍍 Pinia Store > pcLoadModules > this.pcOnModulesLoaded()');
          this.pcOnModulesLoaded();
        }.bind(this));


      },
      async pcLoadGLBEntities(sceneName) {
        
        this.pcApp.on("app:glbFilesLoaded", this.pcOnGLBFilesLoaded, this);

        let glbSceneName = sceneName.replace('.json', '_glb.json');

        // remove the trailing front slash; otherwise it wont get the proper path
        let glbScenePath = (process.env.BASE_URL + settings.SCENE_PATH).slice(1);
        console.log('🍍🍍🍍 Pinia Store > pcLoadGLBEntities > (glbScenePath+glbSceneName)'+(glbScenePath+glbSceneName) );
        try{
          CapacitorHttp.get({
            url: (glbScenePath+glbSceneName),
            responseType: 'json'
          })
          .then(response => {

            console.log('🍍🍍 Pinia Store > pcLoadGLBEntities > (response.data)'+response.data );

            const uniqueGLBFileNames = [];            
            this.jsonEntitiesGLB = response.data.entities;
            
            for (const entityName in this.jsonEntitiesGLB) {

              const glbFileName = ( entityName.split('_glb_')[1] ) + ".glb";

              if (uniqueGLBFileNames.indexOf(glbFileName) === -1) {
                uniqueGLBFileNames.push(glbFileName);
              }
            }

            this.pcLoadGLBFiles(uniqueGLBFileNames);

          })
          .catch(error => {
            console.log('🍍🍍44 Pinia Store > pcLoadGLBEntities > No GLB Entities found');
          });

        } catch(error) {
          console.log('🍍🍍45 Pinia Store > pcLoadGLBEntities > ERROR :', error);
        }
        
        
      },
 
      async pcLoadScene( sceneName ) {
        
        console.log('🍍🍍🍍 Pinia Store > pcLoadScene(sceneName) > Loading Scene named: ', sceneName);

        this.sceneCurrent = sceneName;
        
        const scenePath = process.env.BASE_URL + 
                          settings.SCENE_PATH +
                          sceneName;
                          
        this.pcApp.on("app:sceneLoaded", this.pcOnSceneLoaded, this);

        console.log('🍍🍍 Pinia Store > pcLoadScene > pcLoadSceneData(sceneName) > Scene path: ', scenePath);
        
        this.pcApp.scenes.loadScene( scenePath,
          function(err, scene ){
            console.log('🍍 Pinia Store > loadScene');
            if(!err)
            {
              console.log('🍍 Pinia Store > loadScene > OnSceneLoaded ', scene );
              this.pcApp.fire("app:sceneLoaded");
            }
            else
            {
              console.error('🍍 Pinia Store > onLoadScene FAIL: '+ err);
            }
          }.bind(this)          
        );

        console.log('🍍🍍 Pinia Store > pcLoadScene > End of Function');
      },

      async pcOnAppConfigured() {
        console.log('🍍🍍🍍 Pinia Store > pcOnAppConfigured');
        
        this.pcApp.off("app:configured");
        
        this.pcLoadScene('scene8.json');
      },      

      async pcPreloadAssets() {
        console.log('🍍🍍🍍 Pinia Store > pcPreloadAssets');

        const app = this.pcApp;

        app.preload(
          function(err) {
            if (err) {
              console.error('🍍🍍 Pinia Store > preloadAssets > Preload Error: ', err);
            }
            // Trigger event when the app is configured and assets are preloaded
            console.log('$$$🍍🍍 Pinia Store > preloadAssets > Assets preloaded.');
            app.fire('app:configured');
          }.bind(this)
        );
        
      },
      async pcUpdate() {
        console.log('🍍🍍🍍 Pinia Store > pcUpdate'); 

        const app = this.pcApp;

        app.on('update', (dt) => { 
          console.log('Derpesaurus '+dt);

          
          console.log( 'Derp  I '+app.root.findByGuid('Root') == null);
          /*console.log( 'Derp II '+app.hasEvent('app:configured') );*/
          

          //box.rotate(10*dt,20*dt,30*dt);   
        });

      },
      reflow() {     
        const app = this.pcApp;
        const canvas = this.canvasStage
        const fillMode = app._fillMode;

        console.log('🍍🍍🍍 Pinia Store > reflow: this, app, canvas:'+this+','+app+','+canvas);

        app.resizeCanvas(canvas.width, canvas.height);
        canvas.style.width = '';
        canvas.style.height = '';

        if (fillMode === pc.FILLMODE_NONE || fillMode === pc.FILLMODE_KEEP_ASPECT)
        {
          if ( (fillMode === pc.FILLMODE_NONE &&
                  canvas.clientHeight < window.innerHeight) ||
                  (canvas.clientWidth / canvas.clientHeight) >=
                  (window.innerWidth  / window.innerHeight )    )
          {
            canvas.style.marginTop =
              Math.floor((window.innerHeight - canvas.clientHeight) / 2) + 'px';
          } else {
            canvas.style.marginTop = '';
          }
        }

      },

   },
});

Slide 1 - Entities WITH RENDER COMPONENT “ENABLED” loaded from scene.json files threw exceptions when entity’s material+renderer+shader init is triggered

Slide 2 - Entities WITH RENDER COMPONENT “DISABLED” loaded from scene.json files are loaded into memory, camera(with red clear color) and light components are deployed

Slide 3 - Entities WITH RENDER COMPONENT “ENABLED” AFTER APPLICATION STARTS… yields no errors HOWEVER ENTITIES ARE NOT RENDERED!

Slide 4 - Entities WITH RENDER COMPONENT “ENABLED” BEFORE APPLICATION STARTS… yields same errors as Slide1

Slide 5 - Entities being created on runtime by code are rendered:

      pcSpawnCubeEntity( name ){
        console.log( Pinia Store > pcSpawnCubeEntity ',name );

        const box = new pc.Entity( name );

        box.addComponent('render', {
          type: 'box'
        });

        return box;

      },
      pcSpawnEntitiesByCode()
      {
        console.log('Pinia Store > pcSpawnEntitiesByCode');

        const entityLah = this.pcSpawnCubeEntity('Lah');
        const entityDee = this.pcSpawnCubeEntity('Dee');
        const entityDah = this.pcSpawnCubeEntity('Dah');

        const rootEntity = this.pcApp.root.findByGuid('Root');

        rootEntity.addChild( entityLah );
        rootEntity.addChild( entityDee );
        rootEntity.addChild( entityDah );

        entityLah.setPosition( new pc.Vec3( 3, 0,-7) );
        entityDee.setPosition( new pc.Vec3( 0, 0,-7) );
        entityDah.setPosition( new pc.Vec3(-3, 0,-7) );

      },

Slide 6 - Enabling the render components of the entities loaded from Json after instantiating the objects by code throws exception

Slide 7 - Out of frustations I’ve tried to device a way to render objects using glb files:

  1. Derive a list of unique glb filenames derived from the entity’s scene name.
  2. Create a list of asset container objects to be fed into pc.assetListLoader.
  3. On finished loading , the assets should be on the AssetRegistry.
  4. We iterate the glb entity list once more to instantiate, position, rotate and scale the glb asset containers using the data of the entity derived from the scene.

The errors thrown are eerily similar… render object complaining about initialization of material+shaders …

Thanks in advance =D !!!