mirror of
https://thingvellir.net/git/overte
synced 2025-03-27 23:52:03 +01:00
Merge pull request #6689 from ZappoMan/rightClickMenu
Some early work to support hand driven reticle
This commit is contained in:
commit
46a4a469e9
11 changed files with 410 additions and 9 deletions
87
examples/controllers/philipsVersion.js
Normal file
87
examples/controllers/philipsVersion.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
//
|
||||
// reticleTest.js
|
||||
// examples/controllers
|
||||
//
|
||||
// Created by Brad Hefta-Gaub on 2015/12/15
|
||||
// Copyright 2015 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
function length(posA, posB) {
|
||||
var dx = posA.x - posB.x;
|
||||
var dy = posA.y - posB.y;
|
||||
var length = Math.sqrt((dx*dx) + (dy*dy))
|
||||
return length;
|
||||
}
|
||||
|
||||
var PITCH_DEADZONE = 1.0;
|
||||
var PITCH_MAX = 20.0;
|
||||
var YAW_DEADZONE = 1.0;
|
||||
var YAW_MAX = 20.0;
|
||||
var PITCH_SCALING = 10.0;
|
||||
var YAW_SCALING = 10.0;
|
||||
|
||||
var EXPECTED_CHANGE = 50;
|
||||
var lastPos = Controller.getReticlePosition();
|
||||
function moveReticle(dY, dX) {
|
||||
var globalPos = Controller.getReticlePosition();
|
||||
|
||||
// some debugging to see if position is jumping around on us...
|
||||
var distanceSinceLastMove = length(lastPos, globalPos);
|
||||
if (distanceSinceLastMove > EXPECTED_CHANGE) {
|
||||
print("distanceSinceLastMove:" + distanceSinceLastMove + "----------------------------");
|
||||
}
|
||||
|
||||
if (Math.abs(dX) > EXPECTED_CHANGE) {
|
||||
print("UNEXPECTED dX:" + dX + "----------------------------");
|
||||
dX = 0;
|
||||
}
|
||||
if (Math.abs(dY) > EXPECTED_CHANGE) {
|
||||
print("UNEXPECTED dY:" + dY + "----------------------------");
|
||||
dY = 0;
|
||||
}
|
||||
|
||||
globalPos.x += dX;
|
||||
globalPos.y += dY;
|
||||
Controller.setReticlePosition(globalPos);
|
||||
lastPos = globalPos;
|
||||
}
|
||||
|
||||
|
||||
var MAPPING_NAME = "com.highfidelity.testing.reticleWithHand";
|
||||
var mapping = Controller.newMapping(MAPPING_NAME);
|
||||
|
||||
var lastHandPitch = 0;
|
||||
var lastHandYaw = 0;
|
||||
|
||||
mapping.from(Controller.Standard.LeftHand).peek().to(function(pose) {
|
||||
var handEulers = Quat.safeEulerAngles(pose.rotation);
|
||||
//Vec3.print("handEulers:", handEulers);
|
||||
|
||||
var handPitch = handEulers.y;
|
||||
var handYaw = handEulers.x;
|
||||
var changePitch = (handPitch - lastHandPitch) * PITCH_SCALING;
|
||||
var changeYaw = (handYaw - lastHandYaw) * YAW_SCALING;
|
||||
if (Math.abs(changePitch) > PITCH_MAX) {
|
||||
print("Pitch: " + changePitch);
|
||||
changePitch = 0;
|
||||
}
|
||||
if (Math.abs(changeYaw) > YAW_MAX) {
|
||||
print("Yaw: " + changeYaw);
|
||||
changeYaw = 0;
|
||||
}
|
||||
changePitch = Math.abs(changePitch) < PITCH_DEADZONE ? 0 : changePitch;
|
||||
changeYaw = Math.abs(changeYaw) < YAW_DEADZONE ? 0 : changeYaw;
|
||||
moveReticle(changePitch, changeYaw);
|
||||
lastHandPitch = handPitch;
|
||||
lastHandYaw = handYaw;
|
||||
|
||||
});
|
||||
mapping.enable();
|
||||
|
||||
|
||||
Script.scriptEnding.connect(function(){
|
||||
mapping.disable();
|
||||
});
|
76
examples/controllers/proceduralHandPoseExample.js
Normal file
76
examples/controllers/proceduralHandPoseExample.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// proceduralHandPoseExample.js
|
||||
// examples/controllers
|
||||
//
|
||||
// Created by Brad Hefta-Gaub on 2015/12/15
|
||||
// Copyright 2015 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
|
||||
var MAPPING_NAME = "com.highfidelity.examples.proceduralHandPose";
|
||||
var mapping = Controller.newMapping(MAPPING_NAME);
|
||||
var translation = { x: 0, y: 0.1, z: 0 };
|
||||
var translationDx = 0.01;
|
||||
var translationDy = 0.01;
|
||||
var translationDz = -0.01;
|
||||
var TRANSLATION_LIMIT = 0.5;
|
||||
|
||||
var pitch = 45;
|
||||
var yaw = 0;
|
||||
var roll = 45;
|
||||
var pitchDelta = 1;
|
||||
var yawDelta = -1;
|
||||
var rollDelta = 1;
|
||||
var ROTATION_MIN = -90;
|
||||
var ROTATION_MAX = 90;
|
||||
|
||||
mapping.from(function() {
|
||||
|
||||
// adjust the hand translation in a periodic back and forth motion for each of the 3 axes
|
||||
translation.x = translation.x + translationDx;
|
||||
translation.y = translation.y + translationDy;
|
||||
translation.z = translation.z + translationDz;
|
||||
if ((translation.x > TRANSLATION_LIMIT) || (translation.x < (-1 * TRANSLATION_LIMIT))) {
|
||||
translationDx = translationDx * -1;
|
||||
}
|
||||
if ((translation.y > TRANSLATION_LIMIT) || (translation.y < (-1 * TRANSLATION_LIMIT))) {
|
||||
translationDy = translationDy * -1;
|
||||
}
|
||||
if ((translation.z > TRANSLATION_LIMIT) || (translation.z < (-1 * TRANSLATION_LIMIT))) {
|
||||
translationDz = translationDz * -1;
|
||||
}
|
||||
|
||||
// adjust the hand rotation in a periodic back and forth motion for each of pitch/yaw/roll
|
||||
pitch = pitch + pitchDelta;
|
||||
yaw = yaw + yawDelta;
|
||||
roll = roll + rollDelta;
|
||||
if ((pitch > ROTATION_MAX) || (pitch < ROTATION_MIN)) {
|
||||
pitchDelta = pitchDelta * -1;
|
||||
}
|
||||
if ((yaw > ROTATION_MAX) || (yaw < ROTATION_MIN)) {
|
||||
yawDelta = yawDelta * -1;
|
||||
}
|
||||
if ((roll > ROTATION_MAX) || (roll < ROTATION_MIN)) {
|
||||
rollDelta = rollDelta * -1;
|
||||
}
|
||||
|
||||
var rotation = Quat.fromPitchYawRollDegrees(pitch, yaw, roll);
|
||||
|
||||
var pose = {
|
||||
translation: translation,
|
||||
rotation: rotation,
|
||||
velocity: { x: 0, y: 0, z: 0 },
|
||||
angularVelocity: { x: 0, y: 0, z: 0 }
|
||||
};
|
||||
return pose;
|
||||
}).debug(true).to(Controller.Standard.LeftHand);
|
||||
|
||||
Controller.enableMapping(MAPPING_NAME);
|
||||
|
||||
|
||||
Script.scriptEnding.connect(function(){
|
||||
mapping.disable();
|
||||
});
|
121
examples/controllers/reticleHandAngularVelocityTest.js
Normal file
121
examples/controllers/reticleHandAngularVelocityTest.js
Normal file
|
@ -0,0 +1,121 @@
|
|||
//
|
||||
// reticleHandAngularVelocityTest.js
|
||||
// examples/controllers
|
||||
//
|
||||
// Created by Brad Hefta-Gaub on 2015/12/15
|
||||
// Copyright 2015 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
// If you set this to true, you will get the raw instantaneous angular velocity.
|
||||
// note: there is a LOT of noise in the hydra rotation, you will probably be very
|
||||
// frustrated with the level of jitter.
|
||||
var USE_INSTANTANEOUS_ANGULAR_VELOCITY = false;
|
||||
var whichHand = Controller.Standard.RightHand;
|
||||
var whichTrigger = Controller.Standard.RT;
|
||||
|
||||
|
||||
|
||||
function msecTimestampNow() {
|
||||
var d = new Date();
|
||||
return d.getTime();
|
||||
}
|
||||
|
||||
function length(posA, posB) {
|
||||
var dx = posA.x - posB.x;
|
||||
var dy = posA.y - posB.y;
|
||||
var length = Math.sqrt((dx*dx) + (dy*dy))
|
||||
return length;
|
||||
}
|
||||
|
||||
var EXPECTED_CHANGE = 50;
|
||||
var lastPos = Controller.getReticlePosition();
|
||||
function moveReticle(dX, dY) {
|
||||
var globalPos = Controller.getReticlePosition();
|
||||
|
||||
// some debugging to see if position is jumping around on us...
|
||||
var distanceSinceLastMove = length(lastPos, globalPos);
|
||||
if (distanceSinceLastMove > EXPECTED_CHANGE) {
|
||||
print("------------------ distanceSinceLastMove:" + distanceSinceLastMove + "----------------------------");
|
||||
}
|
||||
|
||||
if (Math.abs(dX) > EXPECTED_CHANGE) {
|
||||
print("surpressing unexpectedly large change dX:" + dX + "----------------------------");
|
||||
dX = 0;
|
||||
}
|
||||
if (Math.abs(dY) > EXPECTED_CHANGE) {
|
||||
print("surpressing unexpectedly large change dY:" + dY + "----------------------------");
|
||||
dY = 0;
|
||||
}
|
||||
|
||||
globalPos.x += dX;
|
||||
globalPos.y += dY;
|
||||
Controller.setReticlePosition(globalPos);
|
||||
lastPos = globalPos;
|
||||
}
|
||||
|
||||
var firstTime = true;
|
||||
var lastTime = msecTimestampNow();
|
||||
var previousRotation;
|
||||
|
||||
var MAPPING_NAME = "com.highfidelity.testing.reticleWithHand";
|
||||
var mapping = Controller.newMapping(MAPPING_NAME);
|
||||
mapping.from(whichTrigger).peek().constrainToInteger().to(Controller.Actions.ReticleClick);
|
||||
mapping.from(whichHand).peek().to(function(pose) {
|
||||
|
||||
var MSECS_PER_SECOND = 1000;
|
||||
var now = msecTimestampNow();
|
||||
var deltaMsecs = (now - lastTime);
|
||||
var deltaTime = deltaMsecs / MSECS_PER_SECOND;
|
||||
|
||||
if (firstTime) {
|
||||
previousRotation = pose.rotation;
|
||||
lastTime = msecTimestampNow();
|
||||
firstTime = false;
|
||||
}
|
||||
|
||||
// pose.angularVelocity - is the angularVelocity in a "physics" sense, that
|
||||
// means the direction of the vector is the axis of symetry of rotation
|
||||
// and the scale of the vector is the speed in radians/second of rotation
|
||||
// around that axis.
|
||||
//
|
||||
// we want to deconstruct that in the portion of the rotation on the Y axis
|
||||
// and make that portion move our reticle in the horizontal/X direction
|
||||
// and the portion of the rotation on the X axis and make that portion
|
||||
// move our reticle in the veritcle/Y direction
|
||||
var xPart = -pose.angularVelocity.y;
|
||||
var yPart = -pose.angularVelocity.x;
|
||||
|
||||
// pose.angularVelocity is "smoothed", we can calculate our own instantaneous
|
||||
// angular velocity as such:
|
||||
if (USE_INSTANTANEOUS_ANGULAR_VELOCITY) {
|
||||
var previousConjugate = Quat.conjugate(previousRotation);
|
||||
var deltaRotation = Quat.multiply(pose.rotation, previousConjugate);
|
||||
var normalizedDeltaRotation = Quat.normalize(deltaRotation);
|
||||
var axis = Quat.axis(normalizedDeltaRotation);
|
||||
var speed = Quat.angle(normalizedDeltaRotation) / deltaTime;
|
||||
var instantaneousAngularVelocity = Vec3.multiply(speed, axis);
|
||||
|
||||
xPart = -instantaneousAngularVelocity.y;
|
||||
yPart = -instantaneousAngularVelocity.x;
|
||||
|
||||
previousRotation = pose.rotation;
|
||||
}
|
||||
|
||||
var MOVE_SCALE = 1;
|
||||
lastTime = now;
|
||||
|
||||
var dX = (xPart * MOVE_SCALE) / deltaTime;
|
||||
var dY = (yPart * MOVE_SCALE) / deltaTime;
|
||||
|
||||
moveReticle(dX, dY);
|
||||
});
|
||||
mapping.enable();
|
||||
|
||||
Script.scriptEnding.connect(function(){
|
||||
mapping.disable();
|
||||
});
|
||||
|
||||
|
|
@ -33,7 +33,6 @@ var mappingJSON = {
|
|||
|
||||
mapping = Controller.parseMapping(JSON.stringify(mappingJSON));
|
||||
mapping.enable();
|
||||
|
||||
Script.scriptEnding.connect(function(){
|
||||
mapping.disable();
|
||||
});
|
||||
|
|
|
@ -703,13 +703,37 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
|
|||
} else if (action == controller::toInt(controller::Action::CONTEXT_MENU)) {
|
||||
VrMenu::toggle(); // show context menu even on non-stereo displays
|
||||
} else if (action == controller::toInt(controller::Action::RETICLE_X)) {
|
||||
auto globalPos = QCursor::pos();
|
||||
globalPos.setX(globalPos.x() + state);
|
||||
QCursor::setPos(globalPos);
|
||||
auto oldPos = QCursor::pos();
|
||||
auto newPos = oldPos;
|
||||
newPos.setX(oldPos.x() + state);
|
||||
QCursor::setPos(newPos);
|
||||
|
||||
|
||||
// NOTE: This is some debugging code we will leave in while debugging various reticle movement strategies,
|
||||
// remove it after we're done
|
||||
const float REASONABLE_CHANGE = 50.0f;
|
||||
glm::vec2 oldPosG = { oldPos.x(), oldPos.y() };
|
||||
glm::vec2 newPosG = { newPos.x(), newPos.y() };
|
||||
auto distance = glm::distance(oldPosG, newPosG);
|
||||
if (distance > REASONABLE_CHANGE) {
|
||||
qDebug() << "Action::RETICLE_X... UNREASONABLE CHANGE! distance:" << distance << " oldPos:" << oldPosG << " newPos:" << newPosG;
|
||||
}
|
||||
|
||||
} else if (action == controller::toInt(controller::Action::RETICLE_Y)) {
|
||||
auto globalPos = QCursor::pos();
|
||||
globalPos.setY(globalPos.y() + state);
|
||||
QCursor::setPos(globalPos);
|
||||
auto oldPos = QCursor::pos();
|
||||
auto newPos = oldPos;
|
||||
newPos.setY(oldPos.y() + state);
|
||||
QCursor::setPos(newPos);
|
||||
|
||||
// NOTE: This is some debugging code we will leave in while debugging various reticle movement strategies,
|
||||
// remove it after we're done
|
||||
const float REASONABLE_CHANGE = 50.0f;
|
||||
glm::vec2 oldPosG = { oldPos.x(), oldPos.y() };
|
||||
glm::vec2 newPosG = { newPos.x(), newPos.y() };
|
||||
auto distance = glm::distance(oldPosG, newPosG);
|
||||
if (distance > REASONABLE_CHANGE) {
|
||||
qDebug() << "Action::RETICLE_Y... UNREASONABLE CHANGE! distance:" << distance << " oldPos:" << oldPosG << " newPos:" << newPosG;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -42,7 +42,22 @@ namespace controller {
|
|||
}
|
||||
|
||||
void Pose::fromScriptValue(const QScriptValue& object, Pose& pose) {
|
||||
// nothing for now...
|
||||
auto translation = object.property("translation");
|
||||
auto rotation = object.property("rotation");
|
||||
auto velocity = object.property("velocity");
|
||||
auto angularVelocity = object.property("angularVelocity");
|
||||
if (translation.isValid() &&
|
||||
rotation.isValid() &&
|
||||
velocity.isValid() &&
|
||||
angularVelocity.isValid()) {
|
||||
vec3FromScriptValue(translation, pose.translation);
|
||||
quatFromScriptValue(rotation, pose.rotation);
|
||||
vec3FromScriptValue(velocity, pose.velocity);
|
||||
vec3FromScriptValue(angularVelocity, pose.angularVelocity);
|
||||
pose.valid = true;
|
||||
} else {
|
||||
pose.valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <QCursor>
|
||||
#include <QThread>
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QVariant>
|
||||
|
@ -29,6 +30,7 @@
|
|||
#include <QtScript/QScriptValue>
|
||||
|
||||
#include <DependencyManager.h>
|
||||
#include <StreamUtils.h>
|
||||
|
||||
#include "UserInputMapper.h"
|
||||
#include "StandardControls.h"
|
||||
|
@ -87,6 +89,21 @@ namespace controller {
|
|||
Q_INVOKABLE QObject* parseMapping(const QString& json);
|
||||
Q_INVOKABLE QObject* loadMapping(const QString& jsonUrl);
|
||||
|
||||
Q_INVOKABLE glm::vec2 getReticlePosition() {
|
||||
return toGlm(QCursor::pos());
|
||||
}
|
||||
Q_INVOKABLE void setReticlePosition(glm::vec2 position) {
|
||||
// NOTE: This is some debugging code we will leave in while debugging various reticle movement strategies,
|
||||
// remove it after we're done
|
||||
const float REASONABLE_CHANGE = 50.0f;
|
||||
glm::vec2 oldPos = toGlm(QCursor::pos());
|
||||
auto distance = glm::distance(oldPos, position);
|
||||
if (distance > REASONABLE_CHANGE) {
|
||||
qDebug() << "Contrller::ScriptingInterface ---- UNREASONABLE CHANGE! distance:" << distance << " oldPos:" << oldPos << " newPos:" << position;
|
||||
}
|
||||
|
||||
QCursor::setPos(position.x, position.y);
|
||||
}
|
||||
|
||||
//Q_INVOKABLE bool isPrimaryButtonPressed() const;
|
||||
//Q_INVOKABLE glm::vec2 getPrimaryJoystickPosition() const;
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
|
||||
#include <QtCore/QThread>
|
||||
|
||||
#include <StreamUtils.h>
|
||||
|
||||
using namespace controller;
|
||||
|
||||
float ScriptEndpoint::peek() const {
|
||||
|
@ -23,7 +25,16 @@ void ScriptEndpoint::updateValue() {
|
|||
return;
|
||||
}
|
||||
|
||||
_lastValueRead = (float)_callable.call().toNumber();
|
||||
QScriptValue result = _callable.call();
|
||||
|
||||
// If the callable ever returns a non-number, we assume it's a pose
|
||||
// and start reporting ourselves as a pose.
|
||||
if (result.isNumber()) {
|
||||
_lastValueRead = (float)_callable.call().toNumber();
|
||||
} else {
|
||||
Pose::fromScriptValue(result, _lastPoseRead);
|
||||
_returnPose = true;
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptEndpoint::apply(float value, const Pointer& source) {
|
||||
|
@ -44,3 +55,36 @@ void ScriptEndpoint::internalApply(float value, int sourceID) {
|
|||
_callable.call(QScriptValue(),
|
||||
QScriptValueList({ QScriptValue(value), QScriptValue(sourceID) }));
|
||||
}
|
||||
|
||||
Pose ScriptEndpoint::peekPose() const {
|
||||
const_cast<ScriptEndpoint*>(this)->updatePose();
|
||||
return _lastPoseRead;
|
||||
}
|
||||
|
||||
void ScriptEndpoint::updatePose() {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "updatePose", Qt::QueuedConnection);
|
||||
return;
|
||||
}
|
||||
QScriptValue result = _callable.call();
|
||||
Pose::fromScriptValue(result, _lastPoseRead);
|
||||
}
|
||||
|
||||
void ScriptEndpoint::apply(const Pose& newPose, const Pointer& source) {
|
||||
if (newPose == _lastPoseWritten) {
|
||||
return;
|
||||
}
|
||||
internalApply(newPose, source->getInput().getID());
|
||||
}
|
||||
|
||||
void ScriptEndpoint::internalApply(const Pose& newPose, int sourceID) {
|
||||
_lastPoseWritten = newPose;
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "internalApply", Qt::QueuedConnection,
|
||||
Q_ARG(const Pose&, newPose),
|
||||
Q_ARG(int, sourceID));
|
||||
return;
|
||||
}
|
||||
_callable.call(QScriptValue(),
|
||||
QScriptValueList({ Pose::toScriptValue(_callable.engine(), newPose), QScriptValue(sourceID) }));
|
||||
}
|
||||
|
|
|
@ -27,13 +27,26 @@ public:
|
|||
virtual float peek() const override;
|
||||
virtual void apply(float newValue, const Pointer& source) override;
|
||||
|
||||
|
||||
virtual Pose peekPose() const override;
|
||||
virtual void apply(const Pose& newValue, const Pointer& source) override;
|
||||
|
||||
virtual bool isPose() const override { return _returnPose; }
|
||||
|
||||
protected:
|
||||
Q_INVOKABLE void updateValue();
|
||||
Q_INVOKABLE virtual void internalApply(float newValue, int sourceID);
|
||||
|
||||
Q_INVOKABLE void updatePose();
|
||||
Q_INVOKABLE virtual void internalApply(const Pose& newValue, int sourceID);
|
||||
private:
|
||||
QScriptValue _callable;
|
||||
float _lastValueRead { 0.0f };
|
||||
float _lastValueWritten { 0.0f };
|
||||
|
||||
bool _returnPose { false };
|
||||
Pose _lastPoseRead;
|
||||
Pose _lastPoseWritten;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -22,6 +22,10 @@ quat Quat::normalize(const glm::quat& q) {
|
|||
return glm::normalize(q);
|
||||
}
|
||||
|
||||
quat Quat::conjugate(const glm::quat& q) {
|
||||
return glm::conjugate(q);
|
||||
}
|
||||
|
||||
glm::quat Quat::rotationBetween(const glm::vec3& v1, const glm::vec3& v2) {
|
||||
return ::rotationBetween(v1, v2);
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ class Quat : public QObject {
|
|||
public slots:
|
||||
glm::quat multiply(const glm::quat& q1, const glm::quat& q2);
|
||||
glm::quat normalize(const glm::quat& q);
|
||||
glm::quat conjugate(const glm::quat& q);
|
||||
glm::quat lookAt(const glm::vec3& eye, const glm::vec3& center, const glm::vec3& up);
|
||||
glm::quat lookAtSimple(const glm::vec3& eye, const glm::vec3& center);
|
||||
glm::quat rotationBetween(const glm::vec3& v1, const glm::vec3& v2);
|
||||
|
|
Loading…
Reference in a new issue