Search code examples
three.jsaframe

Rotate webGL canvas to appear landscape-oriented on a portrait-oriented mobile phone


I’m using a-frame and trying to accomplish this task - force the canvas to be rendered as “landscape” when a mobile device is in portrait orientation (ie. device-width = 414px and device-height = 736px).

I have successfully accomplished this with the following steps

camera.aspect = 736 / 414; 
camera.updateProjectionMatrix();

renderer.setSize(736, 414);

In css...

.a-canvas {
  transform: rotate(90deg) translate(161px, 161px);
  height: 414px !important;
  width: 736px !important;
}

This all works great except for one major thing…I have 3D buttons in my scene and when I go to click them they don’t line up with the rotated canvas, instead their clickable position remains in the same place as before the canvas was rotated.

I’ve tried to set matrixWorldNeedsUpdate = true on the scene’s object3D along with updateWorldMatrix() with no luck. I tried calling refreshObjects on the raycaster with no luck. I tried rotating the scene and the camera with no luck.

I’m not sure what else to do. Any help would be greatly appreciated!

ANSWER:

Thanks to Marquizzo and gman for the help. Here's the updated a-frame source code (v1.0.4) to make the raycaster handle this forced landscape canvas properly

// line: 66884
onMouseMove: (function () {
  var direction = new THREE.Vector3();
  var mouse = new THREE.Vector2();
  var origin = new THREE.Vector3();
  var rayCasterConfig = {origin: origin, direction: direction};

  return function (evt) {
    var bounds = this.canvasBounds;
    var camera = this.el.sceneEl.camera;
    var left;
    var point;
    var top;

    camera.parent.updateMatrixWorld();

    // Calculate mouse position based on the canvas element
    if (evt.type === 'touchmove' || evt.type === 'touchstart') {
      // Track the first touch for simplicity.
      point = evt.touches.item(0);
    } else {
      point = evt;
    }

    left = point.clientX - bounds.left;
    top = point.clientY - bounds.top;
    // mouse.x = (left / bounds.width) * 2 - 1;
    // mouse.y = -(top / bounds.height) * 2 + 1;

    // HAYDEN's CODE: flipping x and y coordinates to force landscape
    // --------------------------------------------------------------
    let clickX = (left / bounds.width) * 2 - 1;
    let clickY = - (top / bounds.height) * 2 + 1;
    mouse.x = -clickY;
    mouse.y = clickX;
    // --------------------------------------------------------------


    origin.setFromMatrixPosition(camera.matrixWorld);
    direction.set(mouse.x, mouse.y, 0.5).unproject(camera).sub(origin).normalize();
    this.el.setAttribute('raycaster', rayCasterConfig);
    if (evt.type === 'touchmove') { evt.preventDefault(); }
  };
})(),

Solution

  • A-Frame uses a Raycaster internally to determine if the spot you clicked has hit an object. You can see in the Three.js documentation the raycaster needs the mouse x&y coordinates to determine where you clicked. Here's a working demo of that concept. However with your setup, x, y turns into -y, x.

    I think you'll have to write your own Raycaster function to trigger on the click event instead of relying on the built-in AFrame functionality, and then swap the x&y values:

    function onClick() {
        let clickX = ( event.clientX / window.innerWidth ) * 2 - 1;
        let clickY = - ( event.clientY / window.innerHeight ) * 2 + 1;
        mouse.x = -clickY;
        mouse.y = clickX;
    
        // Then continue with raycaster.setFromCamera(), etc...
    }
    
    window.addEventListener( 'click', onClick, false );