aframevr / aframe

:a: Web framework for building virtual reality experiences.
https://aframe.io/
MIT License
16.73k stars 3.99k forks source link

Change default desktop look controls to not be mouse drag #848

Closed ngokevin closed 8 years ago

ngokevin commented 8 years ago

Controls should be non-opinionated and should be free to swap out, but for an out-of-the-box experience, we should use some form of pointer lock rather than click/drag (on desktop, maybe VR mode only?).

cemkod commented 8 years ago

Here is my implementation of pointer locked look controls for reference: https://github.com/cemkod/aframe-fps-look-component

RangerMauve commented 8 years ago

Maybe it'd make sense to have it be configurable? Though I think that lock-by-default makes the most sense.

dmarcos commented 8 years ago

I don' think lock by default it's the more sensible since an aframe scene can share space with other content in the page

RangerMauve commented 8 years ago

I see your point, but from what I've seen in most examples and projects around, it's usually aframe taking over the page, and other content showing up seems to show up less frequently.

If it's configurable then it'd be easy enough to go one way or the other either way. Maybe since it's currently not lock-by-default it'd make sense to have the locking opt-in so as not to mess up existing projects.

dmarcos commented 8 years ago

I would prefer to have a separate set of controls to provide that functionality as @cemkod has done. theres's a lot of value on keeping components as simple as possible. There will be less bugs and make it easier for people to understand them and do derivative work. @RangerMove would it make sense to you to see look-controls (with no pointer lock) and a different set of fps-controls (with pointer lock)?

cemkod commented 8 years ago

@dmarcos: Please see: https://github.com/aframevr/aframe/issues/850 as it is somewhat relevant. Currently a component like the fps look control i made needs to duplicate hmd controls too, otherwise it camera doesn't work as expected on vr mode.

RangerMauve commented 8 years ago

@dmarcos @cemkod Yeah, I think that having a clear path to mixing different control schemes would help a lot. And I suppose that it would make sense to have the current default stay, but have it actually be composed of individual components to make customization easier.

ngokevin commented 8 years ago

Pointer lock only on focus or full screen would allow for multiple scenes. Separate components sound good. Until we resolve our component consumption story, might be worth pulling into standard components.

alexrkass commented 8 years ago

I haven't tested this but my no-click-look-controls component is only enabled on mouseover and the way it calculates rotation should support multiple scenes on a single page.

https://github.com/alexrkass/aframe-no-click-look-controls/blob/master/index.js#L145-L152

dmarcos commented 8 years ago

Thanks! @alexrkass I point to your examples so people can have a quick look:

https://alexrkass.github.io/no-click-example/

http://alexrkass.github.io/aframe-thetarestricted-example/

RangerMauve commented 8 years ago

@dmarcos Yeah, only problem with them is that it doesn't seem to be locking the pointer on the screen. D:

alexrkass commented 8 years ago

@RangerMauve true, sorry I conflated pointer lock with mousedrag.

pointer lock on focus would make a good default.

cvan commented 8 years ago

I definitely think we should support Pointer Lock. But I'm not sure if Pointer Lock should be the default. IMO the main reasons would be (1) Pointer Lock, though very useful, is not super common on the Web so it might be confusing for users, and (2) Firefox still prompts for permission first (Chrome doesn't, fwiw). I mentioned some pros and cons of using Pointer Lock in this issue: https://github.com/alexrkass/aframe-no-click-look-controls/issues/1#issue-126760588

ngokevin commented 8 years ago

Pointer lock upon entering fullscreen / VR would be the default. Mousedrag is a bit irritating for first-person scenes.

cvan commented 8 years ago

Pointer lock upon entering fullscreen / VR would be the default.

IMO, a gamepad is the ideal experience. Though I saw you closed #785 since we probably don't want to introduce gamepad support in the core aframe lib. I'd rather us have stellar out-of-the-box gamepad support for many controllers. And if we detect the presence of a gamepad we use that. Otherwise, we fall back to these mouse/WASD controls (or Pointer Lock, in the case of FPS).

Mousedrag is a bit irritating for first-person scenes.

Yeah, good point. I totally agree.

cvan commented 8 years ago

Also, FWIW, I filed this back in September as #471

cvan commented 8 years ago

See my good comment here

bnolan commented 8 years ago

I added quick pointerlock support, doesn't seem to break safari and works in chrome / ff.

ngokevin commented 8 years ago

https://github.com/aframevr/aframe/pull/1248#discussion_r60652822

We're going to publish pointer lock as a third-party controls. The direction of A-Frame is veering more towards serious VR rather than flat experiences. Mousedrag is the bare minimum default controls for non-VR users to at least look around. But if you want to design for flat experiences, the components/controls will be available

jremen commented 7 years ago

Any updates on pointer lock? I would really like to use it for my project but existing (two exactly) components doesn't work. aframe-no-click-look-controls is very nice as it allows to limit axis movement, but it doesn't work at all with aframe >=0.3.0. And also, it's big too.

So, what can I do at this point?

dmarcos commented 7 years ago

@ZhuJo I would open an issue on https://github.com/alexrkass/no-click-look-controls It's also not that big, just 262 lines.

dmarcos commented 7 years ago

I see that the dist is including A-Frame itself and it should not. Try to create your own controls by using only the component code: https://github.com/alexrkass/aframe-no-click-look-controls/blob/master/index.js On a quick glance I don't see any reasons why it should not work with A-Frame 0.7.1

jremen commented 7 years ago

Thanks, I was refering to older version aframe-no-click-look-controls. I tried to use a blob, registering a component. Console doesn't report any error, but it doesn't work. Please note I'm not using node.js, just straight call to javascript in HTML file.

This is a version I made from blob: /**/ (function(modules) { // webpackBootstrap /**/ // The module cache /**/ var installedModules = {};

/******/ // The require function
/******/
function __webpack_require__(moduleId) {

    /******/ // Check if module is in cache
    /******/
    if (installedModules[moduleId])
        /******/
        return installedModules[moduleId].exports;

    /******/ // Create a new module (and put it into the cache)
    /******/
    var module = installedModules[moduleId] = {
        /******/
        exports: {},
        /******/
        id: moduleId,
        /******/
        loaded: false
        /******/
    };

    /******/ // Execute the module function
    /******/
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    /******/ // Flag the module as loaded
    /******/
    module.loaded = true;

    /******/ // Return the exports of the module
    /******/
    return module.exports;
    /******/
}

/******/ // expose the modules object (__webpack_modules__)
/******/
__webpack_require__.m = modules;

/******/ // expose the module cache
/******/
__webpack_require__.c = installedModules;

/******/ // __webpack_public_path__
/******/
__webpack_require__.p = "";

/******/ // Load entry module and return exports
/******/
return __webpack_require__(0);
/******/

}) /****/ /**/ ([ / 0 / // // (function(module, exports) {

    if (typeof AFRAME === 'undefined') {
        throw new Error('Component attempted to register before AFRAME was available.');
    }
    // To avoid recalculation at every mouse movement tick
    var PI_2 = Math.PI / 2;

    AFRAME.registerComponent('no-click-look-controls', {
        dependencies: ['position', 'rotation'],
        schema: {
            enabled: { default: true },
            maxpitch: { default: PI_2 },
            maxyaw: { default: PI_2 * 6 },
        },

        /**
         * Called once when component is attached. Generally for initial setup.
         */
        init: function() {
            var scene = this.el.sceneEl;
            this.setupMouseControls();
            this.setupHMDControls();
            this.attachEventListeners();
            scene.addBehavior(this);
            this.previousPosition = new THREE.Vector3();
            this.deltaPosition = new THREE.Vector3();
        },

        setupMouseControls: function() {
            this.canvasEl = document.querySelector('a-scene').canvas;
            // The canvas where the scene is painted
            this.hovering = false;
            this.pitchObject = new THREE.Object3D();
            this.yawObject = new THREE.Object3D();
            this.yawObject.position.y = 10;
            this.yawObject.add(this.pitchObject);
        },

        setupHMDControls: function() {
            this.dolly = new THREE.Object3D();
            this.euler = new THREE.Euler();
            this.controls = new THREE.VRControls(this.dolly);
            this.zeroQuaternion = new THREE.Quaternion();
        },

        attachEventListeners: function() {
            var canvasEl = document.querySelector('a-scene').canvas;

            // Mouse Events
            canvasEl.addEventListener('mousemove', this.onMouseMove.bind(this), true);
            canvasEl.addEventListener('mouseout', this.onMouseOut.bind(this), true);
            canvasEl.addEventListener('mouseover', this.onMouseOver.bind(this), true);
            // Touch events
            canvasEl.addEventListener('touchstart', this.onTouchStart.bind(this));
            canvasEl.addEventListener('touchmove', this.onTouchMove.bind(this));
            canvasEl.addEventListener('touchend', this.onTouchEnd.bind(this));
        },

        update: function() {
            if (!this.data.enabled) { return; }
            this.controls.update();
            this.updateOrientation();
            this.updatePosition();
        },

        updateOrientation: (function() {
            var hmdEuler = new THREE.Euler();
            hmdEuler.order = 'YXZ';
            return function() {
                var pitchObject = this.pitchObject;
                var yawObject = this.yawObject;
                var hmdQuaternion = this.calculateHMDQuaternion();
                hmdEuler.setFromQuaternion(hmdQuaternion);
                this.el.setAttribute('rotation', {
                    x: THREE.Math.radToDeg(hmdEuler.x) + THREE.Math.radToDeg(pitchObject.rotation.x),
                    y: THREE.Math.radToDeg(hmdEuler.y) + THREE.Math.radToDeg(yawObject.rotation.y),
                    z: THREE.Math.radToDeg(hmdEuler.z)
                });
            };
        })(),

        calculateHMDQuaternion: (function() {
            var hmdQuaternion = new THREE.Quaternion();
            return function() {
                var dolly = this.dolly;
                if (!this.zeroed && !dolly.quaternion.equals(this.zeroQuaternion)) {
                    this.zeroOrientation();
                    this.zeroed = true;
                }
                hmdQuaternion.copy(this.zeroQuaternion).multiply(dolly.quaternion);
                return hmdQuaternion;
            };
        })(),

        updatePosition: (function() {
            var position = new THREE.Vector3();
            var quaternion = new THREE.Quaternion();
            var scale = new THREE.Vector3();
            return function() {
                var el = this.el;
                var deltaPosition = this.calculateDeltaPosition();
                var currentPosition = el.getComputedAttribute('position');
                this.el.object3D.matrixWorld.decompose(position, quaternion, scale);
                deltaPosition.applyQuaternion(quaternion);
                el.setAttribute('position', {
                    x: currentPosition.x + deltaPosition.x,
                    y: currentPosition.y + deltaPosition.y,
                    z: currentPosition.z + deltaPosition.z
                });
            };
        })(),

        calculateDeltaPosition: function() {
            var dolly = this.dolly;
            var deltaPosition = this.deltaPosition;
            var previousPosition = this.previousPosition;
            deltaPosition.copy(dolly.position);
            deltaPosition.sub(previousPosition);
            previousPosition.copy(dolly.position);
            return deltaPosition;
        },

        updateHMDQuaternion: (function() {
            var hmdQuaternion = new THREE.Quaternion();
            return function() {
                var dolly = this.dolly;
                this.controls.update();
                if (!this.zeroed && !dolly.quaternion.equals(this.zeroQuaternion)) {
                    this.zeroOrientation();
                    this.zeroed = true;
                }
                hmdQuaternion.copy(this.zeroQuaternion).multiply(dolly.quaternion);
                return hmdQuaternion;
            };
        })(),

        zeroOrientation: function() {
            var euler = new THREE.Euler();
            euler.setFromQuaternion(this.dolly.quaternion.clone().inverse());
            // Cancel out roll and pitch. We want to only reset yaw
            euler.z = 0;
            euler.x = 0;
            this.zeroQuaternion.setFromEuler(euler);
        },

        getMousePosition: function(event, canvasEl) {

            var rect = canvasEl.getBoundingClientRect();

            // Returns a value from -1 to 1 for X and Y representing the percentage of the max-yaw and max-pitch from the center of the canvas
            // -1 is far left or top, 1 is far right or bottom
            return { x: -2 * (.5 - (event.clientX - rect.left) / rect.width), y: -2 * (.5 - (event.clientY - rect.top) / rect.height) };
        },

        onMouseMove: function(event) {
            var pos = this.getMousePosition(event, this.canvasEl);
            var x = pos.x;
            var y = pos.y;

            var pitchObject = this.pitchObject;
            var yawObject = this.yawObject;

            if (!this.hovering || !this.data.enabled) { return; }
            yawObject.rotation.y = this.data.maxyaw * -x;
            pitchObject.rotation.x = this.data.maxpitch * -y;
        },

        onMouseOver: function(event) {
            this.hovering = true;
        },

        onMouseOut: function(event) {
            this.hovering = false;
        },

        onTouchStart: function(e) {
            if (e.touches.length !== 1) { return; }
            this.touchStart = {
                x: e.touches[0].pageX,
                y: e.touches[0].pageY
            };
            this.touchStarted = true;
        },

        onTouchMove: function(e) {
            var deltaY;
            var yawObject = this.yawObject;
            if (!this.touchStarted) { return; }
            deltaY = 2 * Math.PI * (e.touches[0].pageX - this.touchStart.x) / this.canvasEl.clientWidth;
            // Limits touch orientaion to to yaw (y axis)
            yawObject.rotation.y -= deltaY * 0.5;
            this.touchStart = {
                x: e.touches[0].pageX,
                y: e.touches[0].pageY
            };
        },

        onTouchEnd: function() {
            this.touchStarted = false;
        },
        /**
         * Called when a component is removed (e.g., via removeAttribute).
         * Generally undoes all modifications to the entity.
         */
        remove: function() {}
    });

    /***/
})
/******/

]);