Loading PlayCanvas builds with engine only issue (specularUniform difference)

Once again I am having trouble with loading an editor build in my engine-only app. My models in the custom app look different than the ones in the normal build.
I have been able to narrow the issue down to one property in my material, ‘specularUniform’. It is a different value in my custom loader than in the normal loader (opening index.html from the build), when I simply force that value to be the same, it looks fine.

See the color difference below:

I’ll explain my loader code here,
First of all I set all the assets to not ‘preload’, I want to control this myself, then I download files from my server and turn them into blob urls and set those urls to the asset.file.url for each asset.

for (const assetId in buildData.assets) {
  const asset = buildData.assets[assetId];
  if (asset.type !== 'script') buildData.assets[assetId].preload = false;
  if (asset.file) {
    let responseType = '';
    if (asset.type === 'model') responseType = 'application/json';
    if (asset.type === 'texture') responseType = 'arraybuffer';
    // Loading from local files for debug
    const promise = new Promise((resolve, reject) => {
      axios.get(`/playcanvas_build/${asset.file.url}`, {
      }).then((result) => {
        const data = result.data;
        let assetBlob;
        if (asset.type === 'model') assetBlob = new Blob([JSON.stringify(data)], {
          type: 'application/json'
        if (asset.type === 'texture') assetBlob = bytesToB64Blob(new Uint8Array(data));
        else assetBlob = new Blob([data]);
        const assetUrl = URL.createObjectURL(assetBlob);
        buildData.assets[assetId].file.url = assetUrl;


Then following the start script of normal builds, I run app.configure with the build json file url and create a scene from my scene json. Then I only preload the texture files by directly loading it with the resource manager, otherwise it adds ?t=... which breaks blob urls.

app.configure(buildUrl, async (err) => {
if (err) return console.error(err);

// Create the scene entry
const sceneBlobData = JSON.stringify(sceneData);
const blob = new Blob([sceneBlobData], {
  type: 'application/json',
const url = URL.createObjectURL(blob);

pc.app.scenes.add('CustomBuild', url);
const scene = pc.app.scenes.find('CustomBuild');

// Go through all the texture assets and load them manually
// (bypassing the app.assets.load method)
const assets = app.assets._assets;
const textureLoads = [];

for (let index = 0; index < assets.length; index++) {
  const asset = assets[index];
  if (asset.type === 'texture') {
    const promise = new Promise((resolve) => {
      const file = asset.file;

      // _opened and _loaded copied from Playcanvas source
      const _opened = function (resource) {
        if (resource instanceof Array) {
          asset.resources = resource;
        } else {
          asset.resource = resource;
        app.loader.patch(asset, app.assets);
        app.assets.fire('load', asset);
        app.assets.fire(`load:${asset.id}`, asset);
        if (file && file.url) app.assets.fire(`load:url:${file.url}`, asset);
        asset.fire('load', asset);

      const _loaded = function (err, resource, extra) {
        asset.loaded = true;
        asset.loading = false;
        if (err) {
          app.assets.fire('error', err, asset);
          app.assets.fire(`error:${asset.id}`, err, asset);
          asset.fire('error', err, asset);
        } else {

      // Load through the ResourceLoader directly
      app.loader.load(asset.file.url, asset.type, _loaded, asset);

await Promise.all(textureLoads);

// Finally, add the scene hierarchy to the app
app.loadSceneHierarchy(scene.url, () => {

After this I simply start the app.
Does anyone know why the specularUniform can be different in the different builds? :slight_smile:
If you need more information please let me know!

Update: it doesn’t seem to be the specularUniform value. This was solved by running material.update() after the skybox got added.
I currently can not find any differences between the materials or scene settings.
Correct colors:

Incorrect colors:

Any thoughts? :slight_smile:

1 Like

Hi @Ivolutio,

Not sure what’s the issue is, it isn’t easy to debug like this. Though looking the two screenshots I would tell the 2nd one receives more light, quite a difference. Are you sure that all lighting settings (both your light entity and scene settings) are fully identical?

Yeah. The scenes are identical (checked with yaustars tool), also the scene’s skybox, exposure, ambient light, etc. seems to be the same. I’ve been checking a lot of properties with a text compare tool and everything is the same :confused:

It kinda looks like the scene exposure/gamma setting is higher :thinking:

1 Like

Yeah that would make sense, but they are the same, unfortunately :stuck_out_tongue:
I guess I’ll start from zero and retry recreating the scene loader. I can’t share the source of this code, so unless you have any other ideas to check, I’ll just have to go step by step again. :slight_smile:

Check this, just in case:

1 Like

Unfortunately that doesn’t change anything. It is already ‘1’ and setting it to pc.GAMMA_SRGB doesnt change it (GAMMA_NONE does, but of course thats too dark).

Hi! If it still actual, I’ve found a way to override loading phase.
I’ve changed loader handlers. This is an example of model loading

// save original handler
const modelHandler = app.loader.getHandler('model');
app.loader.addHandler('model', {
    load(url, callback, asset) {
      // try find an asset in my own registry
      const assetData = registry.get(asset.id);

      // if not found - load with default loader
      if (!assetData) {
        modelHandler.load(url, callback, asset);

      // run callback with resource
      // resource is different for different asset types
      // json model waits parsed JSON
      // texture waits <img> html element etc
      callback(null, assetData.resource);
    // just run original `open` - let playcanvas do needed actions
    open(url, data, asset) {
      return modelHandler.open(url, data, asset);
    // run original patch, not all handlers have it, so check if it needed
    patch(asset, assets) {
      return modelHandler.patch(asset, assets);

Assets in my registry looks like

export interface IAsset {
  // id from config.assets (config.json in build folder)
  id: number;
  // type from config.assets (config.json in build folder)  
  type: 'material' | 'audio' | 'model' | 'cubemap' | 'texture' | 'animation' | 'script' | 'json' | 'template';
  // name from config.assets (config.json in build folder)
  name: string;
  meta: any;
  // data from config.assets (config.json in build folder)
  data: any;
  // tags from config.assets (config.json in build folder)
  tags: any[];
  // file from config.assets (config.json in build folder)
  file: null | {
    url: string;
    filename: string;
    hash: string;
    size: number;
  // here is parsed JSON, <img> etc
  resource: any;

And that’s what I do for each needed asset (I needed JSON models and templates only, so didn’t make textures loading yet)

// callback to just to wait for all asset loaded, realisation doesn't matter
const onAssetReady = () => {

  if (count >= result.assets.length) {

assets.forEach((assetData: IAsset) => {
      if (assetData.type === 'template') {
        const entities = assetData.resource ? assetData.resource.entities : assetData.data.entities;
        Object.values(entities).forEach((entity: any) => {
          const model = entity.components.model
          if (model && model.batchGroupId) {
            // `static` models in batch group doesn't rendered twice (maybe bug, maybe I didn't figure out yet)
            model.isStatic = false;

      if (assetData.file) {
        // change url if needed to load with default handler from custom url
        assetData.file.url = `${assetBaseUrl}static/${assetData.file.url}`;

      // Create playcanvas asset
      const asset = new pc.Asset(assetData.name, assetData.type, assetData.file, assetData.data);
      // override id to match config.json
      asset.id = assetData.id;
      // place tags back

      // add asset to playcanvas registry
      // start loading


I’ve write my asset-server to load only needed assets for Scene or Template. I’ve parsed config.json for all assets and their references. Then I just make query to asset-server like include=[assetId], exclude=[assetId2] and asset-server responses with JSON pack of all needed assets.

It doesn’t matter how you preload your assets, the main idea is to override loader handlers loading phase and leaving open as is - so it would be natively registered in playcanvas.