356 lines
No EOL
11 KiB
JavaScript
356 lines
No EOL
11 KiB
JavaScript
/* global Script */
|
|
/*
|
|
|
|
Custom Avatar Blendshape / Script UI Trigger
|
|
|
|
Released under Creative Commons Attribution, 2018 by Menithal
|
|
https://creativecommons.org/licenses/by/3.0/
|
|
|
|
*/
|
|
|
|
const APP_URL = "index.html?fggf";
|
|
const APP = Script.resolvePath(APP_URL);
|
|
|
|
var setInterval = Script.setInterval;
|
|
var setTimeout = Script.setTimeout;
|
|
|
|
// Maps the "unconventional Blendshapes" that are not used by Hifi's audio system
|
|
// To custom emoticon system, Modify to your hearts content
|
|
var emotionMap = {
|
|
"Normal": "",
|
|
"Content": "LipsUpperUp",
|
|
"Calm": "LipsLowerDown",
|
|
"Stare": "LipsLowerOpen",
|
|
"Suprised": "ChinLowerRaise",
|
|
">_<": "ChinUpperRaise", // >_<
|
|
"Λ": "JawFwd",
|
|
"o": "JawChew",
|
|
"▲": "JawLeft" // Triangle
|
|
};
|
|
|
|
function toggleEmote(target, state, booleanStates) {
|
|
return {
|
|
emote: target,
|
|
state: state,
|
|
textStates: booleanStates
|
|
};
|
|
}
|
|
|
|
var rightEyeStatic = {
|
|
"x": -1.3767878215276141e-7,
|
|
"y": 0.7071066498756409,
|
|
"z": 0.70710688829422,
|
|
"w": -1.376787679419067e-7
|
|
};
|
|
var leftEyeStatic = {
|
|
"x": -8.429372400087232e-8,
|
|
"y": 0.7071070671081543,
|
|
"z": 0.7071065902709961,
|
|
"w": -2.2476429606399506e-8
|
|
};
|
|
// These are modes that should stay on or off, instead of temporary "expressions"
|
|
var toggleMap = {
|
|
"CrossEye": toggleEmote(function (state) {
|
|
var rightEyeOverride = MyAvatar.getJointIndex("RightEyeOverride");
|
|
var leftEyeOverride = MyAvatar.getJointIndex("LeftEyeOverride");
|
|
|
|
if (state) {
|
|
MyAvatar.setJointRotation(rightEyeOverride, Quat.multiply(rightEyeStatic, Quat.fromVec3Degrees({
|
|
x: 15,
|
|
y: 0,
|
|
z: -35
|
|
})));
|
|
MyAvatar.setJointRotation(leftEyeOverride, Quat.multiply(leftEyeStatic, Quat.fromVec3Degrees({
|
|
x: -15,
|
|
y: 0,
|
|
z: 35
|
|
})));
|
|
} else {
|
|
MyAvatar.setJointRotation(rightEyeOverride, rightEyeStatic);
|
|
MyAvatar.setJointRotation(leftEyeOverride, leftEyeStatic);
|
|
// Return Joint rotations back to 0.
|
|
}
|
|
}, false, ["Off", "On"]),
|
|
"Glazed Pupil": toggleEmote("MouthDimple_L", false, ["Off", "On"]),
|
|
"Tears": toggleEmote("MouthDimple_R", false, ["Off", "On"]),
|
|
"Starry Eye": toggleEmote("LipsPucker", false, ["Off", "On"]), // Or was it pucker?
|
|
"Highlight": toggleEmote("EyeIn_R", false, ["Off", "On"]),
|
|
"Tiny Pupils": toggleEmote("EyeIn_L", false, ["Off", "On"]),
|
|
"Teeth": toggleEmote("EyeDown_L", false, ["On", "Off"]),
|
|
"Fangs": toggleEmote("EyeDown_R", false, ["Off", "On"]),
|
|
"Teeth Lower": toggleEmote("EyeOut_L", false, ["Off", "On"]),
|
|
"Serious": toggleEmote("EyeUp_L", false, ["Off", "On"]),
|
|
"Sadness": toggleEmote("EyeUp_R", false, ["Off", "On"]),
|
|
"Cheerful": toggleEmote("EyeSquint_L", false, ["Off", "On"]),
|
|
"Anger": toggleEmote("EyeSquint_R", false, ["Off", "On"]),
|
|
"Flat Brows": toggleEmote("CheekSquint_L", false, ["Off", "On"]),
|
|
"Noseline": toggleEmote("Sneer", false, ["On", "Off"]),
|
|
"Blush": toggleEmote("MouthFrown_L", false, ["Off", "On"]),
|
|
"Big Highlight": toggleEmote("MouthFrown_R", false, ["Off", "On"]),
|
|
"Clear Toggles": toggleEmote(function (state) {
|
|
var values = Object.values(toggleMap);
|
|
for (var x = 0; x < values.length; x++) {
|
|
if (typeof values[x].emote !== "function") {
|
|
MyAvatar.setBlendshape(values[x].emote, 0);
|
|
values[x].state = false;
|
|
} else {}
|
|
}
|
|
}, false, ["", ""])
|
|
|
|
};
|
|
|
|
function getCurrentStatus() {
|
|
var keys = Object.keys(toggleMap);
|
|
var values = Object.values(toggleMap);
|
|
var results = [];
|
|
for (var i = 0; i < values.length; i++) {
|
|
results[i] = [keys[i], values[i].textStates[values[i].state ? 1 : 0]];
|
|
}
|
|
return results;
|
|
}
|
|
|
|
function turnEmotesOff(list, blendshape) {
|
|
for (var x = 0; x < list.length; x++) {
|
|
if (list[x] !== blendshape) {
|
|
MyAvatar.setBlendshape(list[x], 0);
|
|
} else {
|
|
MyAvatar.setBlendshape(list[x], 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
var activeApp = false;
|
|
|
|
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
|
// TODO: Investigate a better way to automate this
|
|
var tabletButton = tablet.addButton({
|
|
text: "AVATAR",
|
|
sortOrder: 13
|
|
});
|
|
|
|
var onWebEventReceived;
|
|
|
|
function emitWebEvent(event) {
|
|
tablet.emitScriptEvent(JSON.stringify(event));
|
|
}
|
|
|
|
MyAvatar.setForceFaceTrackerConnected(true);
|
|
|
|
var onclick = function (e) {
|
|
|
|
MyAvatar.setForceFaceTrackerConnected(true);
|
|
if (activeApp) {
|
|
tablet.gotoHomeScreen();
|
|
} else {
|
|
tablet.gotoWebScreen(APP);
|
|
}
|
|
activeApp = !activeApp;
|
|
}
|
|
|
|
// Hifi Polyfill
|
|
if (!Object.values) {
|
|
Object.prototype.values = function (val) {
|
|
return Object.keys(val).map(function (key) {
|
|
return val[key];
|
|
});
|
|
};
|
|
}
|
|
|
|
function cap(value, max) {
|
|
if (value > max) {
|
|
return max;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
var onWebEventReceived = function (event) {
|
|
var data = JSON.parse(event);
|
|
if (data.type === "emote_app_load_complete") {
|
|
console.log("Build App", JSON.stringify(tablet));
|
|
emitWebEvent({
|
|
type: "emote_app_build",
|
|
emotes: Object.keys(emotionMap),
|
|
states: getCurrentStatus(toggleMap)
|
|
});
|
|
return true;
|
|
} else if (data.type === "emote_app_play") {
|
|
console.log("emote app play", data.message);
|
|
targetEmote = emotionMap[data.message];
|
|
emoteValues = Object.values(emotionMap);
|
|
|
|
if (typeof targetEmote !== "function") {
|
|
turnEmotesOff(emoteValues, targetEmote);
|
|
} else {
|
|
targetEmote();
|
|
}
|
|
} else if (data.type === "emote_app_toggle") {
|
|
console.log("emote app toggle", data.message);
|
|
targetEmote = toggleMap[data.message];
|
|
targetEmote.state = !targetEmote.state;
|
|
|
|
if (typeof targetEmote.emote !== "function") {
|
|
MyAvatar.setBlendshape(targetEmote.emote, targetEmote.state);
|
|
} else {
|
|
targetEmote.emote(targetEmote.state);
|
|
}
|
|
|
|
emitWebEvent({
|
|
type: "emote_app_state_sync",
|
|
states: getCurrentStatus(toggleMap)
|
|
});
|
|
}
|
|
return false;
|
|
};
|
|
|
|
|
|
var controllerDirections = [
|
|
"LipsUpperUp", // Content UP
|
|
"ChinLowerRaise", // Suprised DOWN
|
|
"ChinUpperRaise", // >_< LEFT
|
|
"EyeUp_R" // Sadness RIGHT
|
|
];
|
|
|
|
var currentCoordinates = {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
var DISABLED_CONTROL_GROUP = "com.menithal.avatar.emotions.disabled"
|
|
var ENABLED_CONTROL_GROUP = "com.menithal.avatar.emotions.active"
|
|
|
|
var disabledControls = Controller.newMapping(DISABLED_CONTROL_GROUP);
|
|
var enabledControls = Controller.newMapping(ENABLED_CONTROL_GROUP);
|
|
|
|
var Standard = Controller.Standard;
|
|
var Vive = Controller.Hardware.Vive;
|
|
var OculusTouch = Controller.Hardware.OculusTouch;
|
|
var GamePad = Controller.Hardware.GamePad;
|
|
|
|
function emotionControl(toggle) {
|
|
console.log("Emote ", toggle);
|
|
if (toggle) {
|
|
Controller.disableMapping(DISABLED_CONTROL_GROUP);
|
|
Controller.enableMapping(ENABLED_CONTROL_GROUP);
|
|
} else {
|
|
Controller.enableMapping(DISABLED_CONTROL_GROUP);
|
|
Controller.disableMapping(ENABLED_CONTROL_GROUP);
|
|
|
|
currentCoordinates = {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
// Clear Above
|
|
blendShapes(currentCoordinates);
|
|
|
|
var values = Object.values(toggleMap);
|
|
for (var x = 0; x < toggleMap.length; x++) {
|
|
for (var y = 0; y < controllerDirections; y++) {
|
|
if (controllerDirections[y] === toggleMap[x].emote) {
|
|
values[y].state = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
var MIN_TRESHOLD = 0.05;
|
|
|
|
function blendShapes(coordinates) {
|
|
|
|
console.log("coordinates ", coordinates);
|
|
MyAvatar.setBlendshape(controllerDirections[2], cap(Math.abs(coordinates.x > MIN_TRESHOLD ? 0 : coordinates.x) * 2.5, 1.0));
|
|
MyAvatar.setBlendshape(controllerDirections[3], cap(Math.abs(coordinates.x > MIN_TRESHOLD ? coordinates.x : 0) * 2.5, 1.0));
|
|
MyAvatar.setBlendshape(controllerDirections[0], cap(Math.abs(coordinates.y > MIN_TRESHOLD ? 0 : coordinates.y) * 2.5, 1.0));
|
|
MyAvatar.setBlendshape(controllerDirections[1], cap(Math.abs(coordinates.y > MIN_TRESHOLD ? coordinates.y : 0) * 2.5, 1.0));
|
|
}
|
|
|
|
function updateX(val) {
|
|
if (Math.abs(val) > 0) {
|
|
currentCoordinates.x = val;
|
|
|
|
}
|
|
blendShapes(currentCoordinates);
|
|
}
|
|
|
|
function updateY(val) {
|
|
if (Math.abs(val) > 0) {
|
|
currentCoordinates.y = val;
|
|
}
|
|
blendShapes(currentCoordinates);
|
|
}
|
|
|
|
function clear(val) {
|
|
currentCoordinates = {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
}
|
|
|
|
function empty(val) {}
|
|
|
|
if (MyAvatar.getDominantHand() === "right") {
|
|
disabledControls.from(Standard.RightSecondaryThumb).to(emotionControl);
|
|
enabledControls.from(Standard.RightSecondaryThumb).to(emotionControl);
|
|
|
|
enabledControls.from(Standard.LX).to(updateX);
|
|
enabledControls.from(Standard.LY).to(updateY);
|
|
if (GamePad) {
|
|
|
|
disabledControls.from(GamePad.RT).to(emotionControl);
|
|
enabledControls.from(GamePad.RT).to(emotionControl);
|
|
enabledControls.from(GamePad.LX).to(updateX);
|
|
enabledControls.from(GamePad.LY).to(updateY);
|
|
}
|
|
if (Vive) {
|
|
enabledControls.from(Vive.LX).to(updateX);
|
|
enabledControls.from(Vive.LY).to(updateY);
|
|
enabledControls.from(Vive.RightApplicationMenu).to(emotionControl)
|
|
}
|
|
if (OculusTouch) {
|
|
enabledControls.from(OculusTouch.LX).to(updateX);
|
|
enabledControls.from(OculusTouch.LY).to(updateX);
|
|
enabledControls.from(OculusTouch.B).to(emotionControl)
|
|
}
|
|
|
|
enabledControls.from(Standard.LS).to(clear);
|
|
enabledControls.from(Standard.LeftPrimaryThumb).to(empty);
|
|
} else {
|
|
disabledControls.from(Standard.LeftSecondaryThumb).to(emotionControl);
|
|
enabledControls.from(Standard.LeftSecondaryThumb).to(emotionControl);
|
|
|
|
enabledControls.from(Standard.RY).to(updateX);
|
|
enabledControls.from(Standard.RX).to(updateY);
|
|
if (GamePad) {
|
|
disabledControls.from(GamePad.LT).to(emotionControl);
|
|
disabledControls.from(GamePad.LT).to(emotionControl);
|
|
enabledControls.from(GamePad.RX).to(updateX);
|
|
enabledControls.from(GamePad.RY).to(updateY);
|
|
}
|
|
if (Vive) {
|
|
enabledControls.from(Vive.LeftSecondaryThumb).to(emotionControl)
|
|
enabledControls.from(Vive.LX).to(updateX);
|
|
enabledControls.from(Vive.LY).to(updateY);
|
|
enabledControls.from(Vive.LeftApplicationMenu).to(emotionControl)
|
|
}
|
|
if (OculusTouch) {
|
|
enabledControls.from(OculusTouch.RX).to(updateX);
|
|
enabledControls.from(OculusTouch.RY).to(updateX);
|
|
enabledControls.from(OculusTouch.Y).to(emotionControl)
|
|
}
|
|
|
|
enabledControls.from(Standard.RS).to(clear);
|
|
enabledControls.from(Standard.RightPrimaryThumb).to(empty);
|
|
}
|
|
|
|
Controller.enableMapping(DISABLED_CONTROL_GROUP);
|
|
|
|
tabletButton.clicked.connect(onclick);
|
|
tablet.webEventReceived.connect(onWebEventReceived);
|
|
|
|
Script.scriptEnding.connect(function () {
|
|
if (activeApp) {
|
|
tablet.webEventReceived.disconnect(onWebEventReceived);
|
|
}
|
|
activeApp = false;
|
|
tabletButton.clicked.disconnect(onclick);
|
|
tablet.removeButton(tabletButton);
|
|
}); |