Implement Auto-turn and Redirected Walking

Initial implementation of Auto-Turn

Auto-turn leverages Space Turn automatically whenever you reach a configured distance from your chaperone boundary. Turn direction is in order of precedence:

1. Away from a corner where you're already touching one wall. This is to avoid being turned to face a wall, which we pretty much never want. If 'use corner angle' is enabled, it'll turn the angle of the corner, otherwise it'll turn the angle of your headset to the wall you encountered.
2. In whichever direction will start you unwinding your cord. If your rotations is greater than 'cord detangling rotations' and your angle to the wall is less than 'min detangling angle' it'll turn you such that the thing you're walking towards is directly along a wall, in the direction which will begin unwinding your cord.
3. In the closest angle to the wall. This too will put whatever you're walking towards directly along a wall, so you can walk the maximum distance to it. 
- If Smooth turn is checked, it'll turn at a constant (configurable) rate until the world is aligned with the new angle, otherwise it'll snap to the new direction instantly. 
- Activation distance is the distance from your chaperone at which auto-turn starts, and deactivation distance is the minimum distance (+ the activation distance) at which it'll be able to be activated again, to avoid turning twice at once.


Redirected walking tries to gently guide you away from your playspace boundaries by tweaking the world's angle at smaller increments than your brain is capable of noticing. While most playspaces won't be big enough to allow this to give you a true 'infinite walk' experience, it can give the perception of your playspace being ever so slightly bigger by making you walk in a slight arc in real life while walking forward in VR.
- Redirected walking is independent from Auto-turn and can be activated on its own. At both the 'slight' and 'imperceptable' radii, the turning speed is incredibly minimal. When walking along one wall of your chaperone to another, you should notice the far corner of your playspace (relative to the world) drifting ever so slightly away from you. In normal circumstances at the lowest settings this shouldn't be noticable. 


Co-authored-by: feilen <feilen1000@gmail.com>

* Initial implementation of Infinite Walk

* Code cleanup and abstraction, use nearest corner instead of nearest wall to determine turn direction

* Run clang-format

* Turn relative to the last wall if we run into adjacent walls, otherwise turn at easiest angle

* Limit maximum turns (just refuse to turn for now), only turn on shared corners if going the same direction

* Improve same-corner

* get rid of LastDirectionLeft

* WIP: cord untangling hint

* Try to automatically untangle cord, don't rotate if we encounter a wall behind us

* Incorrect offset for walls

* optimize if hmd inactive

* Cleanup/clarify angles

* (probably?) solve rare segfault

* Adjust the defaults and fix a bug with blocking reverse turns

* Implement smooth turn, fix a bug with shared-corners that would cause it to turn the wrong way

* actually really fix shared corner bug

vect[i + 1 % vect.size()] was somehow evaluating to vect[i + (1 %
vect.size())]

* Move configurables for autoturn to .h to make clearer what's configurable

* WIP: true redirected walking

* First pass optimization/cleanup

* Second pass cleanup/optimize

* Missing clang-format

* Somewhat ugly: Optionally turn the exact angle of the corner when using shared corner turns

* added rotation tab controller in progress

* cleaned up chaperone controller/ finished settings/rotationtab

* did first 4 settings qml, and main qml setup

* initial working build, not all settings UI done

* merging master into redirected walking. (#387)

* Update OVR to 1.10.30 (#379)

* updated to ovr 1.10.30

* fixed updated showoverlaykeyboard call

* fixed keyboard location

* Updated Manifest (#380)

* updated to ovr 1.10.30

* fixed updated showoverlaykeyboard call

* fixed keyboard location

* removed un-necessary matrix, updated app key and manifest

* fixed formatting

* Fix push to mute icon not being used when PTM enabled

* added touch default bindings (#385)

travis CI down/broken?

Co-authored-by: Vasily <vautkin@teknik.io>

* added action, and additional settings

* fixed various bugs added turn speed and mode

* fixed conversion error for turn speed

* added redirected walking options

* formatting

* formatting fixing

* removed old comments

* changed logs to debug and some wording changes

* changed UI layout

* added space turn to rotation page

* added icon, fixed autoturn to autoTurn, fixed conversion to be in qml vs set

* Update src/res/qml/RootPage.qml

yep XD

Co-Authored-By: Kung-vr <38249782+Kung-vr@users.noreply.github.com>

* reverted build changes

* removed un-needed app image build commands

* Cleanup some functions, avoid an extra chaperone check.

* Make redirected walking and autoturn independently enable-able

Co-authored-by: Feilen <feilen1000@gmail.com>
Co-authored-by: Vasily <vautkin@teknik.io>
Co-authored-by: Kung-vr <38249782+Kung-vr@users.noreply.github.com>
This commit is contained in:
ykeara
2020-04-08 18:57:53 -07:00
committed by GitHub
co-authored by Vasily Kung-vr Feilen
parent c0d44812d6
commit 1ae740463c
26 changed files with 1483 additions and 41 deletions
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill:#FFFFFF;}
</style>
<path class="st0" d="M0,0h24v24H0V0z"/>
<path class="st1" d="M15.6,5.6L11,1v3.1C7.1,4.6,4,7.9,4,12s3.1,7.4,7,7.9v-2C8.2,17.4,6,15,6,12s2.2-5.4,5-5.9V10L15.6,5.6z
M19.9,11c-0.2-1.4-0.7-2.7-1.6-3.9l-1.4,1.4c0.5,0.8,0.9,1.6,1,2.5H19.9z M13,17.9v2c1.4-0.2,2.7-0.7,3.9-1.6l-1.4-1.4
C14.7,17.4,13.9,17.8,13,17.9z M16.9,15.5l1.4,1.4c0.9-1.2,1.5-2.5,1.6-3.9h-2C17.8,13.9,17.4,14.7,16.9,15.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 827 B

+11
View File
@@ -73,6 +73,17 @@ MyStackViewPage {
mainView.push(motionPage)
}
}
MyPushButton {
id: rotationButton
iconPath: "qrc:/main_menu_icons/rotation_tab_icon"
activationSoundEnabled: false
text: " Rotation"
Layout.fillWidth: true
onClicked: {
MyResources.playFocusChangedSound()
mainView.push(rotationPage)
}
}
MyPushButton {
id: fixFloorButton
+6
View File
@@ -8,6 +8,7 @@ import "../motion_page"
import "../video_page"
import "../chaperone_page"
import "../steamvr_page"
import "../rotation_page"
Rectangle {
id: root
@@ -44,6 +45,11 @@ Rectangle {
visible: false
}
property RotationPage rotationPage: RotationPage {
stackView: mainView
visible: false
}
property FixFloorPage fixFloorPage: FixFloorPage {
stackView: mainView
visible: false
-5
View File
@@ -4,10 +4,8 @@ import QtQuick.Layouts 1.3
import ovras.advsettings 1.0
import "../common"
import "space_drag"
import "space_turn"
import "height_toggle"
import "gravity"
import "snap_turn"
MyStackViewPage {
headerText: "Motion Settings"
@@ -17,13 +15,10 @@ MyStackViewPage {
SpaceDragGroupBox { id: spaceDragGroupBox }
SpaceTurnGroupBox { id: spaceTurnGroupBox }
HeightToggleGroupBox { id: heightToggleGroupBox }
GravityGroupBox { id: gravityGroupBox }
SnapTurnGroupBox { id: snapTurnGroupBox }
Item {
Layout.fillHeight: true
@@ -0,0 +1,38 @@
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import ovras.advsettings 1.0
import "../common"
import "redirected"
import "space_turn"
import "snap_turn"
MyStackViewPage {
width: 1200
headerText: "Rotation Settings"
content: ColumnLayout {
spacing: 10
AutoTurnGroupBox{
id: autoTurnGroupBox
}
RedirectedGroupBox{
id: redirectedGroupBox
}
SpaceTurnGroupBox{
id:spaceTurnGroupBox
}
SnapTurnGroupBox{
id: snapTurnGroupBox
}
Item {
Layout.fillHeight: true
}
}
}
@@ -0,0 +1,336 @@
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import ovras.advsettings 1.0
import "../../common"
GroupBox {
id: autoTurnGroupBox
Layout.fillWidth: true
label: MyText {
leftPadding: 10
text: "Auto-Turn Settings"
bottomPadding: -10
}
background: Rectangle {
color: "transparent"
border.color: "#ffffff"
radius: 8
}
ColumnLayout {
anchors.fill: parent
Rectangle {
color: "#ffffff"
height: 1
Layout.fillWidth: true
Layout.bottomMargin: 5
}
RowLayout {
Layout.fillWidth: true
MyToggleButton {
id: autoTurn
text: "Toggle On/Off"
Layout.preferredWidth: 300
onCheckedChanged: {
RotationTabController.setAutoTurnEnabled(this.checked, true); }
}
MyText {
text: "Activation Dist:"
horizontalAlignment: Text.AlignRight
Layout.rightMargin: 10
Layout.preferredWidth: 215
}
MySlider {
id: activationSlider
from: 0
to: 1.0
stepSize: 0.05
value: .4
Layout.fillWidth: true
onPositionChanged: {
var val = this.value
activationValueText.text = val.toFixed(2)
}
onValueChanged: {
RotationTabController.setAutoTurnActivationDistance(value, true)
}
}
MyTextField {
id: activationValueText
text: "0.4"
keyBoardUID: 1001
Layout.preferredWidth: 100
Layout.leftMargin: 10
horizontalAlignment: Text.AlignHCenter
function onInputEvent(input) {
var val = parseFloat(input)
if (!isNaN(val)) {
if (val < 0) {
val = 0
} else if (val > 22) {
val = 22
}
activationSlider.value = v;
}
text = RotationTabController.autoTurnActivationDistance;
}
}
}
RowLayout {
Layout.fillWidth: true
MyToggleButton {
id: cornerAngle
text: "Use Corner Angle"
Layout.preferredWidth: 300
onCheckedChanged: {
RotationTabController.setAutoTurnUseCornerAngle(this.checked, true);
}
}
MyText {
text: "Deactivation Dist:"
horizontalAlignment: Text.AlignRight
Layout.rightMargin: 10
Layout.preferredWidth: 215
}
MySlider {
id: deactivationSlider
from: 0
to: 1.0
stepSize: 0.05
value: .15
Layout.fillWidth: true
onPositionChanged: {
var val = this.value
deactivationValueText.text = val.toFixed(2)
}
onValueChanged: {
RotationTabController.setAutoTurnDeactivationDistance(value, true)
}
}
MyTextField {
id: deactivationValueText
text: "0.15"
keyBoardUID: 1002
Layout.preferredWidth: 100
Layout.leftMargin: 10
horizontalAlignment: Text.AlignHCenter
function onInputEvent(input) {
var val = parseFloat(input)
if (!isNaN(val)) {
if (val < 0) {
val = 0
} else if (val > 22) {
val = 22
}
deactivationSlider.value = v;
}
text = RotationTabController.autoTurnDeactivationDistance;
}
}
}
RowLayout {
MyToggleButton {
id: autoTurnModeToggle
text: "Use Smooth Turn"
Layout.preferredWidth: 300
onCheckedChanged: {
if(this.checked){
RotationTabController.setAutoTurnMode(1, true);
}else{
RotationTabController.setAutoTurnMode(0, true);
}
}
}
MyText {
text: "Turn Speed (deg/sec):"
horizontalAlignment: Text.AlignRight
Layout.rightMargin: 10
}
MySlider {
id: speedSlider
from: 0
to: 360
stepSize: 1
value: 90
Layout.fillWidth: true
onPositionChanged: {
var val = this.value
speedValueText.text = val.toFixed()
}
onValueChanged: {
RotationTabController.setAutoTurnSpeed(parseInt(value*100), true)
}
}
MyTextField {
id: speedValueText
text: "90"
keyBoardUID: 1003
Layout.preferredWidth: 100
Layout.leftMargin: 10
horizontalAlignment: Text.AlignHCenter
function onInputEvent(input) {
var val = parseFloat(input)
if (!isNaN(val)) {
if (val < 1) {
val = 1
//caps user set Speed to 1000 deg/Second
//used as overflow prevention
//arbitrary
} else if (val > 1000) {
val = 1000
}
speedSlider.value = v;
}
//converts the centidegrees to degrees
text = (((RotationTabController.autoTurnSpeed)/100).toFixed());
}
}
}
RowLayout{
MyText{
text: "Detangle Angle: "
horizontalAlignment: Text.AlignLeft
Layout.preferredWidth: 250
Layout.rightMargin: 5
}
MyText{
text:"Min Rotations(deg):"
Layout.rightMargin: 5
}
MyTextField {
id: detangleAngleStartText
text: "360"
keyBoardUID: 1005
Layout.preferredWidth: 100
Layout.leftMargin: 5
horizontalAlignment: Text.AlignHCenter
function onInputEvent(input) {
var val = parseFloat(input)
if (!isNaN(val)) {
if (val < 0) {
val = 0
} else if (val > 3600) {
val = 3600
}
RotationTabController.setMinCordTangle((val*(Math.PI/180)), true);
}
}
}
Item {
Layout.preferredWidth: 15
}
MyText{
text: "Max Wall Angle(deg): "
horizontalAlignment: Text.AlignRight
Layout.rightMargin: 10
Layout.fillWidth: true
}
MyTextField {
id: detangleAngleAssistText
text: "10"
keyBoardUID: 1006
Layout.preferredWidth: 100
Layout.leftMargin: 10
horizontalAlignment: Text.AlignHCenter
function onInputEvent(input) {
var val = parseFloat(input)
if (!isNaN(val)) {
if (val < 0) {
val = 0
} else if (val > 3600) {
val = 3600
}
RotationTabController.setCordDetangleAngle((val*(Math.PI/180)), true);
}
}
}
}
}
Component.onCompleted: {
activationSlider.value = RotationTabController.autoTurnActivationDistance
autoTurn.checked = RotationTabController.autoTurnEnabled
deactivationSlider.value = RotationTabController.autoTurnDeactivationDistance
cornerAngle.checked = RotationTabController.autoTurnUseCornerAngle
speedSlider.value = (RotationTabController.autoTurnSpeed/100).toFixed()
if(RotationTabController.autoTurnMode === 1){
autoTurnModeToggle.checked = true;
}else{
autoTurnModeToggle.checked = false;
}
detangleAngleStartText.text = parseInt(RotationTabController.minCordTangle*(180/Math.PI))
detangleAngleAssistText.text = parseInt(RotationTabController.cordDetangleAngle*(180/Math.PI))
}
Connections {
target: RotationTabController
onAutoTurnEnabledChanged: {
autoTurn.checked = RotationTabController.autoTurnEnabled
}
onAutoTurnActivationDistanceChanged: {
activationSlider.value = RotationTabController.autoTurnActivationDistance
}
onAutoTurnDeactivationDistanceChanged:{
deactivationSlider.value = RotationTabController.autoTurnDeactivationDistance
}
onAutoTurnUseCornerAngleChanged:{
cornerAngle.checked = RotationTabController.autoTurnUseCornerAngle
}
onAutoTurnSpeedChanged:{
var val = RotationTabController.autoTurnSpeed
speedSlider.value = ((val/100).toFixed())
}
onAutoTurnModeChanged:{
if(RotationTabController.autoTurnMode === 1){
autoTurnModeToggle.checked = true;
}else{
autoTurnModeToggle.checked = false;
}
}
onMinCordTangleChanged:{
detangleAngleStartText.text = parseInt(RotationTabController.minCordTangle*(180/Math.PI))
}
onCordDetangleAngleChanged:{
detangleAngleAssistText.text = parseInt(RotationTabController.cordDetangleAngle*(180/Math.PI))
}
}
}
@@ -0,0 +1,125 @@
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import ovras.advsettings 1.0
import "../../common"
GroupBox {
id: redirectedGroupBox
Layout.fillWidth: true
label: MyText {
leftPadding: 10
text: "Redirected Walking"
bottomPadding: -10
}
background: Rectangle {
color: "transparent"
border.color: "#ffffff"
radius: 8
}
ColumnLayout {
anchors.fill: parent
Rectangle {
color: "#ffffff"
height: 1
Layout.fillWidth: true
Layout.bottomMargin: 5
}
RowLayout{
MyToggleButton {
id: redirectedModeToggle
text: "On"
Layout.preferredWidth: 225
onCheckedChanged: {
RotationTabController.setVestibularMotionEnabled(this.checked, true);
}
}
Item{
Layout.fillWidth: true
}
MyPushButton {
id: imperceptableButton
Layout.preferredWidth: 180
text:"Imperceptable"
onClicked: {
RotationTabController.setVestibularMotionRadius(22.0)
}
}
MyPushButton {
id: lightButton
Layout.preferredWidth: 125
text:"Light"
onClicked: {
RotationTabController.setVestibularMotionRadius(11.0)
}
}
MyPushButton {
id: strongButton
Layout.preferredWidth: 125
text:"Strong"
onClicked: {
RotationTabController.setVestibularMotionRadius(5.0)
}
}
MyPushButton {
id: extremeButton
Layout.preferredWidth: 125
text:"Extreme"
onClicked: {
RotationTabController.setVestibularMotionRadius(1.0)
}
}
MyText{
text: "Radius: "
horizontalAlignment: Text.AlignRight
Layout.rightMargin: 4
}
MyTextField {
id: redirectedWalkingRadiusText
text: "11.0"
keyBoardUID: 1004
Layout.preferredWidth: 100
Layout.leftMargin: 10
horizontalAlignment: Text.AlignHCenter
function onInputEvent(input) {
var val = parseFloat(input)
if (!isNaN(val)) {
if (val < 0.5) {
val = 0.5
} else if (val > 50) {
val = 50
}
RotationTabController.setVestibularMotionRadius(val, true);
}
}
}
}
}
Component.onCompleted: {
redirectedWalkingRadiusText.text = ((RotationTabController.vestibularMotionRadius).toFixed(2))
redirectedModeToggle.checked = RotationTabController.vestibularMotionEnabled
}
Connections {
target: RotationTabController
onVestibularMotionEnabledChanged:{
redirectedModeToggle.checked = RotationTabController.vestibularMotionEnabled
}
onVestibularMotionRadiusChanged:{
redirectedWalkingRadiusText.text = ((RotationTabController.vestibularMotionRadius).toFixed(2))
}
}
}
+1
View File
@@ -29,5 +29,6 @@
<file alias="steamvr_tab_icon">img/main_menu_icons/steamvr_tab_icon.svg</file>
<file alias="utilities_tab_icon">img/main_menu_icons/utilities_tab_icon.svg</file>
<file alias="video_tab_icon">img/main_menu_icons/video_tab_icon.svg</file>
<file alias="rotation_tab_icon">img/main_menu_icons/rotation_tab_icon.svg</file>
</qresource>
</RCC>