Animate Opacity of Aframe Shadow - aframe

So, I've been messing around with opacity fade-in/fade-outs of glTF models in Aframe, and have achieved good results using Piotr Adam Milewski's model-opacity script (from here), and have looped my daisy-chained animation sequences using Tired Eyes' animation-manager script (from here).
However, I'm having difficulties trying to work out how to also animate the opacity of the model's shadow, as at the moment its shadow still remains visible after the model is no longer visible.
Demo Link 👇
I've remixed a Glitch (of Ada Rose Cannon's AR Starter Kit) which you can find here to show what I mean (see line 204 in the Glitch for the model fade-in/out).
I'd be really grateful if anyone can shed any light on whether it's possible to animate the Aframe shadow to match the model's opacity. Many thanks, in advance, for any advice 🙂

I'm afraid a built-in solution is not yet ready (issue / PR)
For a single object, you could just use a ShadowMaterial, which has a opacity property, which could be animated along with the objects opacity:
<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
<script src="https://gftruj.github.io/webzamples/aframe/models/components/model-opacity.js"></script>
<script>
AFRAME.registerComponent("relative-shadow", {
schema: {
target: { type: "selector" }
},
init: function() {
const mesh = this.el.getObject3D("mesh"); // grab the mesh
const oldMaterial = mesh.material; // store the old material
mesh.material = this.material = new THREE.ShadowMaterial(); // apply a shadow material
oldMaterial.dispose(); // dispose the old material
},
update: function() {
this.opacitySource = this.data.target.components["model-opacity"]; // react to the target being set
},
tick: function() {
if (!this.opacitySource) return; // wait until we can access the opacity value
// update the opacity using the t-rex material opacity from the component
this.material.opacity = this.opacitySource.data.opacity;
}
})
</script>
<a-scene>
<a-asset-item id="spino" src="https://rawcdn.githack.com/krlosflip22/aframe-ar-sample/c91a7a9dd8b1428bc8e68bc1b5d8641d7241fd1b/spinosaurus.glb"></a-asset-item>
<a-gltf-model id="trex" position="0 1 -4" shadow="cast: true" scale="0.5 0.5 0.5" src="#spino" model-opacity
animation="property: model-opacity.opacity; to: 0; dur: 1000; dir: alternate; loop: true;"></a-gltf-model>
<a-plane rotation="-90 0 0" scale="40 40 40"
relative-shadow="target: #trex" shadow="receive: true"></a-plane>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
For multiple objects, a simple yet effective solution would be faking shadows - have transparent radial gradient images below the objects like in the threejs fundamentals example. You could control the opacity of each one of them:
<iframe width="100%" height="100%" src="https://r105.threejsfundamentals.org/threejs/threejs-shadows-fake.html"></iframe>

Related

Change opacity of glb 3D model with aframe

I am trying to make a web app where I can use world tracking to view a 3D model. I am trying to change the opacity of the model using the material property of the a-entity tag, however it does not seem to be working. any idea on how to change the opacity of my 3D object in order to make it translucent?
<a-entity
id="model"
gltf-model="#3dmodel"
class="cantap"
geometry="primitive: box"
scale="0.5 0.5 0.5"
material="transparent: true; opacity 0.1"
animation-mixer="clip: idle; loop: repeat"
hold-drag two-finger-spin pinch-scale>
</a-entity>
GLTF models come with their own materials included in the model file. These are handled by THREE.js rather than AFrame, so in order to access their properties you have to search through the elements object3D, using something like this:
this.el.object3D.traverse((child) => {
if (child.type === 'Mesh') {
const material = child.material;
// Do stuff with the material
material.opacity = 0.1;
}
})
See the THREE.js documentation on Materials and Object3D for more detail.
Also, I noticed you have both a geometry primitive and a gltf-model on that entity. Not sure how well those two things can co-exist, if you want both a box and the model you should probably make one a child of the other.
E Purwanto's answer above works for me with an addition of setting transparency true:
this.el.object3D.traverse((child) => {
if (child.type === 'Mesh') {
const material = child.material;
// Do stuff with the material
material.transparent = true; // enable to modify opacity correctly
material.opacity = 0.1;
}
})

animating a custom attribute in a custom component

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.

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
}

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.

How do I "preserve" an entity when changing its parent in the DOM?

I have a "variable assignment" component that looks like the following (the blue diamond with the yellow sphere attached):
http://i.imgur.com/nJotPgW.gif (unfortunately I don't have enough reputation to inline the image)
Here the yellow sphere that things can snap into is created in the initialization of the variable assignment component like so
AFRAME.registerComponent('variable-assignment', {
schema: {
grabbable: {default: true}
},
init: function () {
this.el.innerHTML = `
<a-sphere
snap-site="controller:#right-hand"
radius=".1"
color="yellow"
material="transparent:true; opacity:.5;"
position=".22 0 0">
</a-sphere>
`;
this.label = 'x';
this.el.setAttribute('geometry', {
primitive: 'octahedron',
radius: .1,
color: 'blue'
});
...
The snap-site component has code that detects a collision with the red sphere and then makes it a child element. So the DOM looks something like this before the collision.
<a-sphere color=red></a-sphere>
<a-entity variable-assignment>
<a-sphere snap-site>
<a-sphere>
</a-entity>
and after
<a-entity variable-assignment>
<a-sphere snap-site>
<a-sphere color=red></a-sphere>
<a-sphere>
</a-entity>
The problem is when I want to move the entity with variable-assignment inside another DOM element using appendChild the initialization function for the variable-assignment called again the innerHTML is reset. So for example if we have
<a-entity container></a-entity>
<a-entity variable-assignment>
<a-sphere snap-site>
<a-sphere color=red></a-sphere>
<a-sphere>
</a-entity>
And we want to move variable-assignment into container using something like containerElement.appendChild(variableAssignmentEntity) the inner red sphere gets removed
<a-entity container>
<a-entity variable-assignment>
<a-sphere snap-site>
<a-sphere>
</a-entity>
</a-entity>
As a work around/hack I was thinking about using a flag of some sort to see if initialize had already been called before for the element/entity the component was a property of and then not run the initialization code, something like
init: function () {
if (this.el.getAttribute('initialized')) {
return;
}
this.el.setAttribute('initialized', true);
...
but this seems like a bad way to do it and also looking into the A-Frame source it seems appendChild causes all components to be removed then added again so it doesn't actually work either or at least causes other things to break.
Is there a good way to do this or is there a different way to define the variable-assignment component so the yellow sphere snap-site component isn't a child set in the initialization?
You're correct that appendChild causes components to be re-initialized, and might not work as expected. Here are a couple alternatives:
After removing the element, call el = el.cloneNode() to make a clean copy and append that, instead.
Don't actually make the sphere a child of your variable-assignment component, but instead just have the 'parent' component update the position/rotation of the child in the tick() function.
Use the underlying three.js API to pass around the THREE.Object3D, without actually changing the parent of any elements.
An example of (3):
var mesh = el.getObject3D('mesh)';
el.removeObject3D('mesh');
otherEl.setObject3D('mesh', mesh);

Resources