Problem reading rotation on A-Frame enviroment - aframe

this is my first post here:
I would like to know where users are looking at when using a-frame. I'm now using the rotation listener component to know that. Something like:
AFRAME.registerComponent('rotation-listener', {
tick() {
const newValue = this.el.getAttribute('rotation');
const stringCoords = AFRAME.utils.coordinates.stringify(newValue);
if (this.lastValue !== stringCoords) {
this.el.emit('rotationChanged', newValue);
this.lastValue = stringCoords;
}
},
});
camera[0].addEventListener('rotationChanged', e => {
console.log('Rotation: ', e.detail);
});
}
But I have one issue, I have modified on look-controls the yawobject.rotation.y and the pitchobject.rotation.x and apply a 0,25 factor to get a lower sensitivity and look around more slow.
The problem is that when I get the values of the rotation variable on console.log shows me the correct value but divided by 0,25. This is an issue because on Y axis there is a cap on 90 and -90 (you reach that cap before the true cap is), so i can't obtain all the values correctly.
How can I solve that? Creating a new variable rotationdat which makes the correct calculation, without the 0,25? And then reading that variable on the rotation-listener component? I have done this but I don't know if it is correct:
onMouseMove: function (evt) {
var direction;
var movementX;
var movementY;
var pitchObject = this.pitchObject;
var previousMouseEvent = this.previousMouseEvent;
var yawObject = this.yawObject;
var rotationdat = [0,0,0];
// Not dragging or not enabled.
if (!this.data.enabled || (!this.mouseDown && !this.pointerLocked)) { return; }
// Calculate delta.
if (this.pointerLocked) {
movementX = evt.movementX || evt.mozMovementX || 0;
movementY = evt.movementY || evt.mozMovementY || 0;
} else {
movementX = evt.screenX - previousMouseEvent.screenX;
movementY = evt.screenY - previousMouseEvent.screenY;
}
this.previousMouseEvent.screenX = evt.screenX;
this.previousMouseEvent.screenY = evt.screenY;
// Calculate rotation.
direction = this.data.reverseMouseDrag ? 1 : -1;
yawObject.rotation.y += movementX * 0.002 * this.data.mouseSpeedFactor * direction;
pitchObject.rotation.x += movementY * 0.002 * this.data.mouseSpeedFactor * direction;
pitchObject.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitchObject.rotation.x));
//new rotation calculation calculo de rotacion
yawObject.rotationdat.y += movementX * 0.002 * direction;
pitchObject.rotationdat.x += movementY * 0.002 * direction;
pitchObject.rotationdat.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitchObject.rotationdat.x));
rotationdat = (pitchObject.rotationdat.x, yawObject.rotationdat.y, 0);
},
Thank you for read me!

I think it's simpler to acess the camera in the underlying Threejs layer instead of decomposing the look-controls.
Any threejs object has a getWorldQuaternion() method, which you can use to get the current "global" rotatation:
<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
<script>
AFRAME.registerComponent('foo', {
init: function() {
this.text = document.querySelector("a-text"); // to display the angles
this.tmpQuaternion = new THREE.Quaternion(); // to keep the current rotation
this.tmpEuler = new THREE.Euler(); // we want euler angles, not quaternions
},
tick: function() {
const camera = this.el.sceneEl.camera; // get the camera reference
camera.getWorldQuaternion(this.tmpQuaternion); // get the 'global' rotation as a quaternion object
this.tmpEuler.setFromQuaternion(this.tmpQuaternion, "YXZ"); // convert it to euler angles
// get the yaw, pitch, and roll components, and display them as degrees
const string = [
THREE.MathUtils.radToDeg(this.tmpEuler.x).toFixed(2),
THREE.MathUtils.radToDeg(this.tmpEuler.y).toFixed(2),
THREE.MathUtils.radToDeg(this.tmpEuler.z).toFixed(2)
].join(" ");
this.text.setAttribute("value", string)
}
})
</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-text position="-0.5 1 -2.5" color="black"></a-text>
<a-sky color="#ECECEC"></a-sky>
</a-scene>

Related

Unable to access geometry array of points in pointcloud a-frame and threeJS

I have an a-frame entity with a component which is generating a pointcloud made of points in the init function but i am unable to access the points geometry
I had a demo working just using ThreeJS :
for ( let i = 0; i < mainContainer.children.length; i ++ ) {
const object = mainContainer.children[ i ];
if ( object instanceof THREE.Points ) {
I have set the pointCloud on entity
el.setObject3D('pointCloud', new THREE.Points( this.geometry, this.pointMaterial ));
i cannot access the geometry in the update function of the component, either using el.getObject3D('pointCloud') or using the id of the entity
As you set the object3D with setObject3D('pointCloud', new THREE.Points( ));, getObject3D('pointCloud') will retrieve the created THREE.Points instance.
The geometry is an attribute of the THREE.Points object:
<script src="https://aframe.io/releases/1.4.0/aframe.min.js"></script>
<script>
AFRAME.registerComponent("cloud", {
init: function() {
// create the points
const vertices = [];
for (let i = 0; i < 500; i++) {
const x = THREE.MathUtils.randFloatSpread(5);
const y = THREE.MathUtils.randFloatSpread(5);
const z = THREE.MathUtils.randFloatSpread(5);
vertices.push(x, y, z);
}
// create a geometry from the points
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
// small grey points material
const material = new THREE.PointsMaterial({ color: 0x888888, size: 0.05 });
// setObject3D sets the points under the name 'cloud'
this.el.setObject3D("cloud", new THREE.Points(geometry, material))
},
update: function() {
// retrieve the THREE.Points object
const points = this.el.getObject3D("cloud");
// get it's geometry attribute
const geometry = points.geometry;
// log it
console.log(geometry)
}
})
</script>
<a-scene>
<a-entity position="0 1 -4" cloud></a-entity>
</a-scene>

Draw a pointer on the edge of the screen that points to the target object behind tha back (invisible on the screen by the camera)

Live example: https://aframe-aim-component.glitch.me/
Try in Glitch: https://glitch.com/edit/#!/aframe-aim-component
We will see a blue line when we execute the sample code. It runs from a point 1 meter in front of the camera, to the position point of the red cube, which is behind and slightly away from the camera at the start. If we rotate the camera with the mouse to the left, we will turn to the red cube and the blue line will be drawn to the cube.
This blue bar is just an experiment to implement such an idea:
Draw an invisible line from a point in front of the camera, to the target object.
Find the coordinate on the device screen where the line intersects with the screen boundary.
Find the angle of intersection with the screen of this line.
Use HTML to display an arrow element on top of the canvas and rotate it in the direction of the invisible line.
Thanks to Piotr Adam Milewski for help in rendering the line (AFRAME: Coordinates of the fixed point in front of the camera)
As a result, the following behavior is expected: If the target object, with the "aim" component, is behind the player (the invisible line crosses the screen boundary), then an arrow is displayed on the screen boundary, indicating to the player in which direction he needs to turn to see the object.
I'm new to 3D and don't understand a lot of things yet. Please help me to implement a directional arrow pointing to the object.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
<script>
AFRAME.registerComponent("aim", {
init: function() {
this._matAimRay = new THREE.LineBasicMaterial( { color: 0x0000ff } );
this._frustum = new THREE.Frustum();
},
play: function() {
this._camera = this.el.sceneEl.camera;
},
pause: function() {
this._camera = null;
},
remove: function () {
this.el.sceneEl.removeChild(this._pointerEntity);
},
tick: function() {
if (!!this._camera) {
if (!this._checkIsAtScreen(this._camera, this.el.object3D.position)) {
if (!this._pointerEntity) {
//create entity for pointer line
let pointerEntity = document.createElement("a-entity");
this._pointerEntity = pointerEntity;
this.el.sceneEl.appendChild(pointerEntity);
}
let posViewer = new THREE.Vector3(0, 0, -100);
this._camera.localToWorld(posViewer);
const points = [];
points.push( posViewer, this.el.object3D.position );
const geometry = new THREE.BufferGeometry().setFromPoints( points );
const line = new THREE.Line( geometry, this._matAimRay );
this._pointerEntity.setObject3D("mesh", line );
} else if (!!this._pointerEntity) {
this.el.sceneEl.removeChild(this._pointerEntity);
this._pointerEntity = null;
}
}
},
_checkIsAtScreen(camera, pointVector3) {
camera.updateMatrix();
camera.updateMatrixWorld();
this._frustum.setFromProjectionMatrix(new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse));
return this._frustum.containsPoint(pointVector3);
}
});
</script>
<title>Document</title>
</head>
<body>
<a-scene>
<a-entity camera look-controls position="0 0 0"></a-entity>
<a-box aim position="-5 0 5" color="red"></a-box>
</a-scene>
</body>
</html>
UPDATE:
I updated the example code:
The beam to the target object is visible only when the object is not visible to the camera. The same way the pointer (2d compass) will be hidden from the user screen
Moved the point in front of the camera from 1 meter to 100 meters then, so that there is no effect when the beam is interrupted in the edge of the screen when the object is directly behind the camera. With more distance, the chance of this decreasing.
UPDATE 2 (working (bad) example)!!!
There is progress in my research. I don't fully understand how :) But it's starting to work. The problem is in the hautticity of determining the intersection of the imaginary line with furstum and in filtering the valid point, which makes the arrows move jerkily. If you set isValidScreenCoordinates == true, the arrows begin to move smoothly, but there are strange disappearances in the corners in Y...
Now there is something to build on, I will be very happy if you point out my mistakes in the study and contribute to it, to make the movement of arrows smoothly and accurately :))
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
.aim-arrow {
position: absolute;
z-index: 100000;
font-size: 50px;
opacity: 0;
visibility: hidden;
transition: opacity 0ms ease;
}
.aim-arrow.visible {
opacity: 1;
visibility: visible;
transition: opacity 1050ms ease;
}
</style>
<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
<script>
AFRAME.registerComponent("aim", {
schema: {
color: {type:'color', default: 'orange'}
},
init: function() {
this._frustum = new THREE.Frustum();
let pointerEl = document.createElement("div");
pointerEl.textContent = `◄`;
pointerEl.setAttribute("class", "aim-arrow");
this._pointerEl = pointerEl;
document.querySelector("body").appendChild(this._pointerEl);
},
update: function(oldData) {
this._pointerEl.style.color = this.data.color;
},
play: function() {
this._camera = this.el.sceneEl.camera;
},
pause: function() {
this._camera = null;
},
remove: function () {
this._pointerEl.parentElement.removeChild(this._pointerEl);
},
tick: function() {
if (!!this._camera) {
if (!this._checkIsAtScreen(this._camera, this.el.object3D.position)) {
this._containerWidth = window.innerWidth;
this._containerHeight = window.innerHeight;
let posViewer = new THREE.Vector3(0, 0, (this._camera.near + this._camera.far / 2) * -1);
this._camera.localToWorld(posViewer);
//find intersections
let line3 = new THREE.Line3();
line3.set(posViewer, this.el.object3D.position);
let intersectionPoint = new THREE.Vector3();
let isValidScreenCoordinates = false;
for (let i = 0; i < this._frustum.planes.length; i++) {
this._frustum.planes[i].intersectLine(line3, intersectionPoint);
if(!!intersectionPoint) {
let screenCoordinate = this._vector3ToScreenXY(this._camera, intersectionPoint, this._containerWidth, this._containerHeight);
if (!!screenCoordinate) {
isValidScreenCoordinates = (screenCoordinate.x >= 0) && (screenCoordinate.y >= 0) && (screenCoordinate.x <= this._containerWidth) && (screenCoordinate.y <= this._containerHeight);
if (isValidScreenCoordinates) {
this._placePointer(screenCoordinate, this._containerWidth, this._containerHeight);
//display pointer
this._pointerEl.classList.toggle("visible", true);
} else {
console.log('not valid', screenCoordinate);
}
}
}
}
} else {
//hide pointer
this._pointerEl.classList.toggle("visible", false);
}
}
},
_checkIsAtScreen(camera, pointVector3) {
this._frustum.setFromProjectionMatrix(new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse));
return this._frustum.containsPoint(pointVector3);
},
_vector3ToScreenXY(camera, vector3, containerWidth, containerHeight) {
let result = null;
let pos = vector3.clone();
let projScreenMat = new THREE.Matrix4();
projScreenMat.multiply( camera.projectionMatrix, camera.matrixWorldInverse );
projScreenMat.multiplyVector3( pos );
let x = ( pos.x + 1 ) * containerWidth / 2;
let y = ( - pos.y + 1) * containerHeight / 2;
if (!isNaN(x) && !isNaN(y)) {
result = { x, y };
}
return result;
},
_findAimPointerAngle(pointerXYCoordinate, containerWidth, containerHeight) {
let cx = pointerXYCoordinate.x;
let cy = pointerXYCoordinate.y;
let ex = containerWidth / 2;
let ey = containerHeight / 2;
let dy = ey - cy;
let dx = ex - cx;
let theta = Math.atan2(dy, dx); // range (-PI, PI]
theta *= 180 / Math.PI; // rads to degs, range (-180, 180]
if (theta < 0) theta = 360 + theta; // range [0, 360)
return theta;
},
_placePointer(screenCoordinate, containerWidth, containerHeight) {
let angle = this._findAimPointerAngle(screenCoordinate, containerWidth, containerHeight);
this._pointerEl.style.left = `${screenCoordinate.x}px`;
this._pointerEl.style.top = `${screenCoordinate.y}px`;
this._pointerEl.style.transformOrigin = "0% 0%";
this._pointerEl.style.transform = `rotate(${angle}deg) translate(0%, -50%)`;
}
});
</script>
<title>Document</title>
</head>
<body>
<a-scene>
<a-entity camera look-controls="wasd-controls: false; pointerLockEnabled: true" wasd-controls position="0 0 0"></a-entity>
<a-box aim="color: red" position="-5 0 5" color="red"></a-box>
<a-box aim="color: blue" position="5 0 5" color="blue"></a-box>
</a-scene>
</body>
</html>

How to close the path

I have a shape that is created using a 'for' loop. The first path has no anchors and therefore is pointy. I'm not sure if that is because it is not closed? Joined? Sketch
var circle= new Path.Circle({
radius: 100,
position: [200,200]
})
splat= new Path()
splat.fillColor= 'pink'
var count= 5
var length= circle.length
for(var i = 0; i <= count; i++){
var offset= i / count * length
const normal = i === 0 || i === count
? new Point(0, 0)
: circle.getNormalAt(offset) * (Math.random() * 50);
const point = circle.getPointAt(offset).add(i % 2 == 0 ? normal
: -normal);
console.log(point)
splat.add(point)
splat.smooth({ type: 'catmull-rom', factor: 0.5 });
}
splat.fullySelected= true
splat.closed= true
Thanks in advance again.
You just need to call the smooth() function once and you have to call it after setting the path to closed.
Then another thing that prevent the smoothing to work properly is that your first and last points of the path are the same, remove the last point and it will work as expected.
Sketch

How to listen to changes in the rotation of the camera in A-Frame?

Is there a way to add a listener on the current angle of view?
In other words, I'd like to trigger a function every time the user looks behind him.
The quickest way seems to be having a listener that checks the current head rotation and trust triggers the function if is within a certain range of degrees
Edit
The componentchange event is throttled. And it is more performant to not go through the event system for frequent updates. The camera rotation always changes every frame in VR so there is no need to think whether the camera has changed. So we read rotation every frame with component tick.
AFRAME.registerComponent('rotation-reader', {
tick: function () {
var rotation = this.el.getAttribute('rotation');
if (rotation.y < 180) {
// ...
}
}
});
// <a-camera rotation-reader>
Original
https://aframe.io/docs/0.2.0/core/entity.html#listening-for-component-changes
You can use the componentchanged event to listen to changes in the rotation:
document.querySelector('[camera]').addEventListener('componentchanged', function (evt) {
if (evt.name !== 'rotation') { return; }
if (evt.newData.y < 180) { // ... }
});
Or better as a component (this will trigger an event when rotation is certain amount):
AFRAME.registerComponent('trigger-on-look-behind', {
schema: {type: 'string'},
init: function () {
var eventName = this.data;
this.el.addEventListener('componentchanged', function (evt) {
if (evt.name !== 'rotation') { return; }
if (evt.newData.y < 180) {
this.emit(eventName);
}
});
}
});
And then:
<a-camera trigger-on-look-behind="looked-behind"></a-camera>

How to change material onclick using .obj and .mtl files in Aframe

i'm able to load .obj and .mtl files in aframe, However i wanted to change those materials of the object onclick by pulling out the material names from .mtl file, but how do i do it in aframe ?
<a-entity id="model" position="0 0 -2">
<a-entity obj-model = "obj: #tree-obj; mtl: #tree-mtl" position="0 0 0" rotation="0 0 0" scale="0.8 0.8 0.8"></a-entity>
</a-entity>
i could replicate the same in threejs as follows
object.traverse( function( child ) { if ( child instanceof THREE.Mesh ) {
if (child.material.name == "xyz") { //xyz from .mtl file
child.material = Black;// black is a meshphong material
child.castShadow = true;
child.receiveShadow = true;
}
}
You can write a component that listens to a click event and does the same three.js code:
AFRAME.registerComponent('change-material-on-click', {
schema: {
target: {type: 'selector'}
},
init: function () {
var el = this.el; // Element to add click listener.
var targetEl = this.data.target; // Target to change material.
el.addEventListener('click', function () {
var mesh = targetEl.getObject3D('mesh');
mesh.traverse(...); // Do your logic here.
});
}
});
Then attach the component:
<a-image change-material-on-click="target: #model"></a-image>
<a-obj-model id="model" ...></a-obj-model>

Resources