animating a custom attribute in a custom component - aframe

I am attempting to animate a material property that is buried inside of a gltf-model (that has many objects). I can find the parameter and animate it with a tick event, and it looks like this
https://glitch.com/~uv-reveal-wave
It almost works, but animating with formulas is a nightmare. Instead I want to control it with the animation component. Unfortunately, exposing the parameter directly seems impossible, as I have to use getObject3D.traverse() just to locate the object, and then get the parameter.
Instead I have made a custom attribute (in schema) and and animating that attribute. And in the update function, I'm using that attribute to drive the buried parameter. It should work, but I can't seem to get a response in the update function. Iow, my custom attribute is not animating.
AFRAME.registerComponent('matclipplane', { // attached to gltf-model tree
schema:{
clipHeightA:{type: 'number', default: 0}
},
init: function () {
let el = this.el;
let comp = this;
comp.treeModels = [];
el.addEventListener('model-loaded', function(ev){
let mesh = el.getObject3D('mesh');
mesh.traverse(function(node){
if (node.isMesh){
comp.treeModels.push(node);
}
comp.modelLoaded = true;
});
...
update: function(){
console.log("update"); // returns nothing. no update from
animating attributes clipHeightA
let comp = this;
if (comp.modelLoaded){
comp.treeModels[1].material.map.offset.y =
this.data.clipHeightA;
}
}
...
AFRAME.registerComponent("click-listener", { // attached to box button
schema:{
dir:{type:"boolean", default: false}
},
init: function(){
let el=this.el;
let data = this.data;
el.addEventListener("click", function(evt){
let tree = document.querySelector('#tree');
data.dir = !data.dir;
if(data.dir){
el.setAttribute("material",'color', 'orange');
tree.emit("grow");
} else {
el.setAttribute('material','color','#332211');
tree.emit('shrink');
}
});
}
});
<a-entity id="tree" gltf-model="#tree" scale="5 5 5"
animation__grow="property: clipHeightA; from: 0; to: 1;
startEvents: grow; dur: 500"
matclipplane></a-entity>
<a-entity id="button" geometry="primitive: box" material="color:orange;
metalness: 0.5" scale="0.2 0.2 0.2" class="clickable" click-listener></a-
entity>
<a-entity id="mouseCursor" cursor="rayOrigin: mouse" raycaster="objects:
.clickable"></a-entity>
here is the glitch in progress
https://glitch.com/~uv-reveal
Clicking on the orange cube should launch the uv reveal animation. I expect that calling a custom component attribute should trigger an update event, but the update does not log in the console.
Not sure how to animate a parameter inside a gltf.
Do I have to implement the animejs component directly inside my custom component?
Or can this be done with the animate component?

According to the docs you can animate properties through
object3D like animation="property: object3D.x"
components like animation="property: components.myComp.myProp"
I don't see it mentioned (although it's used in the docs) but it also works when you provide the property as the components attribute:
animation__grow="property: matclipplane.clipHeightA;"
glitch here. You can see update is being called.
Also tree was an id of both the asset and the component, therefore grow was emitted on the asset item.

Related

How to access the interior of gltf-model object3D.children.material in AFrame

I'm loading a gltf-model into aframe and some materials on some objects need adjusting. I am attempting to isolate and manipulate them by directly accessing the object3D property of the entity that contains the gltf component. The part of the object3D tree that I need to access is the .children Array. When I log that part to the console, it is an empty array, but I can twirl it down in the console and see the object properties I need. How do I access this in my script? the .children property is returning an empty array.
You can see my project here:
http://www.sensorium.love/experiments/yamashiro/walkthroughlit2/bonsaiLightsTest.html
The small black rectangle with the flare texture on it is one of many planes from the gltf. The large flare in the background is a primitive I made in aframe with the material as I would like it to be. I attempted to assign this to another plane object in my gltf, and it did not render. It should be applied to the child.
If you inspect the console, you can see where I've logged this children array. It is an empty array, and yet if you twirl down the arrow next to it, you can see the underlying data I'm trying to access. But I can't understand how to access that in my script.
AFRAME.registerComponent('flareplanes',{
init:function(){
let l1 = document.querySelector('#lta1');
let lm3D1 = l1.object3D;
console.log(lm3D1);
let lmc = lm3D1.children;
console.log(lmc);
for(let propName in lmc){
console.log(lmc[propName]);
}
}
});
<a-scene>
<a-assets>
<a-asset-item id="bonsailights" src="BonsaiLights.glb" ></a-asset-item>
<img id="flare" src="assets/ledFlare.png"></a-asset-item>
</a-assets>
<a-entity id="lta1" gltf-part="src: #bonsailights;
part:BonsaiBendDLeafLiteL_01"></a-entity>
<a-entity id="lta2" gltf-part="src: #bonsailights; part:BonsaiBendDLeafLiteL_02" material="src: #flare; shader: flat; opacity: 0.99; blending: additive"></a-entity>
<a-entity id="plane" geometry="primitive: plane" position="1.0 1.6 2" rotation="0 180 0" material="src: #flare; shader: flat; opacity: 0.99; blending: additive" flareplanes></a-entity>
</a-scene>
let lmc = lm3D1.children;
console.log(lmc); // Array empty
console.log(lmc[0]); //undefined
// yet, in the console, twirling the arrow reveals the object I need to //access. It appears that this object is entry 0 in the array, but accessing //directly fails. How do I access this object in my script?
Try iterating through the mesh children, not the object3Ds:
var mesh = el.getObject3D('mesh');
mesh.traverse(node => {
if (node.isMesh) {
console.log(node.material)
}
});
Here's a glitch in which I access child materials to manipulate the opacity.
If the el.getObject3D('mesh') is null, try waiting for the model-loaded event:
handleModel: function() {
let mesh = this.el.getObject3D('mesh')
if (!mesh) {
this.el.addEventListener('model-loaded', this.handleModel.bind(this)
return
}
// the model should be loaded by this point
}

reference to normalMap in material returning undefined

I am attempting to make a simple demo, using dat.GUI to manipulate a material on an Aframe primitive . On the entity tag, I create a material, an assign a texture to the normal slot.
<a-entity id="circle" geometry="primitive:circle; radius: 2; segments: 96"
rotation="-90 0 0"
material="color: #335500; transparent: true; opacity: 0.9;
normalMap: #watertex; side: double"
water ></a-entity>
Then, in a custom component, I access its THREEjs mesh and material.
I am able to get a reference to the material, but when I try to access the normalMap, it returns undefined.
I added a listener, to wait for the "loaded" event. I am not using gltf, so "model-loaded" is unnecessary.
I used getObject3D("mesh"), and traversed the mesh, but still, the normal map still returns undefined. The puzzling part is that in the console, I log the material, and can clearly see the normalMap. But when log it, it returns undefined.
AFRAME.registerComponent("water",{
init: function(){
let el = this.el;
//console.log(el);
let comp = this;
this.counter=0;
el.addEventListener("loaded", function(ev){
console.log('model loaded');
let mesh = el.getObject3D('mesh');
if (!mesh){return;}
mesh.traverse(function(node){
if (node.isMesh){
console.log(node);
console.log(node.material);
console.log(node.material.normalMap); <-- returns
undefined
}
});
});
},
});
<a-entity id="circle" geometry="primitive:circle; radius: 2; segments: 96"
rotation="-90 0 0"
material="color: #335500; transparent: true; opacity: 0.9;
normalMap: #watertex; side: double"
water ></a-entity>
I am surprised that a reference to a material shows the normal map
console.log(node.material);
but a direct reference to the normal map fails
console.log(node.material.normalMap);
I suspect that perhaps something is strange with texture maps made with aframe not being accessible from Threejs, but I don't know why or how to test this.
Ultimately I want to use datGUI to control params in the materials.
I notice that AFrame material component does not expose all the parameters of threejs materials. Also accessing some aframe material parameters with datGUI seem a bit buggy. So this is why I want to access threejs directly.
Here is a link to the project on my server, where you can see the console logs.
http://sensorium.love/experiments/demos/water/waterDemo1.html
and here is a glitch if you want to play with the code
https://glitch.com/~water-demo
It seems to be a race, where you try to access the normalMap just before it's applied.
There is a materialtextureloaded event (docs) which seems to do the trick:
el.addEventListener('materialtextureloaded', e => {
// nomalMap is accessible here
})
Otherwise you can also use a timeout or interval to determine when the normalMap isn't null.
I've seen ar.js do:
let timer = setInterval(e => {
if (!someVal) return;
clearInterval(timer);
// here someVal isn't null or undefined
}, 500)
Glitch here.
I'm pretty sure you could see the normalMap in the console, because the expression gets evaluated once you click the arrow in the browser console. On chrome there's a blue "i" which says "value below was evaluated just now".

A-frame score counter with multiple objects

I've looked at this answer: Implementing a score counter in A-Frame
... but cannot figure out how to write a similar function that would work for multiple objects, since querySelector only finds the first element.
I'm trying to create a scene with 5-8 objects, and each one can only be clicked once. When an object is clicked, the counter will increment by 1.
Either use querySelectorAll:
var targets = querySelectorAll('a-box')`
for (var i = 0; i < targets.length; i++) { // Add event listeners... }
But better, use a container entity that listens to the click events that bubble up.
<a-entity listener>
<a-box data-raycastable></a-box>
<a-box data-raycastable></a-box>
</a-entity>
<a-entity cursor="rayOrigin: mouse" raycaster="objects: [data-raycastable]"></a-entity>
Component:
AFRAME.registerComponent('listener', {
init: function () {
this.el.addEventListener('click', evt => {
if (evt.target.dataset.wasClicked) { return; }
// evt.target tells you which was clicked.
evt.target.dataset.wasClicked = true;
// Score up.
});
}
});
As a side note, state component is a great place to store your score. https://www.npmjs.com/package/aframe-state-component

A-frame Want click on entity shows other entity

I have an entity and I want when I click or point with the a-frame cursor that is changes the attribute visible of another entity to true.
<a-entity id="pug" gltf-model="#pug" position="-1.75 0.0035 3"
scale="0.01 0.01 0.01" rotation="0 -11 0" >shadow
event-set__down="_event: mousedown; scale: 1.2 1.2 1.2"
event-set__up="_event: mouseup; scale: 1 1 1"
event-set__leave="_event: mouseleave; scale: 1 1 1">
</a-entity>
If I have that the interaction with the cursor works but I don't how (or even if) I can make it affect the attribute of another entity.
I want that :
<script>
$(document).ready(function(){
$("#pug").mouseenter(function(){
$('#bubble').attr('visible', 'true');
});
});
</script>
I do have cursor in my camera but I don't know... I'm new to A-Frame and I must be missing something :/ Thanks!
Keep in mind, that changing CSS properties won't affect the rendered entities. The best way to change properties is by using entity.setAttribute("attribute", "value")
I'm not sure if you can do a switch (visible / invisible) using the event-set component, but you can make an entity visible, or invisible by setting the visible attribute:
<a-entity event-set__click="_target: a-cylinder; visible: false;"></a-entity>
check it out here.
But i would recommend creating an a-frame component. You can check them out in the docs, in this case it looks like this:
AFRAME.registerComponent("foo", {
init: function() {
this.el.addEventListener("click", (e) => {
let cylinder = document.querySelector("a-cylinder")
cylinder.setAttribute("visible", !cylinder.getAttribute("visible"))
})
}
})
HTML:
<a-entity foo>
quite simple:
1) The AFRAME.registerComponent("foo" bit "declares" the component.
2) The init: function() function is executed when the component is initialized
3) Inside the click listener, I've made a simple toggle, which sets the visibility to the opposite of the actual value. If its visible, switch it to !visible = false.
Check it out here.

How can I make my entity spin when the user hovers over it?

I am using the event-set-component to cause my obj model under to increase scale when the cursor hovers over it.
This is working correctly.
But how would I make it spin as well as increase size?
I found the following code on AFrame docs but I do not know how to implement it so it triggers when the mouse is over the entity.
<a-animation attribute="material.opacity" begin="fade" to="0"></a-animation>
As you have asked for a different method in your comment I suggest to use a multi use component like the one I have written:
AFRAME.registerComponent('event-animate', {
schema: {
target: {type: 'selector'},
aevent: {default: 'animation1'},
triggeraction: {default: 'click'}
},
init: function () {
var data = this.data;
this.el.addEventListener(data.triggeraction, function () {
data.target.emit(data.aevent);
});
}
});
So in HTML it would look something like this:
<a-entity id="object1"
event-animate="target:object1;
triggeraction:mouseenter;
aevent:eventstart">
<a-animation attribute="scale"
dur="5000"
begin="eventstart"
from="1"
to ="5"
direction="alternate">
</a-animation>
<a-animation attribute="rotation"
dur="5000"
begin="eventstart"
from="0 0 0"
to="0 360 0"
direction="alternate">
</a-animation>
</a-entity>
The direction="alternate" should bring it back to its original position.
The quoted animation will work, if You set the begin event properly:
<a-animation attribute="rotation"
dur="2000"
begin="mouseenter"
to="0 360 0"
repeat="1"><a-animation>
On mouseenter, the animation triggers, and rotates the entity once.
To gain more control over what You do, You would need to get deep into making components.
1. The Easiest way i can think of, is using both the animation component, and Your own. You would need to set up a component listening for the mouseenter/mousexit, and trigger the animation:
AFRAME.registerComponent('mouseenterhandler', {
init: function () {
let el = this.el; //Your element reference
el.addEventListener('mouseenter, function () {
// get the rotation, by getAttribute, or by accessing the
//object3D.rotation
let rotation = el.getAttribute('rotation');
//lets rotate it to the same position
rotation.y += 360;
//set the animation component accordingly:
el.children[0].setAttribute('to',rotation);
//emit the 'begin' event:
el.emit('startrotating');
});
}
});
Quick Improvement if necessary: disable the listener, when the animation is triggered. Made with a boolean switched on the mouseenter event, and the animationend event.
2. You can choose not to use the animation component, and check on tick() if the cursor is over. If so, rotate the element by the actualRotation.y+0.1 ( or any other desired rotation ).
As noted before, You can access the rotation by getAttribute() or el.object3D.rotation.
As for the scale, You if You need to rotate + rescale the object on the mouseenter event, just add another animation, and use it like i did with the rotation.I'm not sure how it's usually done, in my experience animations are good, when there are not that many interactions, because they sometimes do unexpected things, which You have to predict/find out, and prevent.
On the other hand, making any animation manually ( changing properties on tick ) may seem laggy if the rotation delta is too big. You need to play with it, and find out which suits You best.

Resources