overte/scripts/developer/automaticLookAt.js

1344 lines
No EOL
62 KiB
JavaScript

//
// automaticLookAt.js
// This script controls the avatar's look-at-target for the head and eyes, according to other avatar's actions
// It tries to simulate human interaction during group conversations
//
// Created by Luis Cuenca on 11/11/19
// Copyright 2019 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() {
//////////////////////////////////////////////
// debugger.js ///////
//////////////////////////////////////////////
var TEXT_BOX_WIDTH = 350;
var TEXT_BOX_MIN_HEIGHT = 40;
var TEXT_BOX_TOP_MARGIN = 100;
var TEXT_CAPTION_HEIGHT = 30;
var TEXT_CAPTION_COLOR = { red: 0, green: 0, blue: 0 };
var TEXT_CAPTION_SIZE = 18;
var TEXT_CAPTION_MARGIN = 6;
var CHECKBOX_MARK_MARGIN = 3;
var DEGREE_TO_RADIAN = 0.0174533;
var ENGAGED_AVATARS_DEBUG_COLOR = { red: 0, green: 255, blue: 255 };
var ENGAGED_AVATARS_DEBUG_ALPHA = 0.3;
var FOCUS_AVATAR_DEBUG_COLOR = { red: 255, green: 0, blue: 0 };
var FOCUS_AVATAR_DEBUG_ALPHA = 1.0;
var TALKER_AVATAR_DEBUG_COLOR = { red: 0, green: 0, blue: 0 };
var TALKER_AVATAR_DEBUG_ALPHA = 0.8;
var DEFAULT_OUTLINE_COLOR = { red: 155, green: 155, blue: 255 };
var DEFAULT_OUTLINE_WIDTH = 2;
var LookAtDebugger = function() {
var self = this;
var IMAGE_DIMENSIONS = {x: 0.2, y: 0.2, z:0.2};
var TARGET_ICON_PATH = Script.getExternalPath(Script.ExternalPaths.HF_Content, "/luis/test_scripts/LookAtApp/eyeFocus.png");
var INFINITY_ICON_PATH = Script.getExternalPath(Script.ExternalPaths.HF_Content, "/luis/test_scripts/LookAtApp/noFocus.png");
this.items = {};
this.active = false;
// UI elements
this.textBox;
this.activeCheckBox;
this.activeCheckBoxMark;
this.activeCaption;
this.textBoxHeight = 0.0;
this.logs = [];
this.getStyleProps = function(color, alpha) {
return {
fillUnoccludedColor: color,
fillUnoccludedAlpha: alpha,
fillOccludedColor: color,
fillOccludedAlpha: alpha,
outlineUnoccludedColor: DEFAULT_OUTLINE_COLOR,
outlineUnoccludedAlpha: alpha,
outlineOccludedColor: DEFAULT_OUTLINE_COLOR,
outlineOccludedAlpha: alpha,
outlineWidth: DEFAULT_OUTLINE_WIDTH,
isOutlineSmooth: false
}
}
this.Styles = {
"engaged" : {
"style": self.getStyleProps(ENGAGED_AVATARS_DEBUG_COLOR, ENGAGED_AVATARS_DEBUG_ALPHA),
"name" : "engagedSel"
},
"focus" : {
"style": self.getStyleProps(FOCUS_AVATAR_DEBUG_COLOR, FOCUS_AVATAR_DEBUG_ALPHA),
"name" : "focusSel"
},
"talker" : {
"style": self.getStyleProps(TALKER_AVATAR_DEBUG_COLOR, TALKER_AVATAR_DEBUG_ALPHA),
"name" : "talkerSel"
}
};
this.eyeTarget;
this.eyesTargetProps = {
name: "Eyes-Target-Image",
position: { x: 0.0, y: 0.0, z: 0.0 },
color: {red: 255, green: 0, blue: 255},
url: TARGET_ICON_PATH,
dimensions: IMAGE_DIMENSIONS,
alpha: 1.0,
visible: false,
emissive: true,
ignoreRayIntersection: true,
drawInFront: true,
grabbable: false,
isFacingAvatar: true
};
this.headTarget;
this.headTargetProps = {
name: "Head-Target-Image",
position: { x: 0.0, y: 0.0, z: 0.0 },
color: {red: 0, green: 255, blue: 255},
url: TARGET_ICON_PATH,
dimensions: IMAGE_DIMENSIONS,
alpha: 1.0,
visible: false,
emissive: true,
ignoreRayIntersection: true,
drawInFront: true,
grabbable: false,
isFacingAvatar: true
};
this.textBoxProps = {
x: Window.innerWidth - TEXT_BOX_WIDTH,
y: TEXT_BOX_TOP_MARGIN,
width: TEXT_BOX_WIDTH,
height: TEXT_BOX_MIN_HEIGHT,
alpha: 0.7,
color: { red: 255, green: 255, blue: 255 }
};
this.activeCheckBoxProps = {
x: TEXT_CAPTION_MARGIN + self.textBoxProps.x,
y: 2 * TEXT_CAPTION_MARGIN + self.textBoxProps.y,
width: TEXT_CAPTION_SIZE,
height: TEXT_CAPTION_SIZE,
alpha: 0.0,
borderWidth: 2,
borderColor: TEXT_CAPTION_COLOR
};
this.activeCheckBoxMarkProps = {
x: self.activeCheckBoxProps.x + CHECKBOX_MARK_MARGIN,
y: self.activeCheckBoxProps.y + CHECKBOX_MARK_MARGIN,
width: TEXT_CAPTION_SIZE - 2.0 * CHECKBOX_MARK_MARGIN,
height: TEXT_CAPTION_SIZE - 2.0 * CHECKBOX_MARK_MARGIN,
visible: false,
alpha: 1.0,
color: TEXT_CAPTION_COLOR
}
this.captionProps = {
x: 2 * TEXT_CAPTION_SIZE + self.textBoxProps.x,
y: TEXT_CAPTION_MARGIN + self.textBoxProps.y,
width: self.textBoxProps.width,
height: 30,
alpha: 1.0,
backgroundAlpha: 0.0,
visible: true,
text: "Debug Auto Look At",
font: {
size: TEXT_CAPTION_SIZE
},
color: TEXT_CAPTION_COLOR,
topMargin: 0.5 * TEXT_CAPTION_MARGIN
};
this.log = function(txt) {
if (self.active) {
self.logs.push(Overlays.addOverlay("text", self.captionProps));
var y = self.textBoxProps.y + self.captionProps.height * (self.logs.length);
Overlays.editOverlay(self.logs[self.logs.length - 1], {y: y, text: txt});
var height = (TEXT_CAPTION_SIZE + 2.0 * TEXT_CAPTION_MARGIN) * (self.logs.length + 2);
if (this.textBoxHeight !== height) {
this.textBoxHeight = height
Overlays.editOverlay(self.textBox, {height: height});
}
}
}
this.clearLog = function() {
for (var n = 0; n < self.logs.length; n++) {
Overlays.deleteOverlay(self.logs[n]);
}
Overlays.editOverlay(self.textBox, self.textBoxProps);
self.logs = [];
self.log("____________________________");
}
this.setActive = function(isActive) {
self.active = isActive;
if (!self.active) {
self.turnOff();
} else {
self.turnOn();
}
};
this.onClick = function(event) {
if (event.x > self.activeCheckBoxProps.x && event.x < (self.activeCheckBoxProps.x + self.activeCheckBoxProps.width) &&
event.y > self.activeCheckBoxProps.y && event.y < (self.activeCheckBoxProps.y + self.activeCheckBoxProps.height)) {
self.setActive(!self.active);
Overlays.editOverlay(self.activeCheckBoxMark, {visible: self.active});
}
}
this.turnOn = function() {
for (var key in self.Styles) {
var selStyle = self.Styles[key];
Selection.enableListHighlight(selStyle.name, selStyle.style);
}
if (!self.eyeTarget) {
self.eyeTarget = Overlays.addOverlay("image3d", self.eyesTargetProps);
}
if (!self.headTarget) {
self.headTarget = Overlays.addOverlay("image3d", self.headTargetProps);
}
}
this.init = function() {
if (!self.textBox) {
self.textBox = Overlays.addOverlay("rectangle", self.textBoxProps);
}
if (!self.activeCheckBox) {
self.activeCheckBox = Overlays.addOverlay("rectangle", self.activeCheckBoxProps);
}
if (!self.activeCheckBoxMark) {
self.activeCheckBoxMark = Overlays.addOverlay("rectangle", self.activeCheckBoxMarkProps);
}
if (!self.activeCaption) {
self.activeCaption = Overlays.addOverlay("text", self.captionProps);
}
};
this.highLightAvatars = function(engagedIDs, focusID, talkerID) {
if (self.active) {
self.clearSelection();
engagedIDs = !engagedIDs ? [] : engagedIDs;
var focusIDs = !focusID ? [] : [focusID];
var talkerIDs = !talkerID ? [] : [talkerID];
self.highLightIDs(self.Styles.engaged.name, engagedIDs);
self.highLightIDs(self.Styles.focus.name, focusIDs);
self.highLightIDs(self.Styles.talker.name, talkerIDs);
return true;
} else {
return false;
}
};
this.getTargetProps = function(target) {
var distance = Vec3.length(Vec3.subtract(target, Camera.getPosition()));
RETARGET_MAX_DISTANCE = 100.0;
DIMENSION_SCALE = 0.05;
var isInfinite = false;
if (distance > RETARGET_MAX_DISTANCE) {
isInfinite = true;
var eyesToTarget = Vec3.multiply(RETARGET_MAX_DISTANCE, Vec3.normalize(Vec3.subtract(target, MyAvatar.getDefaultEyePosition())));
var newTarget = Vec3.sum(MyAvatar.getDefaultEyePosition(), eyesToTarget);
var cameraToTarget = Vec3.normalize(Vec3.subtract(newTarget, Camera.getPosition()));
target = Vec3.sum(Camera.getPosition(), cameraToTarget);
distance = Vec3.length(cameraToTarget);
}
// Scale the target to appear always with the same size on screen
var fov = DEGREE_TO_RADIAN * Camera.frustum.fieldOfView;
var scale = (Camera.frustum.aspectRatio < 1.0 ? Camera.frustum.aspectRatio : 1.0) * DIMENSION_SCALE;
var dimensionRatio = scale * (distance / (0.5 * Math.tan(0.5 * fov)));
var dimensions = Vec3.multiply(dimensionRatio, IMAGE_DIMENSIONS);
return {"dimensions": dimensions, "visible": true, "position": target, "url": isInfinite ? INFINITY_ICON_PATH : TARGET_ICON_PATH};
}
this.showTarget = function(headTarget, eyeTarget) {
if (!self.active) {
return;
}
var targetProps = self.getTargetProps(eyeTarget);
Overlays.editOverlay(self.eyeTarget, targetProps);
var headTargetProps = self.getTargetProps(headTarget);
Overlays.editOverlay(self.headTarget, headTargetProps);
}
this.hideEyeTarget = function() {
Overlays.editOverlay(self.eyeTarget, {"visible": false});
}
this.highLightIDs = function(selectionName, ids) {
self.items[selectionName] = ids;
for (var idx in ids) {
Selection.addToSelectedItemsList(selectionName, "avatar", ids[idx]);
}
}
this.clearSelection = function() {
for (key in self.Styles) {
var selStyle = self.Styles[key];
Selection.clearSelectedItemsList(selStyle.name);
}
}
this.turnOff = function() {
self.clearLog();
for (var key in self.Styles) {
var selStyle = self.Styles[key];
Selection.disableListHighlight(selStyle.name);
}
Overlays.deleteOverlay(self.headTarget);
self.headTarget = undefined;
Overlays.deleteOverlay(self.eyeTarget);
self.eyeTarget = undefined;
}
this.finish = function() {
if (self.active) {
self.setActive(false);
}
Overlays.deleteOverlay(self.textBox);
self.textBox = undefined;
Overlays.deleteOverlay(self.activeCheckBox);
self.activeCheckBox = undefined;
Overlays.deleteOverlay(self.activeCaption);
self.activeCaption = undefined;
Overlays.deleteOverlay(self.activeCheckBoxMark);
self.activeCheckBoxMark = undefined;
}
this.init();
}
//////////////////////////////////////////////
// randomHelper.js //////////
//////////////////////////////////////////////
var RandomHelper = function() {
var self = this;
this.createRandomIndexes = function(count) {
var indexes = [];
for (var n = 0; n < count; n++) {
indexes.push(n);
}
var randomIndexes = [];
for (var n = 0; n < count; n++) {
var indexesCount = indexes.length;
var randomIndex = 0;
if (indexesCount > 1) {
var randFactor = Math.random();
randomIndex = randFactor !== 1.0 ? Math.floor(randFactor * indexes.length) : indexes.length - 1;
}
randomIndexes.push(indexes[randomIndex]);
indexes.splice(randomIndex, 1);
}
return randomIndexes;
}
this.getRandomKey = function(keypool) {
// keypool can be an object. {key1: percentage1, key2: percentage2} or
// keypool can be an array. [key1, key2] Equal percentage each component
var equalChance = Array.isArray(keypool);
var totalPercentage = 0.0;
var percentages = {};
var normalizedPercentages = {};
var keys = equalChance ? keypool : Object.keys(keypool);
for (var n = 0; n < keys.length; n++) {
var key = keys[n];
percentages[key] = equalChance ? 1.0 / keys.length : keypool[key].chance;
totalPercentage += equalChance ? percentages[key] : keypool[key].chance;
}
var accumulatedVal = 0.0;
for (var n = 0; n < keys.length; n++) {
var key = keys[n];
var val = accumulatedVal + (percentages[key] / totalPercentage);
normalizedPercentages[key] = val;
accumulatedVal = val;
}
var dice = Math.random();
var floor = 0.0;
var hit = normalizedPercentages[keys[0]];
for (var n = 0; n < keys.length; n++) {
var key = keys[n];
if (dice > floor && dice < normalizedPercentages[key]) {
hit = key;
break;
}
floor = normalizedPercentages[key];
}
return { randomKey: hit, chance: percentages[hit]};
}
}
//////////////////////////////////////////////
// automaticMachine.js //////////
//////////////////////////////////////////////
var MIN_LOOKAT_HEAD_MIX_ALPHA = 0.04;
var MAX_LOOKAT_HEAD_MIX_ALPHA = 0.08;
var CAMERA_HEAD_MIX_ALPHA = 0.06;
var TargetType = {
"unknown" : 0,
"avatar" : 1,
"entity" : 2
}
var TargetOffsetMode = {
"noOffset" : 0,
"onlyHead" : 1,
"onlyEyes" : 2,
"headAndEyes" : 3,
"print" : function(sta) {
return ("OffsetMode: " + (Object.keys(TargetOffsetMode))[sta]);
}
}
var TargetMode = {
"noTarget" : 0,
"leftEye" : 1,
"rightEye" : 2,
"mouth" : 3,
"leftHand" : 4,
"rightHand" : 5,
"random" : 6,
"print" : function(sta) {
return ("TargetMode: " + (Object.keys(TargetMode))[sta]);
}
}
var ACTION_CONFIGURATION = {
"TargetMode.mouth": {
"joint": "Head",
"stareTimeRange" : {"min": 0.2, "max": 2.0},
"headSpeedRange" : {"min": MAX_LOOKAT_HEAD_MIX_ALPHA, "max": MIN_LOOKAT_HEAD_MIX_ALPHA},
"offsetChances" : {
"TargetOffsetMode.noOffset": {"chance": 0.7},
"TargetOffsetMode.onlyHead": {"chance": 0.3},
"TargetOffsetMode.onlyEyes": {"chance": 0.0},
"TargetOffsetMode.headAndEyes": {"chance": 0.0}
},
"offsetAngleRange" : {"min": 1.0, "max": 5.0},
"chanceWhileListening" : 0.45,
"chanceWhileTalking" : 0.25
},
"TargetMode.leftEye": {
"joint": "LeftEye",
"stareTimeRange" : {"min": 0.2, "max": 2.0},
"headSpeedRange" : {"min": MAX_LOOKAT_HEAD_MIX_ALPHA, "max": MIN_LOOKAT_HEAD_MIX_ALPHA},
"offsetChances" : {
"TargetOffsetMode.noOffset": {"chance": 0.5},
"TargetOffsetMode.onlyHead": {"chance": 0.3},
"TargetOffsetMode.onlyEyes": {"chance": 0.1},
"TargetOffsetMode.headAndEyes": {"chance": 0.1}
},
"offsetAngleRange" : {"min": 1.0, "max": 5.0},
"chanceWhileListening" : 0.20,
"chanceWhileTalking" : 0.30
},
"TargetMode.rightEye": {
"joint": "RightEye",
"stareTimeRange" : {"min": 0.2, "max": 2.0},
"headSpeedRange" : {"min": MAX_LOOKAT_HEAD_MIX_ALPHA, "max": MIN_LOOKAT_HEAD_MIX_ALPHA},
"offsetChances" : {
"TargetOffsetMode.noOffset": {"chance": 0.5},
"TargetOffsetMode.onlyHead": {"chance": 0.3},
"TargetOffsetMode.onlyEyes": {"chance": 0.1},
"TargetOffsetMode.headAndEyes": {"chance": 0.1}
},
"offsetAngleRange" : {"min": 1.0, "max": 5.0},
"chanceWhileListening" : 0.20,
"chanceWhileTalking" : 0.30
},
"TargetMode.leftHand": {
"joint": "LeftHand",
"stareTimeRange" : {"min": 0.2, "max": 2.0},
"headSpeedRange" : {"min": MAX_LOOKAT_HEAD_MIX_ALPHA, "max": MIN_LOOKAT_HEAD_MIX_ALPHA},
"offsetChances" : {
"TargetOffsetMode.noOffset": {"chance": 0.9},
"TargetOffsetMode.onlyHead": {"chance": 0.1},
"TargetOffsetMode.onlyEyes": {"chance": 0.0},
"TargetOffsetMode.headAndEyes": {"chance": 0.0}
},
"offsetAngleRange" : {"min": 1.0, "max": 10.0},
"chanceWhileListening" : 0.05,
"chanceWhileTalking" : 0.05
},
"TargetMode.rightHand": {
"joint": "RightHand",
"stareTimeRange" : {"min": 0.2, "max": 2.0},
"headSpeedRange" : {"min": MAX_LOOKAT_HEAD_MIX_ALPHA, "max": MIN_LOOKAT_HEAD_MIX_ALPHA},
"offsetChances" : {
"TargetOffsetMode.noOffset": {"chance": 0.9},
"TargetOffsetMode.onlyHead": {"chance": 0.1},
"TargetOffsetMode.onlyEyes": {"chance": 0.0},
"TargetOffsetMode.headAndEyes": {"chance": 0.0}
},
"offsetAngleRange" : {"min": 1.0, "max": 10.0},
"chanceWhileListening" : 0.05,
"chanceWhileTalking" : 0.05
},
"TargetMode.random": {
"joint": "Head",
"stareTimeRange" : {"min": 0.2, "max": 1.0},
"headSpeedRange" : {"min": MAX_LOOKAT_HEAD_MIX_ALPHA, "max": MIN_LOOKAT_HEAD_MIX_ALPHA},
"offsetChances" : {
"TargetOffsetMode.noOffset": {"chance": 0.0},
"TargetOffsetMode.onlyHead": {"chance": 0.0},
"TargetOffsetMode.onlyEyes": {"chance": 0.4},
"TargetOffsetMode.headAndEyes": {"chance": 0.6}
},
"offsetAngleRange" : {"min": 5.0, "max": 12.0},
"chanceWhileListening" : 0.05,
"chanceWhileTalking" : 0.05
},
"TargetMode.noTarget": {
"joint": undefined,
"stareTimeRange" : {"min": 0.1, "max": 2.0},
"headSpeedRange" : {"min": MAX_LOOKAT_HEAD_MIX_ALPHA, "max": MIN_LOOKAT_HEAD_MIX_ALPHA},
"offsetChances" : {
"TargetOffsetMode.noOffset": {"chance": 0.5},
"TargetOffsetMode.onlyHead": {"chance": 0.0},
"TargetOffsetMode.onlyEyes": {"chance": 0.5},
"TargetOffsetMode.headAndEyes": {"chance": 0.0}
},
"offsetAngleRange" : {"min": 1.0, "max": 15.0},
"chanceWhileListening" : 0.0,
"chanceWhileTalking" : 0.0
}
}
var FOCUS_MODE_CHANCES = {
"idle" : {
"TargetMode.mouth": {"chance": 0.2},
"TargetMode.rightEye": {"chance": 0.4},
"TargetMode.leftEye": {"chance": 0.4}
},
"talking" : ["TargetMode.mouth", "TargetMode.rightEye", "TargetMode.leftEye"] // Equal chances (33.33% each)
}
var LookAction = function() {
var self = this;
this.targetType = TargetType.unknown;
this.speed = MIN_LOOKAT_HEAD_MIX_ALPHA;
this.id = "";
this.focusName = "None";
this.focusChance = 0.0;
this.config = undefined;
this.targetMode = undefined;
this.lookAtJoint = undefined;
this.targetPoint = undefined;
this.elapseTime = 0.0;
this.totalTime = 1.0;
this.eyesHeadOffset = Vec3.ZERO;
this.eyesForward = false;
this.offsetEyes = false;
this.offsetHead = false;
this.offsetModeName = "";
this.offsetChance = 0.0;
this.confortAngle = 0.0;
this.printChance = function(chance) {
return "" + Math.floor(100 * chance) + "%";
}
this.print = function() {
var lines = [];
lines.push(TargetMode.print(eval(self.targetMode)) + " P: " + self.printChance(self.focusChance));
lines.push(TargetOffsetMode.print(eval(self.offsetModeName)) + " P: " + self.printChance(self.offsetChance));
lines.push("Action time: " + self.totalTime.toFixed(2) + " seconds");
return lines;
}
}
var AudienceAvatar = function(id) {
var self = this;
this.id = id;
this.name = "";
this.engaged = true;
this.moved = true;
this.isTalking = false;
this.isListening = false;
this.position = Vec3.ZERO;
this.headPosition = Vec3.ZERO;
this.leftPalmPosition = Vec3.ZERO;
this.rightPalmPosition = Vec3.ZERO;
this.leftHandSpeed = 0.0;
this.rightHandSpeed = 0.0;
this.velocity = 0.0;
this.reactionTime = 0.0;
this.distance = 0.0;
this.loudness = 0.0;
this.talkingTime = 0.0;
}
var SmartLookMachine = function() {
var self = this;
this.myAvatarID = MyAvatar.sessionUUID;
this.nearAvatarList = {};
this.dice = new RandomHelper();
var LOOK_FOR_AVATARS_MAX_DISTANCE = 15.0;
var MIN_FOCUS_TO_LISTENER_TIME = 3.0;
var MAX_FOCUS_TO_LISTENER_TIME = 5.0;
var MIN_FOCUS_TO_TALKER_TIME = 0.5;
var MAX_FOCUS_TO_TALKER_RANGE = 1.5;
var TRIGGER_FOCUS_WHILE_IDLE_CHANCE = 0.1;
this.currentAvatarFocusID = undefined;
this.currentTalker;
this.currentAction = new LookAction();
this.eyesTargetPoint = Vec3.ZERO;
this.headTargetPoint = Vec3.ZERO;
this.timeScale = 1.0;
this.lookAtDebugger = new LookAtDebugger();
this.active = true;
this.headTargetSpeed = 0.0;
this.avatarFocusTotalTime = 0.0;
this.avatarFocusMax = 0.0;
this.lockedFocusID = undefined;
this.visibilityCount = 0;
this.shouldUpdateDebug = false;
var TalkingState = {
"noTalking" : 0,
"meTalkingFirst" : 1,
"meTalkingAgain" : 2,
"otherTalkingFirst" : 3,
"otherTalkingAgain" : 4,
"othersTalking": 5,
"print" : function(sta) {
return ("TalkingState: " + (Object.keys(TalkingState))[sta]);
}
}
this.talkingState = TalkingState.noTalking;
var FocusState = {
"onNobody" : 0,
"onTalker" : 1,
"onRandomAudience" : 2,
"onLastTalker" : 3,
"onRandomLastTalker" : 4,
"onLastFocus" : 5,
"onSelected" : 6,
"onMovement" : 7,
"print" : function(sta) {
return ("FocusState: " + (Object.keys(FocusState))[sta]);
}
}
this.focusState = FocusState.onNobody;
var LockFocusType = {
"none" : 0,
"click" : 1,
"movement" : 2
}
self.lockFocusType = LockFocusType.none;
this.wasMeTalking = false;
this.nearAvatarIDs = [];
this.updateAvatarVisibility = function() {
if (self.nearAvatarIDs.length > 0) {
if (self.nearAvatarList[self.myAvatarID] && self.nearAvatarList[self.myAvatarID].moved) {
for (id in self.nearAvatarList) {
self.nearAvatarList[id].moved = true;
}
self.nearAvatarList[self.myAvatarID].moved = false;
}
self.visibilityCount = ((self.visibilityCount + 1) >= self.nearAvatarIDs.length) ? 0 : self.visibilityCount + 1;
var id = self.nearAvatarIDs[self.visibilityCount];
var avatar = self.nearAvatarList[id];
if (id !== self.myAvatarID && avatar !== undefined && avatar.moved) {
self.nearAvatarList[id].moved = false;
var eyePos = MyAvatar.getDefaultEyePosition();
var avatarSight = Vec3.subtract(avatar.headPosition, eyePos);
var intersection = Entities.findRayIntersection({origin: eyePos, direction: Vec3.normalize(avatarSight)}, true);
self.nearAvatarList[avatar.id].engaged = !intersection.intersects || intersection.distance > Vec3.length(avatarSight);
}
}
}
this.getEngagedAvatars = function() {
var engagedAvatarIDs = [];
for (var id in self.nearAvatarList) {
if (self.nearAvatarList[id].engaged) {
engagedAvatarIDs.push(id);
}
}
return engagedAvatarIDs;
}
this.updateAvatarList = function(deltaTime) {
var TALKING_LOUDNESS_THRESHOLD = 50.0;
var SILENCE_TALK_ATTENUATION = 0.5;
var ATTENTION_HANDS_SPEED = 5.0;
var ATTENTION_AVATAR_SPEED = 2.0;
var MAX_TALKING_TIME = 5.0;
var talkingAvatarID;
var maxLoudness = 0.0;
var count = 0;
var previousTalkers = [];
var fastHands = [];
var fastMovers = [];
var lookupCenter = Vec3.sum(MyAvatar.position, Vec3.multiply(0.8 * LOOK_FOR_AVATARS_MAX_DISTANCE, Quat.getFront(MyAvatar.orientation)));
var nearbyAvatars = AvatarManager.getAvatarsInRange(lookupCenter, LOOK_FOR_AVATARS_MAX_DISTANCE);
for (var n = 0; n < nearbyAvatars.length; n++) {
var avatar = AvatarManager.getAvatar(nearbyAvatars[n]);
var distance = Vec3.distance(MyAvatar.position, avatar.position);
var loudness = avatar.audioLoudness;
loudness = avatar.audioLoudness > 30.0 ? 100.0 : 0.0;
var TALKING_TAU = 0.01;
if (self.nearAvatarList[avatar.sessionUUID] === undefined) {
self.nearAvatarList[avatar.sessionUUID] = new AudienceAvatar(avatar.sessionUUID);
self.nearAvatarList[avatar.sessionUUID].name = avatar.displayName;
} else {
if (Vec3.distance(self.nearAvatarList[avatar.sessionUUID].position, avatar.position) > 0.0) {
self.nearAvatarList[avatar.sessionUUID].moved = true;
} else {
self.nearAvatarList[avatar.sessionUUID].velocity = Vec3.length(avatar.velocity);
if (self.nearAvatarList[avatar.sessionUUID].velocity > 0.0) {
self.nearAvatarList[avatar.sessionUUID].moved = true;
}
}
self.nearAvatarList[avatar.sessionUUID].position = avatar.position;
self.nearAvatarList[avatar.sessionUUID].headPosition = avatar.getJointPosition("Head");
if (self.nearAvatarList[avatar.sessionUUID].engaged) {
self.nearAvatarList[avatar.sessionUUID].loudness = self.nearAvatarList[avatar.sessionUUID].loudness + TALKING_TAU * (loudness - self.nearAvatarList[avatar.sessionUUID].loudness);
self.nearAvatarList[avatar.sessionUUID].orientation = avatar.orientation;
var leftPalmPos = avatar.getJointPosition("LeftHand");
var rightPalmPos = avatar.getJointPosition("RightHand");
var distanceAttenuation = (distance > 1.0) ? (1.0 / distance) : 1.0;
var leftPalmSpeed = distanceAttenuation * Vec3.distance(self.nearAvatarList[avatar.sessionUUID].leftPalmPosition, leftPalmPos) / deltaTime;
var rightPalmSpeed = distanceAttenuation * Vec3.distance(self.nearAvatarList[avatar.sessionUUID].rightPalmPosition, rightPalmPos) / deltaTime;
self.nearAvatarList[avatar.sessionUUID].leftPalmSpeed = leftPalmSpeed;
self.nearAvatarList[avatar.sessionUUID].rightPalmSpeed = rightPalmSpeed;
self.nearAvatarList[avatar.sessionUUID].leftPalmPosition = leftPalmPos;
self.nearAvatarList[avatar.sessionUUID].rightPalmPosition = rightPalmPos;
if (self.nearAvatarList[avatar.sessionUUID] && self.nearAvatarList[avatar.sessionUUID].loudness) {
self.nearAvatarList[avatar.sessionUUID].loudness += 0.1;
}
self.nearAvatarList[avatar.sessionUUID].isTalking = false;
self.nearAvatarList[avatar.sessionUUID].isTalker = false;
if (self.nearAvatarList[avatar.sessionUUID].loudness > TALKING_LOUDNESS_THRESHOLD) {
if (self.nearAvatarList[avatar.sessionUUID].talkingTime < MAX_TALKING_TIME) {
self.nearAvatarList[avatar.sessionUUID].talkingTime += deltaTime;
}
self.nearAvatarList[avatar.sessionUUID].isTalking = true;
count++;
if (maxLoudness < self.nearAvatarList[avatar.sessionUUID].loudness) {
maxLoudness = self.nearAvatarList[avatar.sessionUUID].loudness;
talkingAvatarID = avatar.sessionUUID;
}
} else if (self.nearAvatarList[avatar.sessionUUID].talkingTime > 0.0){
self.nearAvatarList[avatar.sessionUUID].talkingTime -= SILENCE_TALK_ATTENUATION * deltaTime;
if (self.nearAvatarList[avatar.sessionUUID].talkingTime < 0.0) {
self.nearAvatarList[avatar.sessionUUID].talkingTime = 0.0;
}
}
if (!self.nearAvatarList[avatar.sessionUUID].isTalking && self.nearAvatarList[avatar.sessionUUID].talkingTime > 0.0){
previousTalkers.push(avatar.sessionUUID);
}
if ((leftPalmSpeed > ATTENTION_AVATAR_SPEED || rightPalmSpeed > ATTENTION_AVATAR_SPEED) && avatar.sessionUUID !== self.myAvatarID) {
if (!self.nearAvatarList[avatar.sessionUUID].isTalking) {
fastHands.push(avatar.sessionUUID);
} else if (self.nearAvatarList[avatar.sessionUUID].isTalking) {
var headPos = avatar.getJointPosition("Neck");
var raisedHand = Math.max(leftPalmPos.y, rightPalmPos.y) > headPos.y;
if (raisedHand) {
// If the talker raise the hands it will trigger attention
fastHands.push(avatar.sessionUUID);
}
}
}
}
}
}
for (var id in self.nearAvatarList) {
if (nearbyAvatars.indexOf(id) == -1) {
delete self.nearAvatarList[id];
}
}
self.nearAvatarIDs = Object.keys(self.nearAvatarList);
if (self.nearAvatarList[self.myAvatarID] === undefined) {
self.myAvatarID = MyAvatar.sessionUUID;
}
if (talkingAvatarID !== undefined) {
self.nearAvatarList[talkingAvatarID].isTalker = true;
}
self.updateAvatarVisibility();
return { talker: talkingAvatarID, talkingCount: count, previousTalkers: previousTalkers, fastHands: fastHands, fastMovers: fastMovers };
}
this.getHeadConfortAngle = function(point) {
var eyesToPoint = Vec3.subtract(point, MyAvatar.getDefaultEyePosition());
var angle = Vec3.getAngle(eyesToPoint, Quat.getFront(MyAvatar.orientation)) / DEGREE_TO_RADIAN;
angle = Math.min(angle, 90.0);
var MAX_OFFSET_DEGREES = 20.0;
var offsetMultiplier = MAX_OFFSET_DEGREES / 90.0;
var facingRight = Vec3.dot(eyesToPoint, Quat.getRight(MyAvatar.orientation)) > 0.0;
angle = (facingRight ? 1.0 : -1.0) * offsetMultiplier * angle;
return angle;
}
this.updateCurrentAction = function(deltaTime) {
if (self.currentAction.lookAtJoint !== undefined) {
var avatar = AvatarList.getAvatar(self.currentAction.id);
self.currentAction.targetPoint = avatar.getJointPosition(self.currentAction.lookAtJoint);
}
self.currentAction.elapseTime += deltaTime;
}
this.requestNewAction = function(targetType, id) {
var HAND_ATTENTION_TRIGGER_SPEED = 0.2;
var action = new LookAction();
action.targetType = targetType;
action.id = id;
var sortStare = false;
action.targetMode = "TargetMode.noTarget";
if (targetType == TargetType.avatar && id !== undefined && self.nearAvatarList[id] !== undefined) {
var avatar = AvatarList.getAvatar(id);
action.focusName = self.nearAvatarList[id].name;
if (self.nearAvatarList[id].rightPalmSpeed > HAND_ATTENTION_TRIGGER_SPEED || self.nearAvatarList[id].leftPalmSpeed > HAND_ATTENTION_TRIGGER_SPEED) {
if (self.nearAvatarList[id].rightPalmSpeed < self.nearAvatarList[id].leftPalmSpeed) {
action.targetMode = "TargetMode.leftHand";
} else {
action.targetMode = "TargetMode.rightHand";
}
action.focusChance = 1.0;
} else {
var faceChances = self.nearAvatarList[id].isTalking ? FOCUS_MODE_CHANCES.talking : FOCUS_MODE_CHANCES.idle;
var randomFaceTargetMode = self.dice.getRandomKey(faceChances);
action.targetMode = randomFaceTargetMode.randomKey;
action.focusChance = randomFaceTargetMode.chance;
}
} else if (targetType == TargetType.entity) {
// TODO
// Randomize around the entity size
}
var actionConfig = ACTION_CONFIGURATION[action.targetMode];
action.config = actionConfig;
action.lookAtJoint = actionConfig.joint;
action.targetPoint = action.lookAtJoint !== undefined ? avatar.getJointPosition(action.lookAtJoint) : action.targetPoint = MyAvatar.getHeadLookAt();
var randomKeyResult = self.dice.getRandomKey(actionConfig.offsetChances);
action.offsetModeName = randomKeyResult.randomKey;
action.offsetChance = randomKeyResult.chance;
var offsetMode = eval(action.offsetModeName);
if (offsetMode !== TargetOffsetMode.noOffset) {
var headPosition = MyAvatar.getJointPosition("Head");
var headToTarget = Vec3.subtract(action.targetPoint, headPosition);
var randAngle = actionConfig.offsetAngleRange.min + Math.random() * (actionConfig.offsetAngleRange.max - actionConfig.offsetAngleRange.min);
var randAngle = Math.random() < 0.5 ? -randAngle : randAngle;
if (self.nearAvatarList[self.myAvatarID]) {
var randOffsetRotation = Quat.angleAxis(randAngle, Vec3.UNIT_Y);
action.eyesHeadOffset = Vec3.subtract(Vec3.sum(headPosition, Vec3.multiplyQbyV(randOffsetRotation, headToTarget)), action.targetPoint);
}
action.offsetEyes = offsetMode === TargetOffsetMode.onlyEyes || offsetMode === TargetOffsetMode.headAndEyes;
action.offsetHead = offsetMode === TargetOffsetMode.onlyHead || offsetMode === TargetOffsetMode.headAndEyes;
}
action.totalTime = actionConfig.stareTimeRange.min + Math.random() * (actionConfig.stareTimeRange.max - actionConfig.stareTimeRange.min);
action.confortAngle = self.getHeadConfortAngle(action.targetPoint);
action.speed = actionConfig.headSpeedRange.min + Math.random() * (actionConfig.headSpeedRange.max - actionConfig.headSpeedRange.min);
return action;
}
this.findAudienceAvatar = function(avatarIDs) {
// We look for avatars on the avatarIDs array if provided
// If not avatarIDs becomes the array with all the engaged avatars nearAvatarList
if (avatarIDs === undefined) {
avatarIDs = self.nearAvatarIDs;
}
var randAvatarID;
var MAX_AUDIENCE_DISTANCE = 8;
var firstAnyOther = undefined;
var firstNearOther = undefined;
if (avatarIDs.length > 1) {
var randomIndexes = self.dice.createRandomIndexes(avatarIDs.length);
for (var n = 0; n < randomIndexes.length; n++) {
var avatarID = avatarIDs[randomIndexes[n]];
var avatar = self.nearAvatarList[avatarID];
if (avatarID != self.myAvatarID && avatar.engaged) {
firstAnyOther = !firstAnyOther ? avatarID : firstAnyOther;
if (avatar.distance < MAX_AUDIENCE_DISTANCE) {
firstNearOther = !firstNearOther ? avatarID : firstNearOther;
var otherToMe = Vec3.normalize(Vec3.subtract(self.nearAvatarList[self.myAvatarID].position, avatar.position));
var myFront = Quat.getFront(self.nearAvatarList[self.myAvatarID].orientation);
var otherFront = Quat.getFront(avatar.orientation);
if (Vec3.dot(otherToMe, otherFront) > 0.0 && Vec3.dot(myFront, otherToMe) < 0.0) {
randAvatarID = avatarID;
break;
}
}
if (n === randomIndexes.length - 1) {
// We have not found a valid candidate facing us
// return the first id different from out avatar's id
randAvatarID = firstNearOther !== undefined ? firstNearOther : firstAnyOther;
}
}
}
} else if (avatarIDs.length > 0 && avatarIDs[0] != self.myAvatarID && self.nearAvatarList[avatarIDs[0]].engaged){
// If the array provided only has one ID
randAvatarID = avatarIDs[0];
}
return randAvatarID;
}
this.applyHeadOffset = function(point, angle) {
var eyesToPoint = Vec3.subtract(point, MyAvatar.getDefaultEyePosition());
var offsetRot = Quat.angleAxis(angle, Quat.getUp(MyAvatar.orientation));
var offsetPoint = Vec3.sum(MyAvatar.getDefaultEyePosition(), Vec3.multiplyQbyV(offsetRot, eyesToPoint));
return offsetPoint;
}
this.computeTalkingState = function(sceneData, myAvatarID, currentTalker, currentFocus) {
var talkingState = TalkingState.noTalking;
if (sceneData.talker === myAvatarID) {
if (currentTalker != myAvatarID) {
talkingState = TalkingState.meTalkingFirst;
} else {
talkingState = TalkingState.meTalkingAgain;
}
} else if (sceneData.talkingCount > 1) {
talkingState = TalkingState.othersTalking;
} else if (sceneData.talkingCount > 0) {
if (sceneData.talker !== currentFocus) {
talkingState = TalkingState.otherTalkingFirst;
} else {
talkingState = TalkingState.otherTalkingAgain;
}
}
return talkingState;
}
this.computeFocusState = function(sceneData, talkingState, currentFocus, lockedFocus, lockType) {
var focusState = FocusState.onNobody;
switch (talkingState) {
case TalkingState.noTalking : {
if (sceneData.previousTalkers.length > 0) {
focusState = FocusState.onLastTalker;
} else if (Math.random() < TRIGGER_FOCUS_WHILE_IDLE_CHANCE) {
// There is chance of triggering a random focus when nobody is talking
focusState = FocusState.onRandomAudience;
} else {
focusState = FocusState.onNobody;
}
break;
}
case TalkingState.meTalkingFirst : {
if (currentFocus !== undefined) {
// Look at the last focused avatar
focusState = FocusState.onLastFocus;
} else if (sceneData.previousTalkers.length > 0) {
// Look at one of the previous talkers
focusState = FocusState.onRandomLastTalker;
} else {
focusState = FocusState.onRandomAudience;
}
break;
}
case TalkingState.meTalkingAgain : {
// Look at any random avatar
focusState = FocusState.onRandomAudience;
break;
}
case TalkingState.otherTalkingAgain : {
// If we were focused already on the talker we have a 15% chance to look at somebody else
// randomly giving preference to the previous talkers
if (Math.random() < 0.15) {
focusState = FocusState.onRandomLastTalker;
} else {
focusState = FocusState.onTalker;
}
break;
}
case TalkingState.otherTalkingFirst : {
// Focus on the new talker
focusState = FocusState.onTalker;
break;
}
case TalkingState.othersTalking : {
// When multiple people talk at the same time we have a 50% chance of not changing focus
if (Math.random() < 0.5) {
focusState = FocusState.onLastFocus;
} else {
focusState = FocusState.onTalker;
}
break;
}
}
if (lockedFocus !== undefined) {
if (lockType === LockFocusType.click) {
focusState = FocusState.onSelected;
} else if (lockType === LockFocusType.movement) {
focusState = FocusState.onMovement;
}
}
return focusState;
}
this.computeAvatarFocus = function(sceneData, focusState, currentFocus, lockedFocus) {
var avatarFocusID = undefined;
switch (focusState) {
case FocusState.onTalker: {
avatarFocusID = sceneData.talker;
break;
}
case FocusState.onRandomAudience: {
avatarFocusID = self.findAudienceAvatar();
break;
}
case FocusState.onLastTalker:
case FocusState.onRandomLastTalker: {
if (sceneData.previousTalkers.length > 0) {
avatarFocusID = self.findAudienceAvatar(sceneData.previousTalkers);
}
if (avatarFocusID === undefined) {
// Guarantee a 20% chance of looking at somebody
if (focusState === FocusState.onRandomLastTalker || Math.random() < 0.2) {
avatarFocusID = self.findAudienceAvatar();
}
}
break;
}
case FocusState.onLastFocus: {
avatarFocusID = currentFocus;
break;
}
case FocusState.onMovement:
case FocusState.onSelected: {
avatarFocusID = lockedFocus;
break;
}
}
return avatarFocusID;
}
this.forceFocus = function(avatarID) {
if (self.nearAvatarList[avatarID] !== undefined) {
self.lockedFocusID = avatarID;
self.lockFocusType = LockFocusType.click;
}
}
this.logAction = function(action) {
self.lookAtDebugger.clearLog();
self.lookAtDebugger.log(TalkingState.print(self.talkingState));
self.lookAtDebugger.log("________________________Focus");
self.lookAtDebugger.log("On avatar: " + action.focusName);
self.lookAtDebugger.log(FocusState.print(self.focusState));
self.lookAtDebugger.log("Focus time: " + self.avatarFocusMax.toFixed(2) + " seconds");
self.lookAtDebugger.log("________________________Action");
var extraLogs = action.print();
for (var n = 0; n < extraLogs.length; n++) {
self.lookAtDebugger.log(extraLogs[n]);
}
}
this.update = function(deltaTime) {
var FPS = 60.0;
var CLICKED_AVATAR_MAX_FOCUS_TIME = 10.0;
self.timeScale = deltaTime * FPS;
var sceneData = self.updateAvatarList(deltaTime);
if (self.nearAvatarIDs.length === 0) {
return;
}
var abortAction = self.lockFocusType === LockFocusType.click;
// Focus on any avatar moving their hands
if (sceneData.fastHands.length > 0 && self.lockFocusType === LockFocusType.none) {
var randomFastHands = self.findAudienceAvatar(sceneData.fastHands);
if (self.nearAvatarList[randomFastHands] && (!self.nearAvatarList[randomFastHands].isTalking || self.nearAvatarList[randomFastHands].isTalker)) {
abortAction = Math.random() < 0.3;
self.lockedFocusID = randomFastHands;
self.lockFocusType = LockFocusType.movement;
}
} else if (self.avatarFocusTotalTime >= self.avatarFocusMax) {
self.lockFocusType = LockFocusType.none;
}
// Set the talking status
self.talkingState = self.computeTalkingState(sceneData, self.myAvatarID, self.currentTalker, self.currentAvatarFocusID);
// If the talker change, we have a 50% chance to focus on them once the next action is completed.
var otherTalkerTriggerRefocus = self.talkingState === TalkingState.otherTalkingFirst && Math.random() < 0.5;
if (self.lockedFocusID !== undefined || otherTalkerTriggerRefocus) {
// Force a new focus
self.avatarFocusTotalTime = self.avatarFocusMax;
if (abortAction) {
// Force a new action
self.currentAction.elapseTime = self.currentAction.totalTime;
}
}
var needsNewFocus = self.avatarFocusTotalTime >= self.avatarFocusMax;
var needsNewAction = self.currentAction.elapseTime >= self.currentAction.totalTime;
var newAvatarFocusID = self.currentAvatarFocusID;
if (needsNewAction && needsNewFocus) {
self.focusState = self.computeFocusState(sceneData, self.talkingState, self.currentAvatarFocusID, self.lockedFocusID, self.lockFocusType);
newAvatarFocusID = self.computeAvatarFocus(sceneData, self.focusState, self.currentAvatarFocusID, self.lockedFocusID);
self.lockedFocusID = undefined;
if (self.lockFocusType !== LockFocusType.click) {
if (self.talkingState === TalkingState.meTalkingAgain) {
self.avatarFocusMax = MIN_FOCUS_TO_TALKER_TIME + Math.random() * MAX_FOCUS_TO_TALKER_RANGE;
} else {
self.avatarFocusMax = MIN_FOCUS_TO_LISTENER_TIME + Math.random() * MAX_FOCUS_TO_LISTENER_TIME;
}
} else {
self.avatarFocusMax = CLICKED_AVATAR_MAX_FOCUS_TIME;
}
self.avatarFocusTotalTime = 0.0;
self.currentTalker = sceneData.talker;
self.shouldUpdateDebug = true;
} else {
self.avatarFocusTotalTime += deltaTime;
}
if (needsNewAction) {
var currentFocus = newAvatarFocusID;
if (otherTalkerTriggerRefocus) {
// Reset the last action on the previous focus to provide a random delay
currentFocus = self.currentAction.id;
self.currentAction.elapseTime = 0.0;
} else {
// Create a new action
self.currentAction = self.requestNewAction(TargetType.avatar, newAvatarFocusID);
}
if (self.currentAvatarFocusID !== newAvatarFocusID) {
self.currentAvatarFocusID = newAvatarFocusID;
}
if (self.currentAvatarFocusID === undefined ||
self.talkingState === TalkingState.meTalkingAgain ||
self.talkingState === TalkingState.meTalkingFirst) {
// Minimize the head speed when we are talking or looking nowhere
self.currentAction.speed = MIN_LOOKAT_HEAD_MIX_ALPHA;
}
self.logAction(self.currentAction);
} else {
self.updateCurrentAction(deltaTime);
}
self.headTargetPoint = self.currentAction.targetPoint;
self.eyesTargetPoint = self.currentAction.targetPoint;
if (self.currentAction.offsetHead) {
self.headTargetPoint = Vec3.sum(self.headTargetPoint, self.currentAction.eyesHeadOffset);
}
self.headTargetPoint = self.applyHeadOffset(self.headTargetPoint, self.currentAction.confortAngle);
if (self.currentAction.offsetEyes) {
self.eyesTargetPoint = Vec3.sum(self.eyesTargetPoint, self.currentAction.eyesHeadOffset);
}
self.headTargetSpeed = Math.min(1.0, self.currentAction.speed * self.timeScale);
}
this.getResults = function() {
return {
"eyesTarget" : self.eyesTargetPoint,
"headTarget" : self.headTargetPoint,
"headSpeed" : self.headTargetSpeed
}
}
}
//////////////////////////////////////////////
// autoLook.js ///////
//////////////////////////////////////////////
var LookAtController = function() {
var CAMERA_HEAD_MIX_ALPHA = 0.06;
var LookState = {
"CameraLookActivating": 0,
"CameraLookActive":1,
"ClickToLookDeactivating":2,
"ClickToLookActive":3,
"AutomaticLook": 4
}
var self = this;
this.smartLookAt = new SmartLookMachine();
this.currentState = LookState.AutomaticLook;
this.lookAtTarget = undefined;
this.lookingAtAvatarID = undefined;
this.lookingAvatarJointIndex = undefined;
this.interpolatedHeadLookAt = MyAvatar.getHeadLookAt();
var CLICK_TO_LOOK_TOTAL_TIME = 5.0;
this.clickToLookTimer = 0;
var CAMERA_LOOK_TOTAL_TIME = 5.0;
this.cameraLookTimer = 0;
this.timeScale = 1.0;
this.eyesTarget = Vec3.ZERO;
this.headTarget = Vec3.ZERO;
this.mousePressEvent = function(event) {
if (event.isLeftButton) {
self.smartLookAt.lookAtDebugger.onClick(event);
if (self.currentState === LookState.AutomaticLook) {
var pickRay = Camera.computePickRay(event.x, event.y);
var intersection = AvatarManager.findRayIntersection({origin: pickRay.origin, direction: pickRay.direction}, [], [self.smartLookAt.myAvatarID], false);
self.lookingAtAvatarID = intersection.intersects ? intersection.avatarID : undefined;
if (self.lookingAtAvatarID) {
self.smartLookAt.forceFocus(self.lookingAtAvatarID);
}
}
}
}
this.mouseMoveEvent = function(event) {
if (event.isRightButton && self.currentState === LookState.AutomaticLook) {
self.currentState = LookState.CameraLookActivating;
self.cameraLookTimer = 0.0;
}
}
this.updateHeadLookAtTarget = function(target, interpolatedTarget, speed, noPitch) {
var eyesPosition = MyAvatar.getDefaultEyePosition();
var targetRot = Quat.lookAt(eyesPosition, target, Quat.getUp(MyAvatar.orientation));
var interpolatedRot = Quat.lookAt(eyesPosition, interpolatedTarget, Quat.getUp(MyAvatar.orientation));
var newInterpolatedRot = Quat.mix(interpolatedRot, targetRot, speed);
var newInterpolatedTarget = Vec3.sum(eyesPosition, Vec3.multiply(Vec3.distance(eyesPosition, target), Quat.getFront(newInterpolatedRot)));
// avoid pitch
if (noPitch) {
newInterpolatedTarget.y = eyesPosition.y;
}
MyAvatar.setHeadLookAt(newInterpolatedTarget);
return newInterpolatedTarget;
}
this.retargetHeadTo = function(target, speed, noPitch, tolerance) {
var eyePos = MyAvatar.getDefaultEyePosition();
var localTarget = Vec3.normalize(Vec3.subtract(target, eyePos));
self.interpolatedHeadLookAt = self.updateHeadLookAtTarget(target, self.interpolatedHeadLookAt, speed, noPitch);
return (Vec3.dot(Vec3.normalize(Vec3.subtract(self.interpolatedHeadLookAt, eyePos)), Vec3.normalize(localTarget)) > tolerance);
}
var MAX_INTERPOLING_STEPS = 100;
this.interpolatingSteps = 0.0;
this.automaticResults = {};
this.update = function(deltaTime) {
// Update timeScale
var FPS = 60.0;
self.timeScale = deltaTime * FPS;
var stateTransitSpeed = Math.min(1.0, CAMERA_HEAD_MIX_ALPHA * self.timeScale);
var CLICK_RETARGET_TOLERANCE = 0.98;
var CAMERA_RETARGET_TOLERANCE = 0.98;
var headTarget = MyAvatar.getHeadLookAt();
var eyesTarget = MyAvatar.getEyesLookAt();
var isTargetValid = (self.lookAtTarget || self.lookingAtAvatarID);
if (isTargetValid && self.currentState === LookState.ClickToLookActive) {
if (self.lookingAtAvatarID) {
self.lookAtTarget = AvatarList.getAvatar(self.lookingAtAvatarID).getJointPosition(self.lookingAvatarJointIndex);
}
self.interpolatedHeadLookAt = self.updateHeadLookAtTarget(self.lookAtTarget, self.interpolatedHeadLookAt, stateTransitSpeed);
MyAvatar.setEyesLookAt(self.lookAtTarget);
if (self.clickToLookTimer > CLICK_TO_LOOK_TOTAL_TIME) {
self.currentState = LookState.ClickToLookDeactivating;
}
self.clickToLookTimer += deltaTime;
} else if (self.currentState === LookState.ClickToLookDeactivating) {
var breakInterpolation = self.interpolatingSteps > MAX_INTERPOLING_STEPS;
if (breakInterpolation || self.retargetHeadTo(self.automaticResults.headTarget, stateTransitSpeed, false, CLICK_RETARGET_TOLERANCE)) {
self.currentState = LookState.AutomaticLook;
self.interpolatingSteps = 0.0;
}
if (breakInterpolation) {
console.log("Breaking look at interpolation");
} else if (self.interpolatingSteps++ === 0.0){
console.log("Interpolating click");
}
} else if (self.currentState === LookState.CameraLookActivating) {
var cameraFront = Quat.getFront(Camera.getOrientation());
if (Camera.mode === "selfie") {
cameraFront = Vec3.multiply(-1, cameraFront);
}
var breakInterpolation = self.interpolatingSteps > MAX_INTERPOLING_STEPS;
if (breakInterpolation || self.retargetHeadTo(Vec3.sum(MyAvatar.getDefaultEyePosition(), cameraFront), stateTransitSpeed, true, CAMERA_RETARGET_TOLERANCE)) {
self.currentState = LookState.CameraLookActive;
self.interpolatingSteps = 0.0;
MyAvatar.releaseHeadLookAtControl();
MyAvatar.releaseEyesLookAtControl();
}
if (breakInterpolation) {
console.log("Breaking camera interpolation");
} else if (self.interpolatingSteps++ === 0.0){
console.log("Interpolating camera");
}
} else if (self.currentState === LookState.CameraLookActive) {
if (self.cameraLookTimer > CAMERA_LOOK_TOTAL_TIME) {
// Set as initial target the current head look at
self.interpolatedHeadLookAt = MyAvatar.getHeadLookAt();
self.currentState = LookState.AutomaticLook;
}
self.cameraLookTimer += deltaTime;
} else if (self.currentState === LookState.AutomaticLook) {
var updateLookat = Vec3.length(MyAvatar.velocity) < 1.0;
self.smartLookAt.update(deltaTime);
if (updateLookat) {
self.automaticResults = self.smartLookAt.getResults();
self.headTarget = self.automaticResults.headTarget;
self.eyesTarget = self.automaticResults.eyesTarget;
self.interpolatedHeadLookAt = self.updateHeadLookAtTarget(self.headTarget, self.interpolatedHeadLookAt, self.automaticResults.headSpeed, true);
MyAvatar.setEyesLookAt(self.eyesTarget);
headTarget = self.interpolatedHeadLookAt;
eyesTarget = self.eyesTarget;
} else {
// Too fast. Stand by automatic look
MyAvatar.setEyesLookAt(MyAvatar.getEyesLookAt());
MyAvatar.setHeadLookAt(MyAvatar.getHeadLookAt());
}
}
if (self.smartLookAt.shouldUpdateDebug) {
// Update engaged avatars debugging when a new action is created
var engagedAvatars = self.smartLookAt.getEngagedAvatars();
self.smartLookAt.lookAtDebugger.highLightAvatars(engagedAvatars,
self.smartLookAt.currentAction.id,
self.smartLookAt.currentTalker !== self.smartLookAt.currentAction.id ? self.smartLookAt.currentTalker : undefined);
}
self.smartLookAt.lookAtDebugger.showTarget(headTarget, eyesTarget);
}
this.finish = function() {
self.smartLookAt.lookAtDebugger.finish();
}
}
var lookAtController = new LookAtController();
Controller.mousePressEvent.connect(lookAtController.mousePressEvent);
Controller.mouseMoveEvent.connect(lookAtController.mouseMoveEvent);
Script.update.connect(lookAtController.update);
Script.scriptEnding.connect(lookAtController.finish);
})();