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>
In Aframe, is there a way to use the controller to rotate a distant component with the raycaster ?
Yes. cursor entity, with raycaster, and class='clickable' like this
<a-entity id="mouseCursor" cursor="rayOrigin: mouse" raycaster="objects:
.clickable"></a-entity>
then add a custom component on the object you want to spin, that listens for the mouse event, and then spins the object, until the mouse Leave event occurs. like this
AFRAME.registerComponent('over-listener', {
schema:{
mouseover:{type: 'boolean', default: false}
},
init: function () {
var el = this.el; // reference to the entity that contains this component
var data = this.data; // reference to the properties of this component.
// Listen for mouseenter event
this.el.addEventListener('mouseenter', function (evt) {
// You can't change the property directly. You must use setAttribute.
el.setAttribute('over-listener','mouseover', true);
// Change the color of the button to indicate rollover state is on.
el.setAttribute('material','color','#55ee00');;
});
// Listen for mouseleave event
this.el.addEventListener('mouseleave', function (evt) {
el.setAttribute('over-listener','mouseover', false);
el.setAttribute('material','color','orange');
});
},
tick: function(){ // called every frame
if(this.data.mouseover){ // Check the mouseover state
let elOcta = document.querySelector('#octahedron');
let rot = elOcta.getAttribute('rotation');
elOcta.setAttribute('rotation',{x: rot.x, y: rot.y , z: rot.z + 1});
}
}
});
here is a glitch
https://glitch.com/~rollover-rotate
I want to make a dynamic digital clock in A-Frame. I'm using a text element but I can't change its text by setting the property in JS. I can still change others attributes like the color.
html:
...
<a-text id="clock" clock-text value="00:00" position="2.45 0 0.01" color="#FFFFFF" align="right"></a-text>
...
js:
...
AFRAME.registerComponent('clock-text', {
init: function() {
var el = this.el;
el.setAttribute('value', '20:30');
el.setAttribute('color', 'black');
},
update: function() {
el.setAttribute('value', '20:30');
}
});
You can get the full code in the jsfiddle I'm using.
So I think this is a race condition issue, since there's actually two updates going on in a-text, when it initializes and when it receives the new value (causing a component update to be called).
the text component does emit one event called 'textfontset' when updateFont gets called from its init.
Using that event, you can start your clock after that event is emitted
AFRAME.registerComponent('clock-text', {
init: function() {
var el = this.el;
this.ready = false;
el.addEventListener('textfontset', function() {
this.ready = true;
}.bind(this));
},
tick: function() {
var el = this.el;
if (!this.ready) {
return;
}
el.setAttribute('value', '20:30');
}
});
https://jsfiddle.net/xcofjjm9/1/
I am trying to write a clock or timer.
function ttimer(){
var options = {
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
};
var el_ttimer=document.querySelector('#a-ttimer');
var d = new Date();
el_ttimer.setAttribute('text', 'text: '+d.toLocaleString("ru", options)+'; font: Droid Sans Mono; size: .77;');
}
setInterval(ttimer, 1000);
<a-entity a-ttimer position="-12 0 -5" id="a-ttimer"></a-entity>
How update text smoothly? Timer for example.
How create new primitives in scene smoothly?
Have you best practice for it?
You can write a component that has the tick handler, which will hook into the scene's render loop. https://aframe.io/docs/0.3.0/core/component.html#tick-time-timedelta
AFRAME.registerComponent('timertext', {
tick: function (time, timeDelta) {
// ...
el_ttimer.setAttribute('text', 'text: '+ d.toLocaleString("ru", options) + '; font: Droid Sans Mono; size: .77;');
}
});
<a-entity timertext text></a-entity>
If you want to create a new primitive, use AFRAME.registerPrimitive. https://aframe.io/docs/0.3.0/primitives/#register-a-primitive
AFRAME.registerPrimitive('a-ttimer', {
defaultComponents: {
timertext: {},
text: {}
},
mappings: {
text: 'text.text'
}
});
<a-ttimer text="0"></a-ttimer>
the recent v0.3.0 blog post mentions WebVR 1.0 support allowing "us to have different content on the desktop display than the headset, opening the door for asynchronous gameplay and spectator modes." This is precisely what I'm trying to get working. I'm looking to have one camera in the scene represent the viewpoint of the HMD and a secondary camera represent a spectator of the same scene and render that view to a canvas on the same webpage. 0.3.0 removes the ability to render a-scene to a specific canvas in favor of embedded component. Any thoughts on how to accomplish two cameras rendering a single scene simultaneously?
My intention is to have a the desktop display show what a user is doing from a different perspective. My end goal is to be able to build a mixed reality green screen component.
While there may be a better or cleaner way to do this in the future, I was able to get a second camera rendering by looking at examples of how this is done in the THREE.js world.
I add a component to a non-active camera called spectator. in the init function I set up a new renderer and attach to div outside the scene to create a new canvas. I then call the render method inside the tick() part of the lifecycle.
I have not worked out how to isolate the movement of this camera yet. The default look controls of the 0.3.0 aframe scene still control both camera
Source code:
https://gist.github.com/derickson/334a48eb1f53f6891c59a2c137c180fa
I've created a set of components that can help with this. https://github.com/diarmidmackenzie/aframe-multi-camera
Here's an example showing usage with A-Frame 1.2.0 to display the main camera on the left half of the screen, and a secondary camera on the right half.
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/diarmidmackenzie/aframe-multi-camera#latest/src/multi-camera.min.js"></script>
</head>
<body>
<div>
<a-scene>
<a-entity camera look-controls wasd-controls position="0 1.6 0">
<!-- first secondary camera is a child of the main camera, so that it always has the same position / rotation -->
<!-- replace main camera (since main camera is rendered across the whole screen, which we don't want) -->
<a-entity
id="camera1"
secondary-camera="outputElement:#viewport1;sequence: replace"
>
</a-entity>
</a-entity>
<!-- PUT YOUR SCENE CONTENT HERE-->
<!-- position of 2nd secondary camera-->
<a-entity
id="camera2"
secondary-camera="outputElement:#viewport2"
position="8 1.6 -6"
rotation="0 90 0"
>
</a-entity>
</a-scene>
</div>
<!-- standard HTML to contrl layout of the two viewports-->
<div style="width: 100%; height:100%; display: flex">
<div id="viewport1" style="width: 50%; height:100%"></div>
<div id="viewport2" style="width: 50%; height:100%"></div>
</div>
</body>
</html>
Also here as a glitch: https://glitch.com/edit/#!/recondite-polar-hyssop
It's also been suggested that I post the entire source code for the multi-camera component here.
Here it is...
/* System that supports capture of the the main A-Frame render() call
by add-render-call */
AFRAME.registerSystem('add-render-call', {
init() {
this.render = this.render.bind(this);
this.originalRender = this.el.sceneEl.renderer.render;
this.el.sceneEl.renderer.render = this.render;
this.el.sceneEl.renderer.autoClear = false;
this.preRenderCalls = [];
this.postRenderCalls = [];
this.suppresssDefaultRenderCount = 0;
},
addPreRenderCall(render) {
this.preRenderCalls.push(render)
},
removePreRenderCall(render) {
const index = this.preRenderCalls.indexOf(render);
if (index > -1) {
this.preRenderCalls.splice(index, 1);
}
},
addPostRenderCall(render) {
this.postRenderCalls.push(render)
},
removePostRenderCall(render) {
const index = this.postRenderCalls.indexOf(render);
if (index > -1) {
this.postRenderCalls.splice(index, 1);
}
else {
console.warn("Unexpected failure to remove render call")
}
},
suppressOriginalRender() {
this.suppresssDefaultRenderCount++;
},
unsuppressOriginalRender() {
this.suppresssDefaultRenderCount--;
if (this.suppresssDefaultRenderCount < 0) {
console.warn("Unexpected unsuppression of original render")
this.suppresssDefaultRenderCount = 0;
}
},
render(scene, camera) {
renderer = this.el.sceneEl.renderer
// set up THREE.js stats to correctly count across all render calls.
renderer.info.autoReset = false;
renderer.info.reset();
this.preRenderCalls.forEach((f) => f());
if (this.suppresssDefaultRenderCount <= 0) {
this.originalRender.call(renderer, scene, camera)
}
this.postRenderCalls.forEach((f) => f());
}
});
/* Component that captures the main A-Frame render() call
and adds an additional render call.
Must specify an entity and component that expose a function call render(). */
AFRAME.registerComponent('add-render-call', {
multiple: true,
schema: {
entity: {type: 'selector'},
componentName: {type: 'string'},
sequence: {type: 'string', oneOf: ['before', 'after', 'replace'], default: 'after'}
},
init() {
this.invokeRender = this.invokeRender.bind(this);
},
update(oldData) {
// first clean up any old settings.
this.removeSettings(oldData)
// now add new settings.
if (this.data.sequence === "before") {
this.system.addPreRenderCall(this.invokeRender)
}
if (this.data.sequence === "replace") {
this.system.suppressOriginalRender()
}
if (this.data.sequence === "after" ||
this.data.sequence === "replace")
{
this.system.addPostRenderCall(this.invokeRender)
}
},
remove() {
this.removeSettings(this.data)
},
removeSettings(data) {
if (data.sequence === "before") {
this.system.removePreRenderCall(this.invokeRender)
}
if (data.sequence === "replace") {
this.system.unsuppressOriginalRender()
}
if (data.sequence === "after" ||
data.sequence === "replace")
{
this.system.removePostRenderCall(this.invokeRender)
}
},
invokeRender()
{
const componentName = this.data.componentName;
if ((this.data.entity) &&
(this.data.entity.components[componentName])) {
this.data.entity.components[componentName].render(this.el.sceneEl.renderer, this.system.originalRender);
}
}
});
/* Component to set layers via HTML attribute. */
AFRAME.registerComponent('layers', {
schema : {type: 'number', default: 0},
init: function() {
setObjectLayer = function(object, layer) {
if (!object.el ||
!object.el.hasAttribute('keep-default-layer')) {
object.layers.set(layer);
}
object.children.forEach(o => setObjectLayer(o, layer));
}
this.el.addEventListener("loaded", () => {
setObjectLayer(this.el.object3D, this.data);
});
if (this.el.hasAttribute('text')) {
this.el.addEventListener("textfontset", () => {
setObjectLayer(this.el.object3D, this.data);
});
}
}
});
/* This component has code in common with viewpoint-selector-renderer
However it's a completely generic stripped-down version, which
just delivers the 2nd camera function.
i.e. it is missing:
- The positioning of the viewpoint-selector entity.
- The cursor / raycaster elements.
*/
AFRAME.registerComponent('secondary-camera', {
schema: {
output: {type: 'string', oneOf: ['screen', 'plane'], default: 'screen'},
outputElement: {type: 'selector'},
cameraType: {type: 'string', oneOf: ['perspective, orthographic'], default: 'perspective'},
sequence: {type: 'string', oneOf: ['before', 'after', 'replace'], default: 'after'},
quality: {type: 'string', oneOf: ['high, low'], default: 'high'}
},
init() {
if (!this.el.id) {
console.error("No id specified on entity. secondary-camera only works on entities with an id")
}
this.savedViewport = new THREE.Vector4();
this.sceneInfo = this.prepareScene();
this.activeRenderTarget = 0;
// add the render call to the scene
this.el.sceneEl.setAttribute(`add-render-call__${this.el.id}`,
{entity: `#${this.el.id}`,
componentName: "secondary-camera",
sequence: this.data.sequence});
// if there is a cursor on this entity, set it up to read this camera.
if (this.el.hasAttribute('cursor')) {
this.el.setAttribute("cursor", "canvas: user; camera: user");
this.el.addEventListener('loaded', () => {
this.el.components['raycaster'].raycaster.layers.mask = this.el.object3D.layers.mask;
const cursor = this.el.components['cursor'];
cursor.removeEventListeners();
cursor.camera = this.camera;
cursor.canvas = this.data.outputElement;
cursor.canvasBounds = cursor.canvas.getBoundingClientRect();
cursor.addEventListeners();
cursor.updateMouseEventListeners();
});
}
if (this.data.output === 'plane') {
if (!this.data.outputElement.hasLoaded) {
this.data.outputElement.addEventListener("loaded", () => {
this.configureCameraToPlane()
});
} else {
this.configureCameraToPlane()
}
}
},
configureCameraToPlane() {
const object = this.data.outputElement.getObject3D('mesh');
function nearestPowerOf2(n) {
return 1 << 31 - Math.clz32(n);
}
// 2 * nearest power of 2 gives a nice look, but at a perf cost.
const factor = (this.data.quality === 'high') ? 2 : 1;
const width = factor * nearestPowerOf2(window.innerWidth * window.devicePixelRatio);
const height = factor * nearestPowerOf2(window.innerHeight * window.devicePixelRatio);
function newRenderTarget() {
const target = new THREE.WebGLRenderTarget(width,
height,
{
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
stencilBuffer: false,
generateMipmaps: false
});
return target;
}
// We use 2 render targets, and alternate each frame, so that we are
// never rendering to a target that is actually in front of the camera.
this.renderTargets = [newRenderTarget(),
newRenderTarget()]
this.camera.aspect = object.geometry.parameters.width /
object.geometry.parameters.height;
},
remove() {
this.el.sceneEl.removeAttribute(`add-render-call__${this.el.id}`);
if (this.renderTargets) {
this.renderTargets[0].dispose();
this.renderTargets[1].dispose();
}
// "Remove" code does not tidy up adjustments made to cursor component.
// rarely necessary as cursor is typically put in place at the same time
// as the secondary camera, and so will be disposed of at the same time.
},
prepareScene() {
this.scene = this.el.sceneEl.object3D;
const width = 2;
const height = 2;
if (this.data.cameraType === "orthographic") {
this.camera = new THREE.OrthographicCamera( width / - 2, width / 2, height / 2, height / - 2, 1, 1000 );
}
else {
this.camera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000);
}
this.scene.add(this.camera);
return;
},
render(renderer, renderFunction) {
// don't bother rendering to screen in VR mode.
if (this.data.output === "screen" && this.el.sceneEl.is('vr-mode')) return;
var elemRect;
if (this.data.output === "screen") {
const elem = this.data.outputElement;
// get the viewport relative position of this element
elemRect = elem.getBoundingClientRect();
this.camera.aspect = elemRect.width / elemRect.height;
}
// Camera position & layers match this entity.
this.el.object3D.getWorldPosition(this.camera.position);
this.el.object3D.getWorldQuaternion(this.camera.quaternion);
this.camera.layers.mask = this.el.object3D.layers.mask;
this.camera.updateProjectionMatrix();
if (this.data.output === "screen") {
// "bottom" position is relative to the whole viewport, not just the canvas.
// We need to turn this into a distance from the bottom of the canvas.
// We need to consider the header bar above the canvas, and the size of the canvas.
const mainRect = renderer.domElement.getBoundingClientRect();
renderer.getViewport(this.savedViewport);
renderer.setViewport(elemRect.left - mainRect.left,
mainRect.bottom - elemRect.bottom,
elemRect.width,
elemRect.height);
renderFunction.call(renderer, this.scene, this.camera);
renderer.setViewport(this.savedViewport);
}
else {
// target === "plane"
// store off current renderer properties so that they can be restored.
const currentRenderTarget = renderer.getRenderTarget();
const currentXrEnabled = renderer.xr.enabled;
const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
// temporarily override renderer proeperties for rendering to a texture.
renderer.xr.enabled = false; // Avoid camera modification
renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows
const renderTarget = this.renderTargets[this.activeRenderTarget];
renderTarget.texture.encoding = renderer.outputEncoding;
renderer.setRenderTarget(renderTarget);
renderer.state.buffers.depth.setMask( true ); // make sure the depth buffer is writable so it can be properly cleared, see #18897
renderer.clear();
renderFunction.call(renderer, this.scene, this.camera);
this.data.outputElement.getObject3D('mesh').material.map = renderTarget.texture;
// restore original renderer settings.
renderer.setRenderTarget(currentRenderTarget);
renderer.xr.enabled = currentXrEnabled;
renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
this.activeRenderTarget = 1 - this.activeRenderTarget;
}
}
});