Aframe update camera fov at runtime - three.js

I've been trying to update properties of the camera on the fly to no avail - really don't want to setup multiple cameras and switch between them.
AFRAME.registerComponent("camera-controller", {
init: function() {
this.el.addEventListener('model-loaded', this.update.bind(this));
},
update: function() {
var data = this.data;
var el = this.el;
// el.fov = 75;
el.object3D.el.components.camera.data.fov = 20;
el.updateProjectionMatrix();
console.log(el.object3D.el.components.camera.data.fov);
}
});
camera.fov just returns undefined and updateProjectionMatrix() is not a function.

You can simply update the [camera]s FOV property via setAttribute("camera", "fov", newFOV). Something like this:
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script>
AFRAME.registerComponent("foo", {
init: function() {
// grab the camera
const cam = document.querySelector("[camera]")
// on click
document.body.addEventListener("click", e => {
// grab the current FOV
const fov = cam.getAttribute("camera").fov
// update the camera with the new FOV
cam.setAttribute("camera", "fov", fov == 80 ? 120 : 80)
})
}
})
</script>
<a-scene foo>
<a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
You can make the transition animated with the standard animation component (animating camera.fov) - here's an example with one animation (to utilize the above js logic, you could make two animations if you prefer a more 'declarative' style).

Related

A-frame random animation delay

I am creating a scene using A-frame (https://aframe.io) and I'm currently using an animation in my scene. I'm wondering how I can randomize the delay on my animation so the animation delays from a random amount from 0 to 2 seconds. How can this be done? My current code:
<html>
<head>
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
</head>
<body>
<a-scene>
<!--Animte the box with a ranom delay from 0 - 2 seconds. -->
<a-box position="-1 1.6 -5" animation="property: position; delay: 1000; to: 1 8 -10; dur: 2000; easing: linear; loop: true" color="tomato"></a-box>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
</body>
</html>
I currently have the delay set to 1000 (1 second since the delay is in milliseconds). What should happen instead is the delay should be a random amount from 0 - 2000 since 2000 milliseconds is 2 seconds. How can this be done?
Random delay only at the start - first cycle
Register the a-box component with a custom one that will do the math and apply it to the animation on the a-box <a-box position="-1 1.6 -5" random-delay animation color="tomato"></a-box>.
Building the custom aframe component:
AFRAME.registerComponent('random-delay', {
init: function (el) {
// this.el points to the element the component is placed on
var el = this.el;
}
});
Set your min and max values
AFRAME.registerComponent('random-delay', {
init: function (el) {
var el = this.el;
var min = 0;
var max = 2000;
}
});
Math.floor(Math.random() * (max - min + 1)) + min returns an integer random number between min and max and than we build the string for the animation:
AFRAME.registerComponent('random-delay', {
init: function (el) {
var el = this.el;
var min = 0;
var max = 2000;
// generates a random number between the set min and max
var delay = Math.floor(Math.random() * (max - min + 1)) + min;
var animation = `property: position; delay: ${delay}; from: -1 1.6 -5; to: 1 8 -10; dur: 2000; easing: linear; loop: true`
}
});
Apply the animation string with the delay generated with .setAttribue():
AFRAME.registerComponent('random-delay', {
init: function (el) {
var el = this.el;
var min = 0;
var max = 2000;
// generates a random number between the set min and max
var delay = Math.floor(Math.random() * (max - min + 1)) + min;
// builds the string for the animation
var animation = `property: position; delay: ${delay}; to: 1 8 -10; dur: 2000; easing: linear; loop: true`
// updating the animation component with the .setAttribute() function
el.setAttribute('animation', animation)
}
});
Random delay on every animation cycle
place these lines of code into a new function:
function setAnimation(min, max) {
// generates a random number between the set min and max
var delay = Math.floor(Math.random() * (max - min + 1)) + min;
var animation = `property: position; delay: ${delay}; from: -1 1.6 -5; to: 1 8 -10; dur: 2000; easing: linear; `
console.log(delay);
// updating the animation component with the .setAttribute function
el.setAttribute('animation', animation)
}
At every animationcomplete event set a new animation with the function we created
function setAnimation(min, max) {
// generates a random number between the set min and max
var delay = Math.floor(Math.random() * (max - min + 1)) + min;
var animation = `property: position; delay: ${delay}; from: -1 1.6 -5; to: 1 8 -10; dur: 2000; easing: linear; `
console.log(delay);
// updating the animation component with the .setAttribute function
el.setAttribute('animation', animation)
}
The result should look something like this:
<html>
<head>
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
</head>
<script>
AFRAME.registerComponent('random-delay', {
init: function (el) {
// this.el points to the element the component is placed on
var el = this.el;
let min = 0;
let max = 2000;
// initial animation
setAnimation(min, max)
function setAnimation(min, max) {
// generates a random number between the set min and max
let delay = Math.floor(Math.random() * (max - min + 1)) + min;
let animation = `property: position; delay: ${delay}; from: -1 1.6 -5; to: 1 8 -10; dur: 2000; easing: linear; `
console.log(delay);
// updating the animation component with the .setAttribute function
el.setAttribute('animation', animation)
}
el.addEventListener('animationcomplete', () => {
setAnimation(min, max)
});
}
});
</script>
<body>
<a-scene>
<a-box position="-1 1.6 -5" random-delay animation autoplay color="tomato">
</a-box>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
</body>
</html>

Threejs update single material on glb with multiple objects

I have a .glb model which has multiple objects and materials. I'm updating the children that have a certain material at runtime, but as there are nearly 1,000 children with this material it's choking a little. I'm looking for a more elegant solution if it exists.
AFRAME.registerComponent('apply-texture', {
schema: {
texture: { type: 'string' }
},
init: function() {
var data = this.data;
var el = this.el;
var loader = new THREE.TextureLoader();
var alphamap = loader.load("./images/PETAL-DATA.jpg");
el.object3D.traverse(function(child) {
if (child.isMesh && child.material.name == "Material_to_update") {
var texture = loader.load(data.texture,
function ( texture ) {
child.material.map = texture;
child.material.alphaMap = alphamap;
child.material.map.encoding = THREE.sRGBEncoding;
child.material.side = THREE.DoubleSide;
child.material.color.set("#FFFFFF");
child.material.alphaTest = 0.5;
child.material.opacity = 0.8;
child.material.needsUpdate = true;
},
function ( xhr ) {
console.log( (xhr.loaded / xhr.total * 100) + '% loaded' );
},
function ( xhr ) {
console.log( 'An error happened' );
}
);
}
As all the child objects are using the same material I'm hoping there's a way of just updating the material itself rather than the 1,000 children.
Instead of creating 1000 image loaders, use a system which will provide the loaded texture to each component:
<script src="https://aframe.io/releases/1.1.0/aframe.min.js"></script>
<script>
// register system
AFRAME.registerSystem("image-provider", {
init: function() {
// load some external image
this.image = new THREE.TextureLoader().load(
"https://i.imgur.com/wjobVTN.jpg"
);
},
getImage() {
// the components will grab the image via this function
return this.image;
}
});
// register the component
AFRAME.registerComponent("image-provider", {
init: function() {
// grab the system reference
const system = this.system;
// wait until the model is loaded
this.el.addEventListener("model-loaded", e => {
// grab the mesh
const mesh = this.el.getObject3D("mesh");
// apply the texture from the system
mesh.traverse(node => {
if (node.material) {
node.material.map = system.getImage();
node.material.needsUpdate = true;
}
});
});
}
});
</script>
<body>
<div style="z-index: 99999; position: fixed; top: 2.5%">
<p>
Model by Kelli Ray
</p>
</div>
<a-scene background="color: #FAFAFA">
<a-assets>
<a-asset-item id="model" src="https://cdn.glitch.com/994b27e1-a801-47b8-9b52-9547b8f25fc1%2Fgoogle%20poly.gltf"></a-asset-item>
<a-mixin id="fish" gltf-model="#model" scale="0.001 0.001 0.001" image-provider></a-mixin>
</a-assets>
<a-text position="-1.5 2 -3" color="black" value="original"></a-text>
<a-entity gltf-model="#model" position="-1 1 -3" scale="0.001 0.001 0.001"></a-entity>
<a-entity position="0 1 -3" mixin="fish"></a-entity>
<a-entity position="1 1 -3" mixin="fish"></a-entity>
<a-entity position="2 1 -3" mixin="fish"></a-entity>
<a-entity position="3 1 -3" mixin="fish"></a-entity>
</a-scene>
If You have multiple identical objects, you should look into Instanced Meshes - which allow you to place multiple objects while creating only one draw call. Check out this SO thread. You can check it out here (source)
"I guess what I'm trying to do is avoid having to traverse the model and just make an update to 1 material in the model, which in turn updates all objects that have that material applied"
If multiple mesh are actually sharing the same exact material (meaning they have the same uuid), if you update a property of the material then it'll update on all mesh using that material. I'm guessing they're not all sharing the same material though.
From what you've said, I think you just need to make your updates to the first mesh's material, create a variable referencing that material, then traverse your model and apply that material to every applicable mesh. For memory sake, it would also be good to .dispose() the materials that your replacing.

Can I put a THREE.js material on a gltf model?

Im currently working on my portfolio in A-frame. I'm trying to change the material of the objects to a wireframe material, i'm using the wireframe material (MeshBasicMaterial) of THREE.js to do that. If I put this material on objects like "a-box" or "a-sphere" it works, but i need to put this wireframe material on my 3D gltf model. Is this possible?
This is my script to call the material:
AFRAME.registerComponent('wireframe', {
dependencies: ['material'],
init: function () {
this.el.components.material.material.wireframe = true;
}
});
<a-box position="-1 0.5 -3" rotation="0 45 0" material="color: blue" wireframe></a-box>
It is possible to modify the material on a gltf.
One way to do it is to drill into the THREEjs level of the mesh inside the gltf, and create a new material and assign it a wireframe property.
AFRAME.registerComponent('tree-manager', {
init: function () {
let el = this.el;
let comp = this;
let data = this.data;
comp.scene = el.sceneEl.object3D;
comp.counter = 0;
comp.treeModels = [];
comp.modelLoaded = false;
// After gltf model has loaded, modify it materials.
el.addEventListener('model-loaded', function(ev){
let mesh = el.getObject3D('mesh');
if (!mesh){return;}
//console.log(mesh);
mesh.traverse(function(node){
if (node.isMesh){
let mat = new THREE.MeshStandardMaterial;
let color = new THREE.Color(0xaa5511);
mat.color = color;
mat.wireframe = true;
node.material = mat;
}
});
comp.modelLoaded = true;
});
}
});
<a-entity id="tree" gltf-model="#tree-gltf" scale="5 5 5" tree-manager></a-entity>
Here is a glitch that shows how.

animation component along with custom component in aframe

I am trying to add animation along y axis to an entity using the animation component and also trying to change it's position along z axis in a custom component. However, the animation component takes the precedence and the position change in z-axis in the custom component does not work.
I would like to change the position along y axis using the animation component and change the position along z-axis using my custom component.
Here is my code -
<a-entity id="orca" position="4 1 -18" gltf-model="#orca1" static-body animation="property: position.y; dir: alternate; dur: 1000; easing: easeInSine; loop: true; to: -1" move></a-entity>
AFRAME.registerComponent('move', {
schema: {
},
init: function() {
this.directionVec3 = new THREE.Vector3(0, 1, 1);
},
tick: function(t, dt) {
var directionVec3 = this.directionVec3;
var currentPosition = this.el.object3D.position;
directionVec3.z = dt/1000;
this.el.setAttribute('position', {
z: currentPosition.z + directionVec3.z
});
if (currentPosition.z > 10) {
this.el.setAttribute('position', {
z: -14
});
}
}
});
Could you please help? The only reason for me to choose the animation component for movement along y-axis is that I get the smooth sine curve movement using easeInSine.
As you’ve seen the animation component overrides your position. In this case you can apply your component to a parent entity to avoid the collision:
<a-entity move>
<a-entity id="orca" position="4 1 -18" gltf-model="#orca1" static-body animation="property: position.y; dir: alternate; dur: 1000; easing: easeInSine; loop: true; to: -1">
</a-entity>
</a-entity>

Kinetic JS - Calculate the degree of rotation of a circle

I am trying to calculate the angle of rotation of a circle, I am using the following script:
var circle = new Kinetic.Circle({
x: 256,
y: 256,
radius: 140,
stroke: 'black',
strokeWidth: 4 ,
offset: [0, 0],
draggable: true,
dragBoundFunc: function (pos) {
var pos = stage.getMousePosition();
var xd = 140 - pos.x;
var yd = 140 - pos.y;
var theta = Math.atan2(yd, xd);
var degree = (theta / (Math.PI / 180) - 45);
this.setRotationDeg(degree);
return {
x: this.getAbsolutePosition().x,
y: this.getAbsolutePosition().y
};
}
});
I don't think it is accurate, I added a shape inside the circle to see the rotation but could not group them together, I would appreciate your suggestions on how to calculate the degree of rotation and how to group the shape with the circle so the rotate at the same time. The complete project script is at http://jsfiddle.net/user373721/Ja6GB. Thanks in advance.
Here is how you calculate the angle of the mouse position from "12 o'clock"
Pretend your canvas is a clock centered in the canvas.
Here's how to calculate the angle of the current mouse position assuming 12 o'clock is zero degrees.
function degreesFromTwelveOclock(cx,cy,mouseX,mouseY){
// calculate the angle(theta)
var theta=Math.atan2(mouseY-centerY,mouseX-centerX);
// be sure theta is positive
if(theta<0){theta += 2*Math.PI};
// convert to degrees and rotate so 0 degrees = 12 o'clock
var degrees=(theta*180/Math.PI+90)%360;
return(degrees);
}
Here is complete code and a Fiddle: http://jsfiddle.net/m1erickson/HKq77/
<style>
body{ background-color: ivory; }
canvas{border:1px solid red;}
</style>
<script>
$(function(){
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var canvasOffset=$("#canvas").offset();
var offsetX=canvasOffset.left;
var offsetY=canvasOffset.top;
var centerX=canvas.width/2;
var centerY=canvas.height/2;
var radius=10;
// draw a center dot for user's reference
ctx.beginPath();
ctx.arc(centerX,centerY, radius, 0 , 2 * Math.PI, false);
ctx.fill();
function handleMouseMove(e){
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
$("#movelog").html("Mouse: "+ mouseX + " / " + mouseY);
$("#angle").html("Angle: "+parseInt(degreesFromTwelveOclock(centerX,centerY,mouseX,mouseY)));
}
function degreesFromTwelveOclock(cx,cy,mouseX,mouseY){
// calculate the angle(theta)
var theta=Math.atan2(mouseY-centerY,mouseX-centerX);
// be sure theta is positive
if(theta<0){theta += 2*Math.PI};
// convert to degrees and rotate so 0 degrees = 12 o'clock
var degrees=(theta*180/Math.PI+90)%360;
return(degrees);
}
$("#canvas").mousemove(function(e){handleMouseMove(e);});
}); // end $(function(){});
</script>
</head>
<body>
<p id="movelog">Move</p>
<p id="angle">Out</p>
<canvas id="canvas" width=300 height=300></canvas>
</body>
</html>

Resources