动态添加“a-asset-item”元素时,A-Frame 场景在资产准备好之前初始化

A-Frame scene initializes before assets ready when dynamically adding `a-asset-item` elements

我已经发布了一个 A-Frame 组件 street (source),它依赖于许多资产来构建街景。

我按照另一个 A-Frame 组件应用 vartiste-toolkit 的工作示例在场景初始化之前动态注入 a-asset-item 元素。目标是使用 A-Frame 资产加载器(并显示其内置加载屏幕),同时还允许其他开发人员使用简单的语法将此组件包含在他们的场景中,例如:

<a-assets>
  <a-asset streetmix-assets-url="https://kfarr.github.io/streetmix3d/"></a-asset>
</a-assets>

我创建了一个以这种方式加载组件和资产的测试项目:

https://street-component-test.glitch.me

当加载所有新文件(无本地缓存文件)时,资产和场景在第一页加载时按预期加载。它看起来像下面的“期望状态”屏幕截图。

但是,在第二次或后续加载(使用缓存文件)时,我遇到了一些实体未按预期加载的问题。它看起来像下面的“错误状态”屏幕截图。

在“错误状态”场景中,控制台包含许多这样的错误: THREE.WebGLRenderer: Texture marked for update but image is incomplete

我的假设是,当使用本地缓存文件加载场景时,A-Frame 场景会在 street 组件资产被注入到 a-assets 部分之前进行初始化。这会导致实体在它们的基础纹理准备好开始渲染场景之前被操作,因此会出现上述三个控制台错误。

即使在加载缓存文件时,如何确保“期望状态”的结果?

是否有更好的方法让用户加载组件所需的一组资产?


截图:

期望的状态:

错误状态:

正如您所说,将资产注入文档的脚本与使用它们的组件之间存在竞争

一个解决方案是保留资产是否被注入的信息,并在所述组件中使用它:

// - assetsloaded can be a global flag, assets dataset attribute
// a-frame component / system attribute.
// - streetmix-assets-load can be an event emitted on the <a-assets> node
if (assetsloaded)
   parseResponse(response)
else
   assets.addEventListener("streetmix-assets-loaded", e => parseResponse(response))

你可以看到类似的做法here in aframe-extras


您可以修改 assets.js 以保留 streetmix-assets 是否准备就绪的信息,发出 streetmix-assets-loaded,并确保 street 组件在资产准备就绪。

就像 this glitch 中那样,这是你的错误的混音,我在 assets.jsstreet.js.

中应用了上述逻辑

我认为最好的方法是创建自定义 A-Node 元素。我们称它为 streetmix-assets。只要我们在 a-assets 元素完成加载之前将 streetmix-assets 附加到 a-assets 元素,我们就可以强制 a-assets 等待所有其余资源加载。

这里有两个棘手的部分:

  1. 说服 a-assets 等待 streetmix-assets 完成加载。
  2. 阻止 streetmix-assets 加载,直到我们注入的所有资产都完成加载。

我们通过将 streetmix-assets 伪装成 a-asset-item 实体来处理 1(例如,设置 isAssetItem = true 并添加 src属性)。我们可以通过重用 a-assets 本身的方法来处理 2 。注册 streetmix-assets 元素的代码如下:

// Avoid adding everything twice
var alreadyAttached = false;
  
// Needed to masquerade as an a-assets element
var fileLoader = new THREE.FileLoader();
  
window.AFRAME.registerElement('streetmix-assets', {
  prototype: Object.create(window.AFRAME.ANode.prototype, {
    createdCallback: {
      value: function() {
        // Masquerade as a an a-asset-item so that a-assets will wait for it to load
        this.setAttribute('src', '')
        this.isAssetItem = true;
        
        // Properties needed for compatibility with a-assets prototype
        this.isAssets = true;
        this.fileLoader = fileLoader;
        this.timeout = null;
      }
    },
    attachedCallback: {
      value: function () {
        if (alreadyAttached) return;
        if (this.parentNode && this.parentNode.hasLoaded) console.warn("Assets have already loaded. streetmix-assets may have problems")
        
        alreadyAttached = true;
        
        // Set the innerHTML to all of the actual assets to inject
        this.innerHTML = buildAssetHTML(this.getAttribute("url"));
        
        var parent = this.parentNode
        
        // Copy the parent's timeout, so we don't give up too soon
        this.setAttribute('timeout', parent.getAttribute('timeout'))
        
        // Make the parent pretend to be a scene, since that's what a-assets expects
        this.parentNode.isScene = true
        
        // Since we expect the parent element to be a-assets, this will invoke the a-asset attachedCallback, 
        // which handles waiting for all of the children to load. Since we're calling it with `this`, it 
        // will wait for the streetmix-assets's children to load
        Object.getPrototypeOf(parent).attachedCallback.call(this)
        
        // No more pretending needed
        this.parentNode.isScene = false
      }
    },
    load: {
      value: function() {
        // Wait for children to load, just like a-assets
        AFRAME.ANode.prototype.load.call(this, null, function waitOnFilter (el) {
          return el.isAssetItem && el.hasAttribute('src');
        });
      }
    }
  })
})

关于第一个片段的一些简短说明:

  • buildAssetHTML(assetUrl) 是一个函数,它 returns 一个包含您所有资产的 HTML 代码的字符串。 (例如 <img src="myimage.jpg><a-asset src="my-asset.glb></a-asset> 等)
  • this.getAttribute("url") 允许用户指定 url 从中加载资源。所以它可以像这样使用:<streetmix-asset url="http://mycdn.example.com/streetmix"></streetmix-asset>
  • alreadyAttached 防止多次意外添加 streetmix 资产。

此代码段应该让您到达用户可以包含您的库然后放置的地步:

<a-assets>
  <streetmix-asset></streetmix-asset>
</a-assets>

在自己的场景中正确加载您的资产。但是,如果用户不包含 streetmix-assets 元素,则不会加载您的资产。

为了确保您的资产无论如何都能加载(即 aframe-vartiste-toolkit 中资产背后的意图),需要考虑三种情况:

  1. 用户已包含 a-assets 元素 streetmix-assets 元素。
  2. 用户既没有包含 a-assets 元素 也没有包含 streetmix-assets 元素。
  3. 用户包含了 a-assets 元素 但没有包含 streetmix-assets 元素。

第一种情况已经通过注册元素得到处理。第二种情况可以通过监听 DOMContentLoaded 以相当直接的方式处理,例如:

window.addEventListener('DOMContentLoaded', (e) => { 
  if (alreadyAttached) return;
  let assets = document.querySelector('a-assets')
  if (!assets)
  {
    assets = document.createElement('a-assets')
    document.querySelector('a-scene').append(assets)
  }
  
  if (assets.hasLoaded)
  {
    console.warn("Assets already loaded. May lead to bugs")
  }
  
  let streetMix = document.createElement('streetmix-assets')
  assets.append(streetMix)
});

但是,在情况 3 中,这将 运行 变成您现在遇到的相同问题,因为 a-assets 可能在 DOMContentLoaded 发出时已经完成加载。所以我们会忘记 DOMContentLoaded 事件。

相反,我们将使用 DOMSubtreeModified 事件同时处理案例 2 和案例 3。我们可以使用 DOMSubtreeModifieda-scene 到位后立即添加 streetmix-assets 元素。代码看起来像这样:

var domModifiedHandler = function(evt) {
  // Only care about events affecting an a-scene
  if (evt.target.nodeName !== 'A-SCENE') return;
  
  // Try to find the a-assets element in the a-scene
  let assets = evt.target.querySelector('a-assets');  
  
  if (!assets) {
    // Create and add the assets if they don't already exist
    assets = document.createElement('a-assets')
    evt.target.append(assets)
  }
  
  // Already have the streetmix assets. No need to add them
  if (assets.querySelector('streetmix-assets')) {
    document.removeEventListener("DOMSubtreeModified", domModifiedHandler);
    return
  }
  
  // Create and add the custom streetmix-assets element
  let streetMix = document.createElement('streetmix-assets')
  assets.append(streetMix)
  
  // Clean up by removing the event listener
  document.removeEventListener("DOMSubtreeModified", domModifiedHandler);
}

document.addEventListener("DOMSubtreeModified", domModifiedHandler, false);

所有这一切都应该允许您非常可靠地将资源从包含的库注入到用户的场景中。请注意,此解决方案在很大程度上依赖于一些可能在未来版本中更改的 A-Frame 实现细节。