动态添加“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.js
和 street.js
.
中应用了上述逻辑
我认为最好的方法是创建自定义 A-Node
元素。我们称它为 streetmix-assets
。只要我们在 a-assets
元素完成加载之前将 streetmix-assets
附加到 a-assets
元素,我们就可以强制 a-assets
等待所有其余资源加载。
这里有两个棘手的部分:
- 说服
a-assets
等待 streetmix-assets
完成加载。
- 阻止
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 中资产背后的意图),需要考虑三种情况:
- 用户已包含
a-assets
元素 和 streetmix-assets
元素。
- 用户既没有包含
a-assets
元素 也没有包含 streetmix-assets
元素。
- 用户包含了
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。我们可以使用 DOMSubtreeModified
在 a-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 实现细节。
我已经发布了一个 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.js
和 street.js
.
我认为最好的方法是创建自定义 A-Node
元素。我们称它为 streetmix-assets
。只要我们在 a-assets
元素完成加载之前将 streetmix-assets
附加到 a-assets
元素,我们就可以强制 a-assets
等待所有其余资源加载。
这里有两个棘手的部分:
- 说服
a-assets
等待streetmix-assets
完成加载。 - 阻止
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 中资产背后的意图),需要考虑三种情况:
- 用户已包含
a-assets
元素 和streetmix-assets
元素。 - 用户既没有包含
a-assets
元素 也没有包含streetmix-assets
元素。 - 用户包含了
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。我们可以使用 DOMSubtreeModified
在 a-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 实现细节。