I am seeking advice on implementing an A-Frame scene using OffscreenCanvas. I am using https://github.com/spite/ccapture.js to record my VR scene and the result is very janky and slow when FPS is above 30.
The video and the code samples within the Google link below provide examples of modifying Three.js to leverage OffscreenCanvas. Thus, I believe it is possible to modify A-Frame to include an OffscreenCanvas option.
https://developers.google.com/web/updates/2018/08/offscreen-canvas
https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
I have experimented with modifying the A-Frame a-scene setupRenderer function to use an OffscreenCanvas within the rendererConfig, but I am running into getContext issues.
I am not sure what the best approach is to offload rendering (and getting blob/bitmap image) to a Web Worker.
I want to take advantage of rendering in the Worker without conflicting with the built in requestAnimationFrame functionality of A-Frame.
Any advice is greatly appreciated. Thank you.
https://github.com/aframevr/aframe/blob/master/src/core/scene/a-scene.js#L561
setupRenderer: {
value: function () {
var self = this;
var renderer;
var rendererAttr;
var rendererAttrString;
var rendererConfig;
const offscreenCanvas = this.canvas.transferControlToOffscreen();
const worker = new Worker('./canvasworker.js');
worker.postMessage({ msg: 'init', canvas: offscreenCanvas }, [offscreenCanvas]);
rendererConfig = {
alpha: true,
antialias: !isMobile,
canvas: offscreenCanvas,
logarithmicDepthBuffer: false
};
this.maxCanvasSize = {height: 1920, width: 1920};
if (this.hasAttribute('renderer')) {
rendererAttrString = this.getAttribute('renderer');
rendererAttr = utils.styleParser.parse(rendererAttrString);
if (rendererAttr.precision) {
rendererConfig.precision = rendererAttr.precision + 'p';
}
if (rendererAttr.antialias && rendererAttr.antialias !== 'auto') {
rendererConfig.antialias = rendererAttr.antialias === 'true';
}
if (rendererAttr.logarithmicDepthBuffer && rendererAttr.logarithmicDepthBuffer !== 'auto') {
rendererConfig.logarithmicDepthBuffer = rendererAttr.logarithmicDepthBuffer === 'true';
}
this.maxCanvasSize = {
width: rendererAttr.maxCanvasWidth
? parseInt(rendererAttr.maxCanvasWidth)
: this.maxCanvasSize.width,
height: rendererAttr.maxCanvasHeight
? parseInt(rendererAttr.maxCanvasHeight)
: this.maxCanvasSize.height
};
}
renderer = this.renderer = new THREE.WebGLRenderer(rendererConfig);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.sortObjects = false;
if (this.camera) { renderer.vr.setPoseTarget(this.camera.el.object3D); }
this.addEventListener('camera-set-active', function () {
renderer.vr.setPoseTarget(self.camera.el.object3D);
});
loadingScreen.setup(this, getCanvasSize);
},
writable: window.debug
},
I would like to take advantage of OffscreenCanvas and rendering on a Web Worker to maintain a consistent 60 fps.
Related
so I'm experimenting with the LOD object in Three.js however I'm having trouble getting it to work. The code that i am using (excluding some standard setup code like creating a scene etc.) is shown here:
let treeHighDetail = new THREE.Object3D();
let treeLowDetail = new THREE.Object3D();
const gltfLoader = new GLTFLoader();
gltfLoader.load('./resources/low_poly_tree.glb', (gltf) => {
console.log(gltf)
treeLowDetail.add(gltf.scene);
treeLowDetail.traverse(c => {
c.castShadow = true;
})
})
gltfLoader.load('./resources/tree.glb', (gltf) => {
console.log(gltf)
treeHighDetail.add(gltf.scene);
treeHighDetail.traverse(c => {
c.castShadow = true;
})
})
const lod = new THREE.LOD();
let treeMesh = treeHighDetail.clone();
lod.addLevel(treeMesh, 5);
treeMesh = treeLowDetail.clone();
lod.addLevel(treeMesh, 20);
scene.add(lod);
I have two tree models, one low poly and one relatively high. I am attempting to have the high poly tree be displayed when i am close to the object, and the low poly tree render when i am further away from it. Running this code doesn't produce any errors in the console, however none of the models are rendered into the scene at any distance. Any ideas on what the issue could be would be greatly appreciated.
You are accessing the glTF asset before they have been loaded. The above load() method calls are asynchronously so you have to wait for their completion.
There are different ways to achieve this. You could create an instance of THREE.LoadingManager, use it as an argument for GLTFLoader's constructor call and then use its onLoad() callback for creating the LODs.
Or you rewrite your code to use the async/await pattern which is probably the more elegant solution. It would look like so:
const gltfLoader = new GLTFLoader();
const [ gltfLowDetail, gltfHighDetail ] = await Promise.all( [
gltfLoader.loadAsync( './resources/low_poly_tree.glb' ),
gltfLoader.loadAsync( './resources/tree.glb' ),
] );
const treeLowDetail = new THREE.Object3D();
const treeHighDetail = new THREE.Object3D();
treeLowDetail.add(gltfLowDetail.scene);
treeLowDetail.traverse(c => {
c.castShadow = true;
})
treeHighDetail.add(gltfHighDetail.scene);
treeHighDetail.traverse(c => {
c.castShadow = true;
})
const lod = new THREE.LOD();
let treeMesh = treeHighDetail.clone();
lod.addLevel(treeMesh, 5);
treeMesh = treeLowDetail.clone();
lod.addLevel(treeMesh, 20);
scene.add(lod);
You have to ensure the function/method holding this code is async as well.
ive got a component where i try to make some sort of magnetic snap scroll. i know the css usual css way of snap-y, and setting snap behavior to smooth, but the snap is too fast. so i tried on my own to create a component where if the app detects a scroll down or up with the function, and a scroll below
const [scrollDir, setScrollDir] = useState("");
const [index, setIndex] = useState("0")
useEffect(() => {
const threshold = 0;
let lastScrollY = window.pageYOffset;
let ticking = false;
const updateScrollDir = () => {
const scrollY = window.pageYOffset;
if (Math.abs(scrollY - lastScrollY) < threshold) {
ticking = false;
return;
}
if(scrollY>lastScrollY){
// setScrollDir("scrolling down")
scroller.scrollTo('about',{
duration: 1000,
delay: 100,
smooth: 'linear',
})
}
// setScrollDir(scrollY > lastScrollY ? "scrolling down" : "scrolling up");
lastScrollY = scrollY > 0 ? scrollY : 0;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDir);
ticking = true;
}
};
window.addEventListener("scroll", onScroll);
console.log(scrollDir);
return () => window.removeEventListener("scroll", onScroll);
}, [scrollDir]);
the magnetic scroll doesnt work, the page goes all jumpy. i could add a global css to make smooth scrolling, but that defeats the purpose of giving easein animation of scrolling. is there a way to work around this?
I have loaded different textures using textureLoader and I am trying to update them using dat.gui controls.
Why is the code below not working?
gui.add(mesh.position, "y", -1, 1, 0.1);
gui.add(mesh.material, "map", { alpha: alphaTexture, color: colorTexture, normal: normalTexture })
.onChange(() => {
mesh.material.needsUpdate = true;
console.log("updated");
});
It gives this error:
"Uncaught TypeError: m is undefined" [error][1]
After some tweaking, I found that the values of object(or array) in the third argument only supports string types, so passing a object as a value would not work.
This is the closest workaround that I could think of..
/* GUI options */
const guiOptions = {
mesh_material_map: "color",
};
/* Textures */
const textureLoader = new THREE.TextureLoader(loadingManager);
const colorTexture = textureLoader.load("/textures/door/color.jpg");
const alphaTexture = textureLoader.load("/textures/door/alpha.jpg");
const normalTexture = textureLoader.load("/textures/door/normal.jpg");
const guiTextureHash = {
color: colorTexture,
alpha: alphaTexture,
normal: normalTexture,
};
/* Add to gui */
gui.add(guiOptions, "mesh_material_map", Object.keys(guiTextureHash)).onChange((value) => {
mesh.material.map = guiTextureHash[value];
mesh.needsUpdate = true;
console.log("updated", value);
});
I found your topic looking for a texture picker. It's probably a little away from your starting point but could help some other. I finally made a simple texture picker with a dropdown selection key with dat.gui. The goal is to be able to change on the fly my matcap texture, going through an array of loaded texture.
const gui = new dat.GUI()
const textureLoader = new THREE.TextureLoader()
const myMatCap = [
textureLoader.load('./textures/matcaps/1.png'),
textureLoader.load('./textures/matcaps/2.png'),
textureLoader.load('./textures/matcaps/3.png')
]
const parameters = {
color: 0xff0000,
matCapTexture: 0
}
const updateAllMaterials = () => {
scene.traverse( (child)=>{
if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshMatcapMaterial) {
child.material.matcap = myMatCap[ parameters.matCapTexture]
child.material.needsUpdate = true
}
})
}
gui.add(parameters, 'matCapTexture', {
terracotta: 0,
grey: 1,
chrome: 2,
}).onFinishChange(()=>{
updateAllMaterials()
})
let mesh = new THREE.Mesh(
geometry,
new THREE.MeshMatcapMaterial({
side: THREE.DoubleSide,
matcap: myMatCap[ parameters.matCapTexture ]
})
);
scene.add(mesh)
I'm hoping to get some help using geometry instancing with A-Frame. I was trying to figure out the bottleneck for my web app and after implementing pooling for physics objects being created in the scene, saw that the number of draw calls was increasing with each new object -- I had thought that by utilizing the asset management system in A-Frame my models were automatically cached, but I think I was mistaken.
I was wondering, if I register the geometry of the model using AFRAME.registerGeometry, would I be able to utilize geometry instancing? I saw that creating from a pool of object using the A-Frame geometry primitives did not increase the geometry count of the scene on a per-entity basis. I took a shot at loading my GLTF and registering the geometry from the mesh, but I'm getting an error from a-node that I don't understand:
<script>
AFRAME.registerGeometry('ramshorn', {
schema: {
depth: {default: 1, min: 0},
height: {default: 1, min: 0},
width: {default: 1, min: 0},
},
init: function(data) {
var model = null;
var geometry = null;
var manager = new THREE.LoadingManager();
manager.onLoad = function () {
console.log(geometry);
this.geometry = geometry;
console.log(this.geometry);
}
var gltfLoader = new THREE.GLTFLoader(manager);
gltfLoader.setCrossOrigin('anonymous');
const src = "./assets/ramsHorn/Ram's Horn 2.gltf";
gltfLoader.load(src, function ( gltf ) {
console.log("Gltf: " + gltf);
model = gltf.scene;
console.log("Model: " + model)
model.children.forEach((child) => {
console.log(child);
});
gltf.scene.traverse(function (o) {
if (o.isMesh) {
geometry = o.geometry;
//console.log(geometry);
//tried assigning "this.geometry" here
}
});
}, undefined, function ( error ) {
console.error(error);
});
//tried assigning "this.geometry" here
}
});
</script>
Error:
core:a-node:error Failure loading node: TypeError: "t is undefined"
aframe-master.min.js:19:658
Any help with this would be appreciated! Thanks
The code at 19:658 in aframe-master.min.js is trying to run something with a variable t but it has not been declared.
By using aframe-master.js it would be possible to get a more meaningful error.
This is my first attempt at building an a-frame experience, and I'm doing so by cobbling together multiple community components.
I'm currently attempting to implement a spritesheet animation using aframe-spritesheet-component ( https://github.com/EkoLabs/aframe-spritesheet-component ) and having an issue getting it to play well with others.
My build here: https://afantasy.github.io/spritetest/
So far, I've managed to get the spritesheet animation to loop through once, after which it causes a total freeze of the entire environment.
I am not sure whether it's a bad interaction with the other components I've installed, or with the aframe framework itself.
From what I gather from the console, something is up with the update / tick functions.
I've managed to isolate the direct cause to this bit of code I grabbed directly from the aframe-spritesheet-component example on github ( https://ekolabs.github.io/aframe-spritesheet-component/examples/rowscols/ )
<!-- SPRITE ANIMATION LOOP -->
<script type="text/javascript">
var animation = { progress: 0 };
var tween = new TWEEN.Tween(animation)
.to({ progress: 1 }, 1000)
.onUpdate(function(){
document.querySelector('a-image').setAttribute('sprite-sheet', 'progress', animation.progress);
});
tween.onComplete(function() { animation.progress = 0; });
tween.chain(tween);
tween.start();
</script>
Super stumped and sure how to proceed from here, any hints would be appreciated.
Here is the code for an alternative spritesheet animation component that might work for you, without requiring the external TWEEN.js dependency:
AFRAME.registerComponent('spritesheet-animation', {
schema:
{
rows: {type: 'number', default: 1},
columns: {type: 'number', default: 1},
// set these values to play a (consecutive) subset of frames from spritesheet
firstFrameIndex: {type: 'number', default: 0},
lastFrameIndex: {type: 'number', default: -1}, // index is inclusive
// goes from top-left to bottom-right.
frameDuration: {type: 'number', default: 1}, // seconds to display each frame
loop: {type: 'boolean', default: true},
},
init: function()
{
this.repeatX = 1 / this.data.columns;
this.repeatY = 1 / this.data.rows;
if (this.data.lastFrameIndex == -1) // indicates value not set; default to full sheet
this.data.lastFrameIndex = this.data.columns * this.data.rows - 1;
this.mesh = this.el.getObject3D("mesh");
this.frameTimer = 0;
this.currentFrameIndex = this.data.firstFrameIndex;
this.animationFinished = false;
},
tick: function (time, timeDelta)
{
// return if animation finished.
if (this.animationFinished)
return;
this.frameTimer += timeDelta / 1000;
while (this.frameTimer > this.data.frameDuration)
{
this.currentFrameIndex += 1;
this.frameTimer -= this.data.frameDuration;
if (this.currentFrameIndex > this.data.lastFrameIndex)
{
if (this.data.loop)
{
this.currentFrameIndex = this.data.firstFrameIndex;
}
else
{
this.animationFinished = true;
return;
}
}
}
let rowNumber = Math.floor(this.currentFrameIndex / this.data.columns);
let columnNumber = this.currentFrameIndex % this.data.columns;
let offsetY = (this.data.rows - rowNumber - 1) / this.data.rows;
let offsetX = columnNumber / this.data.columns;
if ( this.mesh.material.map )
{
this.mesh.material.map.repeat.set(this.repeatX, this.repeatY);
this.mesh.material.map.offset.set(offsetX, offsetY);
}
}
});
A complete example of the component in code:
https://github.com/stemkoski/A-Frame-Examples/blob/master/spritesheet.html
Live preview of example:
https://stemkoski.github.io/A-Frame-Examples/spritesheet.html
Your app.js is minified, so it's hard to trace the issue. However, I replaced the Tween library in your version with the exact Tween library used on their version and it seemed to work:
https://stackoverflow-51620935.glitch.me
My guess is that either you may be forgetting to configure Tween (I have never used this library before) or are using an unsupported version (the console also shows several warnings about deprecated methods).
I copy/pasted your code into that glitch above, and changed line 96 to include the overwrite:
<!-- SPRITE ANIMATION LOOP -->
<script src="tween-rewrite.js"></script>
<script type="text/javascript">
var animation = { progress: 0 };
var tween = new TWEEN.Tween(animation)
.to({ progress: 1 }, 1000)
.onUpdate(function(){
document.querySelector('a-image').setAttribute('sprite-sheet', 'progress', animation.progress);
});
tween.onComplete(function() { animation.progress = 0; });
tween.chain(tween);
tween.start();
</script>
Ran into the same problem. Looping the animation like this works for me:
<script>
function smokeLoop(){
var animation = { progress: 0 };
var tween = new TWEEN.Tween(animation)
.to({ progress: 1 }, 4000)
.onUpdate(function(){
document.querySelector('a-image').setAttribute('sprite-sheet', 'progress', animation.progress);
});
tween.onComplete(function() { animation.progress = 0; smokeLoop();});
tween.start();
}
smokeLoop();
</script>