mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-08 14:42:09 +02:00
CP
This commit is contained in:
parent
c67eafffba
commit
21903b746c
6 changed files with 1990 additions and 1665 deletions
0
examples/baseballEntityScript.js
Normal file
0
examples/baseballEntityScript.js
Normal file
File diff suppressed because it is too large
Load diff
323
examples/map.js~
Normal file
323
examples/map.js~
Normal file
|
@ -0,0 +1,323 @@
|
|||
Script.include("entityManager.js");
|
||||
Script.include("overlayManager.js");
|
||||
|
||||
|
||||
// Poll for nearby map data
|
||||
|
||||
var entityManager = new EntityManager();
|
||||
|
||||
// From http://evanw.github.io/lightgl.js/docs/raytracer.html
|
||||
function raySphereIntersection(origin, ray, center, radius) {
|
||||
var offset = Vec3.subtract(origin, center);
|
||||
var a = Vec3.dot(ray, ray);
|
||||
// var a = ray.dot(ray);
|
||||
var b = 2 * Vec3.dot(ray, offset);
|
||||
// var b = 2 * ray.dot(offset);
|
||||
var c = Vec3.dot(offset, offset) - radius * radius;
|
||||
// var c = offset.dot(offset) - radius * radius;
|
||||
var discriminant = b * b - 4 * a * c;
|
||||
|
||||
if (discriminant > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
Map = function(data) {
|
||||
var visible = false;
|
||||
|
||||
var ROOT_OFFSET = Vec3.multiply(0.3, Quat.getFront(MyAvatar.orientation));
|
||||
var ROOT_POSITION = Vec3.sum(MyAvatar.position, ROOT_OFFSET);
|
||||
|
||||
var ROOT_SCALE = 0.0005;
|
||||
|
||||
// Create object in objectManager
|
||||
var rootObject = entityManager.addBare();
|
||||
var position = ROOT_POSITION;
|
||||
rootObject.position = ROOT_POSITION;
|
||||
rootObject.scale = ROOT_SCALE
|
||||
Vec3.print("Position:", position);
|
||||
|
||||
// Search for all nearby objects that have the userData "mapped"
|
||||
// TODO Update to use the zone's bounds
|
||||
var entities = Entities.findEntities(MyAvatar.position, 32000);
|
||||
var mappedEntities = [];
|
||||
var minCorner = {
|
||||
x: 4294967295,
|
||||
y: 4294967295,
|
||||
z: 4294967295,
|
||||
};
|
||||
var maxCorner = {
|
||||
x: -4294967295,
|
||||
y: -4294967295,
|
||||
z: -4294967295,
|
||||
};
|
||||
|
||||
for (var i = 0; i < entities.length; ++i) {
|
||||
var entityID = entities[i];
|
||||
var properties = Entities.getEntityProperties(entityID);
|
||||
if (properties.userData == "mapped" || properties.userData == "tracked") {
|
||||
|
||||
print("Found: ", properties.name);
|
||||
|
||||
minCorner.x = Math.min(minCorner.x, properties.position.x - (properties.dimensions.x / 2));
|
||||
minCorner.y = Math.min(minCorner.y, properties.position.y - (properties.dimensions.y / 2));
|
||||
minCorner.z = Math.min(minCorner.z, properties.position.z - (properties.dimensions.z / 2));
|
||||
|
||||
maxCorner.x = Math.max(maxCorner.x, properties.position.x - (properties.dimensions.x / 2));
|
||||
maxCorner.y = Math.max(maxCorner.y, properties.position.y - (properties.dimensions.y / 2));
|
||||
maxCorner.z = Math.max(maxCorner.z, properties.position.z - (properties.dimensions.z / 2));
|
||||
|
||||
}
|
||||
// if (properties.userData == "mapped") {
|
||||
// properties.visible = false;
|
||||
// var entity = entityManager.add(properties.type, properties);
|
||||
// mappedEntities.push(entity);
|
||||
// } else if (properties.userData == "tracked") {
|
||||
// // TODO implement tracking of objects
|
||||
// }
|
||||
}
|
||||
|
||||
var dimensions = {
|
||||
x: maxCorner.x - minCorner.x,
|
||||
y: maxCorner.y - minCorner.y,
|
||||
z: maxCorner.z - minCorner.z,
|
||||
};
|
||||
Vec3.print("dims", dimensions);
|
||||
|
||||
var center = {
|
||||
x: minCorner.x + (dimensions.x / 2),
|
||||
y: minCorner.y + (dimensions.y / 2),
|
||||
z: minCorner.z + (dimensions.z / 2),
|
||||
};
|
||||
Vec3.print("center", center);
|
||||
|
||||
var trackedEntities = [];
|
||||
var waypointEntities = [];
|
||||
for (var i = 0; i < entities.length; ++i) {
|
||||
var entityID = entities[i];
|
||||
var properties = Entities.getEntityProperties(entityID);
|
||||
var mapData = null;
|
||||
try {
|
||||
var data = JSON.parse(properties.userData.replace(/(\r\n|\n|\r)/gm,""));
|
||||
mapData = data.mapData;
|
||||
} catch (e) {
|
||||
print("Caught: ", properties.name);
|
||||
}
|
||||
|
||||
if (mapData) {
|
||||
print("Creating copy of", properties.name);
|
||||
properties.name += " (COPY)";
|
||||
properties.userData = "";
|
||||
properties.visible = true;
|
||||
var position = properties.position;
|
||||
properties.position = Vec3.subtract(properties.position, center);
|
||||
properties.position = Vec3.multiply(properties.position, ROOT_SCALE);
|
||||
var extra = { };
|
||||
|
||||
if (mapData.track) {
|
||||
extra.trackingEntityID= entityID;
|
||||
trackedEntities.push(entity);
|
||||
rootObject.addChild(entity);
|
||||
}
|
||||
if (mapData.waypoint) {
|
||||
print("Waypoint: ", mapData.waypoint.name);
|
||||
// properties.type = "Model";
|
||||
// properties.modelURL = "atp:ca49a13938376b3eb68d7b2b9189afb3f580c07b6950ea9e65b5260787204145.fbx";
|
||||
extra.waypoint = mapData.waypoint;
|
||||
extra.waypoint.position = position;
|
||||
}
|
||||
|
||||
var entity = entityManager.add(properties.type, properties);
|
||||
entity.__extra__ = extra;
|
||||
if (mapData.waypoint) {
|
||||
waypointEntities.push(entity);
|
||||
}
|
||||
mappedEntities.push(entity);
|
||||
|
||||
rootObject.addChild(entity);
|
||||
|
||||
} else {
|
||||
// print("Not creating copy of", properties.name);
|
||||
}
|
||||
}
|
||||
|
||||
var avatarArrowEntity = entityManager.add("Model", {
|
||||
name: "You Are Here",
|
||||
modelURL: "atp:ce4f0c4e491e40b73d28f2646da4f676fe9ea364cf5f1bf5615522ef6acfd80e.fbx",
|
||||
position: Vec3.multiply(Vec3.subtract(MyAvatar.position, center), ROOT_SCALE),
|
||||
dimensions: { x: 30, y: 100, z: 100 },
|
||||
});
|
||||
rootObject.addChild(avatarArrowEntity);
|
||||
|
||||
this.isVisible = function() {
|
||||
return visible;
|
||||
}
|
||||
|
||||
Controller.mousePressEvent.connect(mousePressEvent);
|
||||
function mousePressEvent(event) {
|
||||
// Entities.setZonesArePickable(false);
|
||||
|
||||
var pickRay = Camera.computePickRay(event.x, event.y);
|
||||
for (var i = 0; i < waypointEntities.length; ++i) {
|
||||
var entity = waypointEntities[i];
|
||||
print("Checkit for hit", entity.__extra__.waypoint.name);
|
||||
var result = raySphereIntersection(pickRay.origin, pickRay.direction, entity.worldPosition, 0.1);//entity.worldScale);
|
||||
if (result) {
|
||||
print("Pressed entity: ", entity.id);
|
||||
print("Pressed waypoint: ", entity.__extra__.waypoint.name);
|
||||
print("Teleporting...");
|
||||
MyAvatar.position = entity.__extra__.waypoint.position;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// var result = Entities.findRayIntersection(pickRay, false);
|
||||
// if (result.intersects) {
|
||||
// var entity = entityManager.get(result.entityID);
|
||||
// if (entity) {
|
||||
// print("Pressed entity: ", entity.id);
|
||||
// }
|
||||
// if (entity && entity.__extra__.waypoint) {
|
||||
// print("Pressed waypoint: ", entity.__extra__.waypoint.name);
|
||||
// print("Teleporting...");
|
||||
// MyAvatar.position = entity.__extra__.waypoint.position;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Entities.setZonesArePickable(true);
|
||||
};
|
||||
|
||||
var time = 0;
|
||||
Script.update.connect(function(dt) {
|
||||
time += dt;
|
||||
// Update tracked entities
|
||||
for (var i = 0; i < trackedEntities.length; ++i) {
|
||||
entity = trackedEntities[i];
|
||||
var entityID = entity.__extra__.trackingEntityID;
|
||||
var properties = Entities.getEntityProperties(entityID);
|
||||
properties.position = Vec3.subtract(properties.position, center);
|
||||
properties.position = Vec3.multiply(properties.position, ROOT_SCALE);
|
||||
entity.position = properties.position;
|
||||
}
|
||||
|
||||
|
||||
var position = Vec3.subtract(MyAvatar.position, center)
|
||||
position.y += 60 + (Math.sin(time) * 10);
|
||||
position = Vec3.multiply(position, ROOT_SCALE);
|
||||
avatarArrowEntity.position = position;
|
||||
// Vec3.print("Position:", avatarArrowEntity.position);
|
||||
|
||||
// rootObject.position = Vec3.sum(position, { x: 0, y: Math.sin(time) / 30, z: 0 });
|
||||
//var ROOT_OFFSET = Vec3.multiply(0.3, Quat.getFront(MyAvatar.orientation));
|
||||
//var ROOT_POSITION = Vec3.sum(MyAvatar.position, ROOT_OFFSET);
|
||||
// position = ROOT_POSITION;
|
||||
rootObject.position = ROOT_POSITION;
|
||||
entityManager.update();
|
||||
|
||||
// Update waypoint highlights
|
||||
var pickRay = Camera.computePickRay(event.x, event.y);
|
||||
for (var i = 0; i < waypointEntities.length; ++i) {
|
||||
var entity = waypointEntities[i];
|
||||
print("Checkit for hit", entity.__extra__.waypoint.name);
|
||||
var result = raySphereIntersection(pickRay.origin, pickRay.direction, entity.worldPosition, 0.1);//entity.worldScale);
|
||||
if (result) {
|
||||
print("Pressed entity: ", entity.id);
|
||||
print("Pressed waypoint: ", entity.__extra__.waypoint.name);
|
||||
print("Teleporting...");
|
||||
MyAvatar.position = entity.__extra__.waypoint.position;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function setVisible(newValue) {
|
||||
if (visible != newValue) {
|
||||
visible = newValue;
|
||||
|
||||
if (visible) {
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.show = function() {
|
||||
setVisible(true);
|
||||
}
|
||||
|
||||
this.hide = function() {
|
||||
setVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
var map = null;
|
||||
map = Map(mapData);
|
||||
|
||||
// On press key
|
||||
Controller.keyPressEvent.connect(function(event) {
|
||||
if (event.text == "m") {
|
||||
if (!map) {
|
||||
map = Map(mapData);
|
||||
}
|
||||
|
||||
map.show();
|
||||
print("MAP!");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var mapData = {
|
||||
config: {
|
||||
// World dimensions that the minimap maps to
|
||||
worldDimensions: {
|
||||
x: 10.0,
|
||||
y: 10.0,
|
||||
z: 10.0,
|
||||
},
|
||||
// The center of the map should map to this location in the center of the area
|
||||
worldCenter: {
|
||||
x: 5.0,
|
||||
y: 5.0,
|
||||
z: 5.0,
|
||||
},
|
||||
// Map dimensions
|
||||
mapDimensions: {
|
||||
x: 10.0,
|
||||
y: 10.0,
|
||||
z: 10.0,
|
||||
},
|
||||
|
||||
// Can this be automated? Tag entities that should be included? Store in UserData?
|
||||
objects: [
|
||||
{
|
||||
type: "Model",
|
||||
modelURL: "https://hifi-public.s3.amazonaws.com/ozan/sets/huffman_set/huffman_set.fbx",
|
||||
},
|
||||
],
|
||||
},
|
||||
waypoints: [
|
||||
{
|
||||
name: "Forest's Edge",
|
||||
position: {
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
// entityManager = new OverlayManager();
|
||||
// entityManager = new EntityManager();
|
||||
//
|
||||
// var rootEntity = entityManager.addBare();
|
||||
//
|
||||
// var time = 0;
|
||||
//
|
||||
//
|
||||
// rootEntity.scale = 0.1;
|
||||
// Script.include("sfData.js");
|
||||
// rootEntity.addChild(entity);
|
||||
entityManager.update();
|
|
@ -142,6 +142,7 @@ function createBaseball(position, velocity, ballScale) {
|
|||
|
||||
var buildBaseballHitCallback = function(entityID) {
|
||||
var f = function(entityA, entityB, collision) {
|
||||
print("Got baseball hit callback");
|
||||
var properties = Entities.getEntityProperties(entityID, ['position', 'velocity']);
|
||||
var line = new InfiniteLine(properties.position, { red: 255, green: 128, blue: 89 });
|
||||
var lastPosition = properties.position;
|
||||
|
@ -154,11 +155,12 @@ var buildBaseballHitCallback = function(entityID) {
|
|||
lastPosition = properties.position;
|
||||
}
|
||||
}, 50);
|
||||
var speed = Vec3.length(properties.velocity);
|
||||
Entities.editEntity(entityID, {
|
||||
velocity: Vec3.multiply(3, properties.velocity),
|
||||
velocity: Vec3.multiply(2, properties.velocity),
|
||||
gravity: {
|
||||
x: 0,
|
||||
y: -2.8,
|
||||
y: -9.8,
|
||||
z: 0
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,269 +1,269 @@
|
|||
//
|
||||
// flashlight.js
|
||||
//
|
||||
// Script Type: Entity
|
||||
//
|
||||
// Created by Sam Gateau on 9/9/15.
|
||||
// Additions by James B. Pollack @imgntn on 9/21/2015
|
||||
// Copyright 2015 High Fidelity, Inc.
|
||||
//
|
||||
// This is a toy script that can be added to the Flashlight model entity:
|
||||
// "https://hifi-public.s3.amazonaws.com/models/props/flashlight.fbx"
|
||||
// that creates a spotlight attached with the flashlight model while the entity is grabbed
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
/*global MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, randFloat, randInt */
|
||||
(function() {
|
||||
|
||||
Script.include("../../libraries/utils.js");
|
||||
|
||||
var ON_SOUND_URL = 'http://hifi-public.s3.amazonaws.com/sounds/Switches%20and%20sliders/flashlight_on.wav';
|
||||
var OFF_SOUND_URL = 'http://hifi-public.s3.amazonaws.com/sounds/Switches%20and%20sliders/flashlight_off.wav';
|
||||
|
||||
//we are creating lights that we don't want to get stranded so lets make sure that we can get rid of them
|
||||
var startTime = Date.now();
|
||||
//if you're going to be using this in a dungeon or something and holding it for a long time, increase this lifetime value.
|
||||
var LIFETIME = 25;
|
||||
var MSEC_PER_SEC = 1000.0;
|
||||
|
||||
// this is the "constructor" for the entity as a JS object we don't do much here, but we do want to remember
|
||||
// our this object, so we can access it in cases where we're called without a this (like in the case of various global signals)
|
||||
function Flashlight() {
|
||||
return;
|
||||
}
|
||||
|
||||
//if the trigger value goes below this while held, the flashlight will turn off. if it goes above, it will
|
||||
var DISABLE_LIGHT_THRESHOLD = 0.7;
|
||||
|
||||
// These constants define the Spotlight position and orientation relative to the model
|
||||
var MODEL_LIGHT_POSITION = {
|
||||
x: 0,
|
||||
y: -0.3,
|
||||
z: 0
|
||||
};
|
||||
var MODEL_LIGHT_ROTATION = Quat.angleAxis(-90, {
|
||||
x: 1,
|
||||
y: 0,
|
||||
z: 0
|
||||
});
|
||||
|
||||
var GLOW_LIGHT_POSITION = {
|
||||
x: 0,
|
||||
y: -0.1,
|
||||
z: 0
|
||||
};
|
||||
|
||||
// Evaluate the world light entity positions and orientations from the model ones
|
||||
function evalLightWorldTransform(modelPos, modelRot) {
|
||||
|
||||
return {
|
||||
p: Vec3.sum(modelPos, Vec3.multiplyQbyV(modelRot, MODEL_LIGHT_POSITION)),
|
||||
q: Quat.multiply(modelRot, MODEL_LIGHT_ROTATION)
|
||||
};
|
||||
}
|
||||
|
||||
function glowLightWorldTransform(modelPos, modelRot) {
|
||||
return {
|
||||
p: Vec3.sum(modelPos, Vec3.multiplyQbyV(modelRot, GLOW_LIGHT_POSITION)),
|
||||
q: Quat.multiply(modelRot, MODEL_LIGHT_ROTATION)
|
||||
};
|
||||
}
|
||||
|
||||
Flashlight.prototype = {
|
||||
lightOn: false,
|
||||
hand: null,
|
||||
whichHand: null,
|
||||
hasSpotlight: false,
|
||||
spotlight: null,
|
||||
setRightHand: function() {
|
||||
this.hand = 'RIGHT';
|
||||
},
|
||||
|
||||
setLeftHand: function() {
|
||||
this.hand = 'LEFT';
|
||||
},
|
||||
|
||||
startNearGrab: function() {
|
||||
if (!this.hasSpotlight) {
|
||||
|
||||
//this light casts the beam
|
||||
this.spotlight = Entities.addEntity({
|
||||
type: "Light",
|
||||
isSpotlight: true,
|
||||
dimensions: {
|
||||
x: 2,
|
||||
y: 2,
|
||||
z: 20
|
||||
},
|
||||
color: {
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255
|
||||
},
|
||||
intensity: 2,
|
||||
exponent: 0.3,
|
||||
cutoff: 20,
|
||||
lifetime: LIFETIME
|
||||
});
|
||||
|
||||
//this light creates the effect of a bulb at the end of the flashlight
|
||||
this.glowLight = Entities.addEntity({
|
||||
type: "Light",
|
||||
dimensions: {
|
||||
x: 0.25,
|
||||
y: 0.25,
|
||||
z: 0.25
|
||||
},
|
||||
isSpotlight: false,
|
||||
color: {
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255
|
||||
},
|
||||
exponent: 0,
|
||||
cutoff: 90, // in degrees
|
||||
lifetime: LIFETIME
|
||||
});
|
||||
|
||||
this.hasSpotlight = true;
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
setWhichHand: function() {
|
||||
this.whichHand = this.hand;
|
||||
},
|
||||
|
||||
continueNearGrab: function() {
|
||||
if (this.whichHand === null) {
|
||||
//only set the active hand once -- if we always read the current hand, our 'holding' hand will get overwritten
|
||||
this.setWhichHand();
|
||||
} else {
|
||||
this.updateLightPositions();
|
||||
this.changeLightWithTriggerPressure(this.whichHand);
|
||||
}
|
||||
},
|
||||
|
||||
releaseGrab: function() {
|
||||
//delete the lights and reset state
|
||||
if (this.hasSpotlight) {
|
||||
Entities.deleteEntity(this.spotlight);
|
||||
Entities.deleteEntity(this.glowLight);
|
||||
this.hasSpotlight = false;
|
||||
this.glowLight = null;
|
||||
this.spotlight = null;
|
||||
this.whichHand = null;
|
||||
this.lightOn = false;
|
||||
}
|
||||
},
|
||||
|
||||
updateLightPositions: function() {
|
||||
var modelProperties = Entities.getEntityProperties(this.entityID, ['position', 'rotation']);
|
||||
|
||||
//move the two lights along the vectors we set above
|
||||
var lightTransform = evalLightWorldTransform(modelProperties.position, modelProperties.rotation);
|
||||
var glowLightTransform = glowLightWorldTransform(modelProperties.position, modelProperties.rotation);
|
||||
|
||||
//move them with the entity model
|
||||
Entities.editEntity(this.spotlight, {
|
||||
position: lightTransform.p,
|
||||
rotation: lightTransform.q,
|
||||
lifetime: (Date.now() - startTime) / MSEC_PER_SEC + LIFETIME
|
||||
});
|
||||
|
||||
Entities.editEntity(this.glowLight, {
|
||||
position: glowLightTransform.p,
|
||||
rotation: glowLightTransform.q,
|
||||
lifetime: (Date.now() - startTime) / MSEC_PER_SEC + LIFETIME
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
changeLightWithTriggerPressure: function(flashLightHand) {
|
||||
var handClickString = flashLightHand + "_HAND_CLICK";
|
||||
|
||||
var handClick = Controller.findAction(handClickString);
|
||||
|
||||
this.triggerValue = Controller.getActionValue(handClick);
|
||||
|
||||
if (this.triggerValue < DISABLE_LIGHT_THRESHOLD && this.lightOn === true) {
|
||||
this.turnLightOff();
|
||||
} else if (this.triggerValue >= DISABLE_LIGHT_THRESHOLD && this.lightOn === false) {
|
||||
this.turnLightOn();
|
||||
}
|
||||
return;
|
||||
},
|
||||
|
||||
turnLightOff: function() {
|
||||
this.playSoundAtCurrentPosition(false);
|
||||
Entities.editEntity(this.spotlight, {
|
||||
intensity: 0
|
||||
});
|
||||
Entities.editEntity(this.glowLight, {
|
||||
intensity: 0
|
||||
});
|
||||
this.lightOn = false;
|
||||
},
|
||||
|
||||
turnLightOn: function() {
|
||||
this.playSoundAtCurrentPosition(true);
|
||||
|
||||
Entities.editEntity(this.glowLight, {
|
||||
intensity: 2
|
||||
});
|
||||
Entities.editEntity(this.spotlight, {
|
||||
intensity: 2
|
||||
});
|
||||
this.lightOn = true;
|
||||
},
|
||||
|
||||
playSoundAtCurrentPosition: function(playOnSound) {
|
||||
var position = Entities.getEntityProperties(this.entityID, "position").position;
|
||||
|
||||
var audioProperties = {
|
||||
volume: 0.25,
|
||||
position: position
|
||||
};
|
||||
|
||||
if (playOnSound) {
|
||||
Audio.playSound(this.ON_SOUND, audioProperties);
|
||||
} else {
|
||||
Audio.playSound(this.OFF_SOUND, audioProperties);
|
||||
}
|
||||
},
|
||||
|
||||
preload: function(entityID) {
|
||||
|
||||
// preload() will be called when the entity has become visible (or known) to the interface
|
||||
// it gives us a chance to set our local JavaScript object up. In this case it means:
|
||||
// * remembering our entityID, so we can access it in cases where we're called without an entityID
|
||||
// * preloading sounds
|
||||
this.entityID = entityID;
|
||||
this.ON_SOUND = SoundCache.getSound(ON_SOUND_URL);
|
||||
this.OFF_SOUND = SoundCache.getSound(OFF_SOUND_URL);
|
||||
|
||||
},
|
||||
|
||||
unload: function() {
|
||||
// unload() will be called when our entity is no longer available. It may be because we were deleted,
|
||||
// or because we've left the domain or quit the application.
|
||||
if (this.hasSpotlight) {
|
||||
Entities.deleteEntity(this.spotlight);
|
||||
Entities.deleteEntity(this.glowLight);
|
||||
this.hasSpotlight = false;
|
||||
this.glowLight = null;
|
||||
this.spotlight = null;
|
||||
this.whichHand = null;
|
||||
this.lightOn = false;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
// entity scripts always need to return a newly constructed object of our type
|
||||
return new Flashlight();
|
||||
//
|
||||
// flashlight.js
|
||||
//
|
||||
// Script Type: Entity
|
||||
//
|
||||
// Created by Sam Gateau on 9/9/15.
|
||||
// Additions by James B. Pollack @imgntn on 9/21/2015
|
||||
// Copyright 2015 High Fidelity, Inc.
|
||||
//
|
||||
// This is a toy script that can be added to the Flashlight model entity:
|
||||
// "https://hifi-public.s3.amazonaws.com/models/props/flashlight.fbx"
|
||||
// that creates a spotlight attached with the flashlight model while the entity is grabbed
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
/*global MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, randFloat, randInt */
|
||||
(function() {
|
||||
|
||||
Script.include("../../libraries/utils.js");
|
||||
|
||||
var ON_SOUND_URL = 'http://hifi-public.s3.amazonaws.com/sounds/Switches%20and%20sliders/flashlight_on.wav';
|
||||
var OFF_SOUND_URL = 'http://hifi-public.s3.amazonaws.com/sounds/Switches%20and%20sliders/flashlight_off.wav';
|
||||
|
||||
//we are creating lights that we don't want to get stranded so lets make sure that we can get rid of them
|
||||
var startTime = Date.now();
|
||||
//if you're going to be using this in a dungeon or something and holding it for a long time, increase this lifetime value.
|
||||
var LIFETIME = 25;
|
||||
var MSEC_PER_SEC = 1000.0;
|
||||
|
||||
// this is the "constructor" for the entity as a JS object we don't do much here, but we do want to remember
|
||||
// our this object, so we can access it in cases where we're called without a this (like in the case of various global signals)
|
||||
function Flashlight() {
|
||||
return;
|
||||
}
|
||||
|
||||
//if the trigger value goes below this while held, the flashlight will turn off. if it goes above, it will
|
||||
var DISABLE_LIGHT_THRESHOLD = 0.7;
|
||||
|
||||
// These constants define the Spotlight position and orientation relative to the model
|
||||
var MODEL_LIGHT_POSITION = {
|
||||
x: 0,
|
||||
y: -0.3,
|
||||
z: 0
|
||||
};
|
||||
var MODEL_LIGHT_ROTATION = Quat.angleAxis(-90, {
|
||||
x: 1,
|
||||
y: 0,
|
||||
z: 0
|
||||
});
|
||||
|
||||
var GLOW_LIGHT_POSITION = {
|
||||
x: 0,
|
||||
y: -0.1,
|
||||
z: 0
|
||||
};
|
||||
|
||||
// Evaluate the world light entity positions and orientations from the model ones
|
||||
function evalLightWorldTransform(modelPos, modelRot) {
|
||||
|
||||
return {
|
||||
p: Vec3.sum(modelPos, Vec3.multiplyQbyV(modelRot, MODEL_LIGHT_POSITION)),
|
||||
q: Quat.multiply(modelRot, MODEL_LIGHT_ROTATION)
|
||||
};
|
||||
}
|
||||
|
||||
function glowLightWorldTransform(modelPos, modelRot) {
|
||||
return {
|
||||
p: Vec3.sum(modelPos, Vec3.multiplyQbyV(modelRot, GLOW_LIGHT_POSITION)),
|
||||
q: Quat.multiply(modelRot, MODEL_LIGHT_ROTATION)
|
||||
};
|
||||
}
|
||||
|
||||
Flashlight.prototype = {
|
||||
lightOn: false,
|
||||
hand: null,
|
||||
whichHand: null,
|
||||
hasSpotlight: false,
|
||||
spotlight: null,
|
||||
setRightHand: function() {
|
||||
this.hand = 'RIGHT';
|
||||
},
|
||||
|
||||
setLeftHand: function() {
|
||||
this.hand = 'LEFT';
|
||||
},
|
||||
|
||||
startNearGrab: function() {
|
||||
if (!this.hasSpotlight) {
|
||||
|
||||
//this light casts the beam
|
||||
this.spotlight = Entities.addEntity({
|
||||
type: "Light",
|
||||
isSpotlight: true,
|
||||
dimensions: {
|
||||
x: 2,
|
||||
y: 2,
|
||||
z: 20
|
||||
},
|
||||
color: {
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255
|
||||
},
|
||||
intensity: 2,
|
||||
exponent: 0.3,
|
||||
cutoff: 20,
|
||||
lifetime: LIFETIME
|
||||
});
|
||||
|
||||
//this light creates the effect of a bulb at the end of the flashlight
|
||||
this.glowLight = Entities.addEntity({
|
||||
type: "Light",
|
||||
dimensions: {
|
||||
x: 0.25,
|
||||
y: 0.25,
|
||||
z: 0.25
|
||||
},
|
||||
isSpotlight: false,
|
||||
color: {
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255
|
||||
},
|
||||
exponent: 0,
|
||||
cutoff: 90, // in degrees
|
||||
lifetime: LIFETIME
|
||||
});
|
||||
|
||||
this.hasSpotlight = true;
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
setWhichHand: function() {
|
||||
this.whichHand = this.hand;
|
||||
},
|
||||
|
||||
continueNearGrab: function() {
|
||||
if (this.whichHand === null) {
|
||||
//only set the active hand once -- if we always read the current hand, our 'holding' hand will get overwritten
|
||||
this.setWhichHand();
|
||||
} else {
|
||||
this.updateLightPositions();
|
||||
this.changeLightWithTriggerPressure(this.whichHand);
|
||||
}
|
||||
},
|
||||
|
||||
releaseGrab: function() {
|
||||
//delete the lights and reset state
|
||||
if (this.hasSpotlight) {
|
||||
Entities.deleteEntity(this.spotlight);
|
||||
Entities.deleteEntity(this.glowLight);
|
||||
this.hasSpotlight = false;
|
||||
this.glowLight = null;
|
||||
this.spotlight = null;
|
||||
this.whichHand = null;
|
||||
this.lightOn = false;
|
||||
}
|
||||
},
|
||||
|
||||
updateLightPositions: function() {
|
||||
var modelProperties = Entities.getEntityProperties(this.entityID, ['position', 'rotation']);
|
||||
|
||||
//move the two lights along the vectors we set above
|
||||
var lightTransform = evalLightWorldTransform(modelProperties.position, modelProperties.rotation);
|
||||
var glowLightTransform = glowLightWorldTransform(modelProperties.position, modelProperties.rotation);
|
||||
|
||||
//move them with the entity model
|
||||
Entities.editEntity(this.spotlight, {
|
||||
position: lightTransform.p,
|
||||
rotation: lightTransform.q,
|
||||
lifetime: (Date.now() - startTime) / MSEC_PER_SEC + LIFETIME
|
||||
});
|
||||
|
||||
Entities.editEntity(this.glowLight, {
|
||||
position: glowLightTransform.p,
|
||||
rotation: glowLightTransform.q,
|
||||
lifetime: (Date.now() - startTime) / MSEC_PER_SEC + LIFETIME
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
changeLightWithTriggerPressure: function(flashLightHand) {
|
||||
var handClickString = flashLightHand + "_HAND_CLICK";
|
||||
|
||||
var handClick = Controller.findAction(handClickString);
|
||||
|
||||
this.triggerValue = Controller.getActionValue(handClick);
|
||||
|
||||
if (this.triggerValue < DISABLE_LIGHT_THRESHOLD && this.lightOn === true) {
|
||||
this.turnLightOff();
|
||||
} else if (this.triggerValue >= DISABLE_LIGHT_THRESHOLD && this.lightOn === false) {
|
||||
this.turnLightOn();
|
||||
}
|
||||
return;
|
||||
},
|
||||
|
||||
turnLightOff: function() {
|
||||
this.playSoundAtCurrentPosition(false);
|
||||
Entities.editEntity(this.spotlight, {
|
||||
intensity: 0
|
||||
});
|
||||
Entities.editEntity(this.glowLight, {
|
||||
intensity: 0
|
||||
});
|
||||
this.lightOn = false;
|
||||
},
|
||||
|
||||
turnLightOn: function() {
|
||||
this.playSoundAtCurrentPosition(true);
|
||||
|
||||
Entities.editEntity(this.glowLight, {
|
||||
intensity: 2
|
||||
});
|
||||
Entities.editEntity(this.spotlight, {
|
||||
intensity: 2
|
||||
});
|
||||
this.lightOn = true;
|
||||
},
|
||||
|
||||
playSoundAtCurrentPosition: function(playOnSound) {
|
||||
var position = Entities.getEntityProperties(this.entityID, "position").position;
|
||||
|
||||
var audioProperties = {
|
||||
volume: 0.25,
|
||||
position: position
|
||||
};
|
||||
|
||||
if (playOnSound) {
|
||||
Audio.playSound(this.ON_SOUND, audioProperties);
|
||||
} else {
|
||||
Audio.playSound(this.OFF_SOUND, audioProperties);
|
||||
}
|
||||
},
|
||||
|
||||
preload: function(entityID) {
|
||||
|
||||
// preload() will be called when the entity has become visible (or known) to the interface
|
||||
// it gives us a chance to set our local JavaScript object up. In this case it means:
|
||||
// * remembering our entityID, so we can access it in cases where we're called without an entityID
|
||||
// * preloading sounds
|
||||
this.entityID = entityID;
|
||||
this.ON_SOUND = SoundCache.getSound(ON_SOUND_URL);
|
||||
this.OFF_SOUND = SoundCache.getSound(OFF_SOUND_URL);
|
||||
|
||||
},
|
||||
|
||||
unload: function() {
|
||||
// unload() will be called when our entity is no longer available. It may be because we were deleted,
|
||||
// or because we've left the domain or quit the application.
|
||||
if (this.hasSpotlight) {
|
||||
Entities.deleteEntity(this.spotlight);
|
||||
Entities.deleteEntity(this.glowLight);
|
||||
this.hasSpotlight = false;
|
||||
this.glowLight = null;
|
||||
this.spotlight = null;
|
||||
this.whichHand = null;
|
||||
this.lightOn = false;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
// entity scripts always need to return a newly constructed object of our type
|
||||
return new Flashlight();
|
||||
});
|
906
examples/walk.js
906
examples/walk.js
|
@ -1,454 +1,454 @@
|
|||
//
|
||||
// walk.js
|
||||
// version 1.25
|
||||
//
|
||||
// Created by David Wooldridge, June 2015
|
||||
// Copyright © 2014 - 2015 High Fidelity, Inc.
|
||||
//
|
||||
// Animates an avatar using procedural animation techniques.
|
||||
//
|
||||
// Editing tools for animation data files available here: https://github.com/DaveDubUK/walkTools
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
// animations, reach poses, reach pose parameters, transitions, transition parameters, sounds, image/s and reference files
|
||||
const HIFI_PUBLIC_BUCKET = "https://hifi-public.s3.amazonaws.com/";
|
||||
var pathToAssets = HIFI_PUBLIC_BUCKET + "procedural-animator/assets/";
|
||||
|
||||
Script.include([
|
||||
"./libraries/walkConstants.js",
|
||||
"./libraries/walkFilters.js",
|
||||
"./libraries/walkApi.js",
|
||||
pathToAssets + "walkAssets.js"
|
||||
]);
|
||||
|
||||
// construct Avatar, Motion and (null) Transition
|
||||
var avatar = new Avatar();
|
||||
var motion = new Motion();
|
||||
var nullTransition = new Transition();
|
||||
motion.currentTransition = nullTransition;
|
||||
|
||||
// create settings (gets initial values from avatar)
|
||||
Script.include("./libraries/walkSettings.js");
|
||||
|
||||
// Main loop
|
||||
Script.update.connect(function(deltaTime) {
|
||||
|
||||
if (motion.isLive) {
|
||||
|
||||
// assess current locomotion state
|
||||
motion.assess(deltaTime);
|
||||
|
||||
// decide which animation should be playing
|
||||
selectAnimation();
|
||||
|
||||
// advance the animation cycle/s by the correct amount/s
|
||||
advanceAnimations();
|
||||
|
||||
// update the progress of any live transitions
|
||||
updateTransitions();
|
||||
|
||||
// apply translation and rotations
|
||||
renderMotion();
|
||||
|
||||
// save this frame's parameters
|
||||
motion.saveHistory();
|
||||
}
|
||||
});
|
||||
|
||||
// helper function for selectAnimation()
|
||||
function setTransition(nextAnimation, playTransitionReachPoses) {
|
||||
var lastTransition = motion.currentTransition;
|
||||
var lastAnimation = avatar.currentAnimation;
|
||||
|
||||
// if already transitioning from a blended walk need to maintain the previous walk's direction
|
||||
if (lastAnimation.lastDirection) {
|
||||
switch(lastAnimation.lastDirection) {
|
||||
|
||||
case FORWARDS:
|
||||
lastAnimation = avatar.selectedWalk;
|
||||
break;
|
||||
|
||||
case BACKWARDS:
|
||||
lastAnimation = avatar.selectedWalkBackwards;
|
||||
break;
|
||||
|
||||
case LEFT:
|
||||
lastAnimation = avatar.selectedSideStepLeft;
|
||||
break;
|
||||
|
||||
case RIGHT:
|
||||
lastAnimation = avatar.selectedSideStepRight;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
motion.currentTransition = new Transition(nextAnimation, lastAnimation, lastTransition, playTransitionReachPoses);
|
||||
avatar.currentAnimation = nextAnimation;
|
||||
|
||||
// reset default first footstep
|
||||
if (nextAnimation === avatar.selectedWalkBlend && lastTransition === nullTransition) {
|
||||
avatar.nextStep = RIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
// fly animation blending: smoothing / damping filters
|
||||
const FLY_BLEND_DAMPING = 50;
|
||||
var flyUpFilter = filter.createAveragingFilter(FLY_BLEND_DAMPING);
|
||||
var flyDownFilter = filter.createAveragingFilter(FLY_BLEND_DAMPING);
|
||||
var flyForwardFilter = filter.createAveragingFilter(FLY_BLEND_DAMPING);
|
||||
var flyBackwardFilter = filter.createAveragingFilter(FLY_BLEND_DAMPING);
|
||||
|
||||
// select / blend the appropriate animation for the current state of motion
|
||||
function selectAnimation() {
|
||||
var playTransitionReachPoses = true;
|
||||
|
||||
// select appropriate animation. create transitions where appropriate
|
||||
switch (motion.nextState) {
|
||||
case STATIC: {
|
||||
if (avatar.distanceFromSurface < ON_SURFACE_THRESHOLD &&
|
||||
avatar.currentAnimation !== avatar.selectedIdle) {
|
||||
setTransition(avatar.selectedIdle, playTransitionReachPoses);
|
||||
} else if (!(avatar.distanceFromSurface < ON_SURFACE_THRESHOLD) &&
|
||||
avatar.currentAnimation !== avatar.selectedHover) {
|
||||
setTransition(avatar.selectedHover, playTransitionReachPoses);
|
||||
}
|
||||
motion.state = STATIC;
|
||||
avatar.selectedWalkBlend.lastDirection = NONE;
|
||||
break;
|
||||
}
|
||||
|
||||
case SURFACE_MOTION: {
|
||||
// walk transition reach poses are currently only specified for starting to walk forwards
|
||||
playTransitionReachPoses = (motion.direction === FORWARDS);
|
||||
var isAlreadyWalking = (avatar.currentAnimation === avatar.selectedWalkBlend);
|
||||
|
||||
switch (motion.direction) {
|
||||
case FORWARDS:
|
||||
if (avatar.selectedWalkBlend.lastDirection !== FORWARDS) {
|
||||
animationOperations.deepCopy(avatar.selectedWalk, avatar.selectedWalkBlend);
|
||||
avatar.calibration.strideLength = avatar.selectedWalk.calibration.strideLength;
|
||||
}
|
||||
avatar.selectedWalkBlend.lastDirection = FORWARDS;
|
||||
break;
|
||||
|
||||
case BACKWARDS:
|
||||
if (avatar.selectedWalkBlend.lastDirection !== BACKWARDS) {
|
||||
animationOperations.deepCopy(avatar.selectedWalkBackwards, avatar.selectedWalkBlend);
|
||||
avatar.calibration.strideLength = avatar.selectedWalkBackwards.calibration.strideLength;
|
||||
}
|
||||
avatar.selectedWalkBlend.lastDirection = BACKWARDS;
|
||||
break;
|
||||
|
||||
case LEFT:
|
||||
animationOperations.deepCopy(avatar.selectedSideStepLeft, avatar.selectedWalkBlend);
|
||||
avatar.selectedWalkBlend.lastDirection = LEFT;
|
||||
avatar.calibration.strideLength = avatar.selectedSideStepLeft.calibration.strideLength;
|
||||
break
|
||||
|
||||
case RIGHT:
|
||||
animationOperations.deepCopy(avatar.selectedSideStepRight, avatar.selectedWalkBlend);
|
||||
avatar.selectedWalkBlend.lastDirection = RIGHT;
|
||||
avatar.calibration.strideLength = avatar.selectedSideStepRight.calibration.strideLength;
|
||||
break;
|
||||
|
||||
default:
|
||||
// condition occurs when the avi goes through the floor due to collision hull errors
|
||||
animationOperations.deepCopy(avatar.selectedWalk, avatar.selectedWalkBlend);
|
||||
avatar.selectedWalkBlend.lastDirection = FORWARDS;
|
||||
avatar.calibration.strideLength = avatar.selectedWalk.calibration.strideLength;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isAlreadyWalking && !motion.isComingToHalt) {
|
||||
setTransition(avatar.selectedWalkBlend, playTransitionReachPoses);
|
||||
}
|
||||
motion.state = SURFACE_MOTION;
|
||||
break;
|
||||
}
|
||||
|
||||
case AIR_MOTION: {
|
||||
// blend the up, down, forward and backward flying animations relative to motion speed and direction
|
||||
animationOperations.zeroAnimation(avatar.selectedFlyBlend);
|
||||
|
||||
// calculate influences based on velocity and direction
|
||||
var velocityMagnitude = Vec3.length(motion.velocity);
|
||||
var verticalProportion = motion.velocity.y / velocityMagnitude;
|
||||
var thrustProportion = motion.velocity.z / velocityMagnitude / 2;
|
||||
|
||||
// directional components
|
||||
var upComponent = motion.velocity.y > 0 ? verticalProportion : 0;
|
||||
var downComponent = motion.velocity.y < 0 ? -verticalProportion : 0;
|
||||
var forwardComponent = motion.velocity.z < 0 ? -thrustProportion : 0;
|
||||
var backwardComponent = motion.velocity.z > 0 ? thrustProportion : 0;
|
||||
|
||||
// smooth / damp directional components to add visual 'weight'
|
||||
upComponent = flyUpFilter.process(upComponent);
|
||||
downComponent = flyDownFilter.process(downComponent);
|
||||
forwardComponent = flyForwardFilter.process(forwardComponent);
|
||||
backwardComponent = flyBackwardFilter.process(backwardComponent);
|
||||
|
||||
// normalise directional components
|
||||
var normaliser = upComponent + downComponent + forwardComponent + backwardComponent;
|
||||
upComponent = upComponent / normaliser;
|
||||
downComponent = downComponent / normaliser;
|
||||
forwardComponent = forwardComponent / normaliser;
|
||||
backwardComponent = backwardComponent / normaliser;
|
||||
|
||||
// blend animations proportionally
|
||||
if (upComponent > 0) {
|
||||
animationOperations.blendAnimation(avatar.selectedFlyUp,
|
||||
avatar.selectedFlyBlend,
|
||||
upComponent);
|
||||
}
|
||||
if (downComponent > 0) {
|
||||
animationOperations.blendAnimation(avatar.selectedFlyDown,
|
||||
avatar.selectedFlyBlend,
|
||||
downComponent);
|
||||
}
|
||||
if (forwardComponent > 0) {
|
||||
animationOperations.blendAnimation(avatar.selectedFly,
|
||||
avatar.selectedFlyBlend,
|
||||
Math.abs(forwardComponent));
|
||||
}
|
||||
if (backwardComponent > 0) {
|
||||
animationOperations.blendAnimation(avatar.selectedFlyBackwards,
|
||||
avatar.selectedFlyBlend,
|
||||
Math.abs(backwardComponent));
|
||||
}
|
||||
|
||||
if (avatar.currentAnimation !== avatar.selectedFlyBlend) {
|
||||
setTransition(avatar.selectedFlyBlend, playTransitionReachPoses);
|
||||
}
|
||||
motion.state = AIR_MOTION;
|
||||
avatar.selectedWalkBlend.lastDirection = NONE;
|
||||
break;
|
||||
}
|
||||
} // end switch next state of motion
|
||||
}
|
||||
|
||||
// determine the length of stride. advance the frequency time wheels. advance frequency time wheels for any live transitions
|
||||
function advanceAnimations() {
|
||||
var wheelAdvance = 0;
|
||||
|
||||
// turn the frequency time wheel
|
||||
if (avatar.currentAnimation === avatar.selectedWalkBlend) {
|
||||
// Using technique described here: http://www.gdcvault.com/play/1020583/Animation-Bootcamp-An-Indie-Approach
|
||||
// wrap the stride length around a 'surveyor's wheel' twice and calculate the angular speed at the given (linear) speed
|
||||
// omega = v / r , where r = circumference / 2 PI and circumference = 2 * stride length
|
||||
var speed = Vec3.length(motion.velocity);
|
||||
motion.frequencyTimeWheelRadius = avatar.calibration.strideLength / Math.PI;
|
||||
var ftWheelAngularVelocity = speed / motion.frequencyTimeWheelRadius;
|
||||
// calculate the degrees turned (at this angular speed) since last frame
|
||||
wheelAdvance = filter.radToDeg(motion.deltaTime * ftWheelAngularVelocity);
|
||||
} else {
|
||||
// turn the frequency time wheel by the amount specified for this animation
|
||||
wheelAdvance = filter.radToDeg(avatar.currentAnimation.calibration.frequency * motion.deltaTime);
|
||||
}
|
||||
|
||||
if (motion.currentTransition !== nullTransition) {
|
||||
// the last animation is still playing so we turn it's frequency time wheel to maintain the animation
|
||||
if (motion.currentTransition.lastAnimation === motion.selectedWalkBlend) {
|
||||
// if at a stop angle (i.e. feet now under the avi) hold the wheel position for remainder of transition
|
||||
var tolerance = motion.currentTransition.lastFrequencyTimeIncrement + 0.1;
|
||||
if ((motion.currentTransition.lastFrequencyTimeWheelPos >
|
||||
(motion.currentTransition.stopAngle - tolerance)) &&
|
||||
(motion.currentTransition.lastFrequencyTimeWheelPos <
|
||||
(motion.currentTransition.stopAngle + tolerance))) {
|
||||
motion.currentTransition.lastFrequencyTimeIncrement = 0;
|
||||
}
|
||||
}
|
||||
motion.currentTransition.advancePreviousFrequencyTimeWheel(motion.deltaTime);
|
||||
}
|
||||
|
||||
// avoid unnaturally fast walking when landing at speed - simulates skimming / skidding
|
||||
if (Math.abs(wheelAdvance) > MAX_FT_WHEEL_INCREMENT) {
|
||||
wheelAdvance = 0;
|
||||
}
|
||||
|
||||
// advance the walk wheel the appropriate amount
|
||||
motion.advanceFrequencyTimeWheel(wheelAdvance);
|
||||
|
||||
// walking? then see if it's a good time to measure the stride length (needs to be at least 97% of max walking speed)
|
||||
const ALMOST_ONE = 0.97;
|
||||
if (avatar.currentAnimation === avatar.selectedWalkBlend &&
|
||||
(Vec3.length(motion.velocity) / MAX_WALK_SPEED > ALMOST_ONE)) {
|
||||
|
||||
var strideMaxAt = avatar.currentAnimation.calibration.strideMaxAt;
|
||||
const TOLERANCE = 1.0;
|
||||
|
||||
if (motion.frequencyTimeWheelPos < (strideMaxAt + TOLERANCE) &&
|
||||
motion.frequencyTimeWheelPos > (strideMaxAt - TOLERANCE) &&
|
||||
motion.currentTransition === nullTransition) {
|
||||
// measure and save stride length
|
||||
var footRPos = MyAvatar.getJointPosition("RightFoot");
|
||||
var footLPos = MyAvatar.getJointPosition("LeftFoot");
|
||||
avatar.calibration.strideLength = Vec3.distance(footRPos, footLPos);
|
||||
avatar.currentAnimation.calibration.strideLength = avatar.calibration.strideLength;
|
||||
} else {
|
||||
// use the previously saved value for stride length
|
||||
avatar.calibration.strideLength = avatar.currentAnimation.calibration.strideLength;
|
||||
}
|
||||
} // end get walk stride length
|
||||
}
|
||||
|
||||
// initialise a new transition. update progress of a live transition
|
||||
function updateTransitions() {
|
||||
|
||||
if (motion.currentTransition !== nullTransition) {
|
||||
// is this a new transition?
|
||||
if (motion.currentTransition.progress === 0) {
|
||||
// do we have overlapping transitions?
|
||||
if (motion.currentTransition.lastTransition !== nullTransition) {
|
||||
// is the last animation for the nested transition the same as the new animation?
|
||||
if (motion.currentTransition.lastTransition.lastAnimation === avatar.currentAnimation) {
|
||||
// then sync the nested transition's frequency time wheel for a smooth animation blend
|
||||
motion.frequencyTimeWheelPos = motion.currentTransition.lastTransition.lastFrequencyTimeWheelPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (motion.currentTransition.updateProgress() === TRANSITION_COMPLETE) {
|
||||
motion.currentTransition = nullTransition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helper function for renderMotion(). calculate the amount to lean forwards (or backwards) based on the avi's velocity
|
||||
var leanPitchSmoothingFilter = filter.createButterworthFilter();
|
||||
function getLeanPitch() {
|
||||
var leanProgress = 0;
|
||||
|
||||
if (motion.direction === DOWN ||
|
||||
motion.direction === FORWARDS ||
|
||||
motion.direction === BACKWARDS) {
|
||||
leanProgress = -motion.velocity.z / TOP_SPEED;
|
||||
}
|
||||
// use filters to shape the walking acceleration response
|
||||
leanProgress = leanPitchSmoothingFilter.process(leanProgress);
|
||||
return PITCH_MAX * leanProgress;
|
||||
}
|
||||
|
||||
// helper function for renderMotion(). calculate the angle at which to bank into corners whilst turning
|
||||
var leanRollSmoothingFilter = filter.createButterworthFilter();
|
||||
function getLeanRoll() {
|
||||
var leanRollProgress = 0;
|
||||
var linearContribution = 0;
|
||||
const LOG_SCALER = 8;
|
||||
|
||||
if (Vec3.length(motion.velocity) > 0) {
|
||||
linearContribution = (Math.log(Vec3.length(motion.velocity) / TOP_SPEED) + LOG_SCALER) / LOG_SCALER;
|
||||
}
|
||||
var angularContribution = Math.abs(motion.yawDelta) / DELTA_YAW_MAX;
|
||||
leanRollProgress = linearContribution;
|
||||
leanRollProgress *= angularContribution;
|
||||
// shape the response curve
|
||||
leanRollProgress = filter.bezier(leanRollProgress, {x: 1, y: 0}, {x: 1, y: 0});
|
||||
// which way to lean?
|
||||
var turnSign = (motion.yawDelta >= 0) ? 1 : -1;
|
||||
|
||||
if (motion.direction === BACKWARDS ||
|
||||
motion.direction === LEFT) {
|
||||
turnSign *= -1;
|
||||
}
|
||||
// filter progress
|
||||
leanRollProgress = leanRollSmoothingFilter.process(turnSign * leanRollProgress);
|
||||
return ROLL_MAX * leanRollProgress;
|
||||
}
|
||||
|
||||
// animate the avatar using sine waves, geometric waveforms and harmonic generators
|
||||
function renderMotion() {
|
||||
// leaning in response to speed and acceleration
|
||||
var leanPitch = motion.state === STATIC ? 0 : getLeanPitch();
|
||||
var leanRoll = motion.state === STATIC ? 0 : getLeanRoll();
|
||||
var lastDirection = motion.lastDirection;
|
||||
// hips translations from currently playing animations
|
||||
var hipsTranslations = {x:0, y:0, z:0};
|
||||
|
||||
if (motion.currentTransition !== nullTransition) {
|
||||
// maintain previous direction when transitioning from a walk
|
||||
if (motion.currentTransition.lastAnimation === avatar.selectedWalkBlend) {
|
||||
motion.lastDirection = motion.currentTransition.lastDirection;
|
||||
}
|
||||
hipsTranslations = motion.currentTransition.blendTranslations(motion.frequencyTimeWheelPos,
|
||||
motion.lastDirection);
|
||||
} else {
|
||||
hipsTranslations = animationOperations.calculateTranslations(avatar.currentAnimation,
|
||||
motion.frequencyTimeWheelPos,
|
||||
motion.direction);
|
||||
}
|
||||
// factor any leaning into the hips offset
|
||||
hipsTranslations.z += avatar.calibration.hipsToFeet * Math.sin(filter.degToRad(leanPitch));
|
||||
hipsTranslations.x += avatar.calibration.hipsToFeet * Math.sin(filter.degToRad(leanRoll));
|
||||
|
||||
// ensure skeleton offsets are within the 1m limit
|
||||
hipsTranslations.x = hipsTranslations.x > 1 ? 1 : hipsTranslations.x;
|
||||
hipsTranslations.x = hipsTranslations.x < -1 ? -1 : hipsTranslations.x;
|
||||
hipsTranslations.y = hipsTranslations.y > 1 ? 1 : hipsTranslations.y;
|
||||
hipsTranslations.y = hipsTranslations.y < -1 ? -1 : hipsTranslations.y;
|
||||
hipsTranslations.z = hipsTranslations.z > 1 ? 1 : hipsTranslations.z;
|
||||
hipsTranslations.z = hipsTranslations.z < -1 ? -1 : hipsTranslations.z;
|
||||
// apply translations
|
||||
MyAvatar.setSkeletonOffset(hipsTranslations);
|
||||
|
||||
// play footfall sound?
|
||||
var producingFootstepSounds = (avatar.currentAnimation === avatar.selectedWalkBlend) && avatar.makesFootStepSounds;
|
||||
|
||||
if (motion.currentTransition !== nullTransition && avatar.makesFootStepSounds) {
|
||||
if (motion.currentTransition.nextAnimation === avatar.selectedWalkBlend ||
|
||||
motion.currentTransition.lastAnimation === avatar.selectedWalkBlend) {
|
||||
producingFootstepSounds = true;
|
||||
}
|
||||
}
|
||||
if (producingFootstepSounds) {
|
||||
const QUARTER_CYCLE = 90;
|
||||
const THREE_QUARTER_CYCLE = 270;
|
||||
var ftWheelPosition = motion.frequencyTimeWheelPos;
|
||||
|
||||
if (motion.currentTransition !== nullTransition &&
|
||||
motion.currentTransition.lastAnimation === avatar.selectedWalkBlend) {
|
||||
ftWheelPosition = motion.currentTransition.lastFrequencyTimeWheelPos;
|
||||
}
|
||||
if (avatar.nextStep === LEFT && ftWheelPosition > THREE_QUARTER_CYCLE) {
|
||||
avatar.makeFootStepSound();
|
||||
} else if (avatar.nextStep === RIGHT && (ftWheelPosition < THREE_QUARTER_CYCLE && ftWheelPosition > QUARTER_CYCLE)) {
|
||||
avatar.makeFootStepSound();
|
||||
}
|
||||
}
|
||||
|
||||
// apply joint rotations
|
||||
for (jointName in avatar.currentAnimation.joints) {
|
||||
var joint = walkAssets.animationReference.joints[jointName];
|
||||
var jointRotations = undefined;
|
||||
|
||||
// ignore arms / head rotations if options are selected in the settings
|
||||
if (avatar.armsFree && (joint.IKChain === "LeftArm" || joint.IKChain === "RightArm")) {
|
||||
continue;
|
||||
}
|
||||
if (avatar.headFree && joint.IKChain === "Head") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if there's a live transition, blend the rotations with the last animation's rotations
|
||||
if (motion.currentTransition !== nullTransition) {
|
||||
jointRotations = motion.currentTransition.blendRotations(jointName,
|
||||
motion.frequencyTimeWheelPos,
|
||||
motion.lastDirection);
|
||||
} else {
|
||||
jointRotations = animationOperations.calculateRotations(jointName,
|
||||
avatar.currentAnimation,
|
||||
motion.frequencyTimeWheelPos,
|
||||
motion.direction);
|
||||
}
|
||||
|
||||
// apply angular velocity and speed induced leaning
|
||||
if (jointName === "Hips") {
|
||||
jointRotations.x += leanPitch;
|
||||
jointRotations.z += leanRoll;
|
||||
}
|
||||
|
||||
// apply rotations
|
||||
MyAvatar.setJointRotation(jointName, Quat.fromVec3Degrees(jointRotations));
|
||||
}
|
||||
//
|
||||
// walk.js
|
||||
// version 1.25
|
||||
//
|
||||
// Created by David Wooldridge, June 2015
|
||||
// Copyright © 2014 - 2015 High Fidelity, Inc.
|
||||
//
|
||||
// Animates an avatar using procedural animation techniques.
|
||||
//
|
||||
// Editing tools for animation data files available here: https://github.com/DaveDubUK/walkTools
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
// animations, reach poses, reach pose parameters, transitions, transition parameters, sounds, image/s and reference files
|
||||
const HIFI_PUBLIC_BUCKET = "https://hifi-public.s3.amazonaws.com/";
|
||||
var pathToAssets = HIFI_PUBLIC_BUCKET + "procedural-animator/assets/";
|
||||
|
||||
Script.include([
|
||||
"./libraries/walkConstants.js",
|
||||
"./libraries/walkFilters.js",
|
||||
"./libraries/walkApi.js",
|
||||
pathToAssets + "walkAssets.js"
|
||||
]);
|
||||
|
||||
// construct Avatar, Motion and (null) Transition
|
||||
var avatar = new Avatar();
|
||||
var motion = new Motion();
|
||||
var nullTransition = new Transition();
|
||||
motion.currentTransition = nullTransition;
|
||||
|
||||
// create settings (gets initial values from avatar)
|
||||
Script.include("./libraries/walkSettings.js");
|
||||
|
||||
// Main loop
|
||||
Script.update.connect(function(deltaTime) {
|
||||
|
||||
if (motion.isLive) {
|
||||
|
||||
// assess current locomotion state
|
||||
motion.assess(deltaTime);
|
||||
|
||||
// decide which animation should be playing
|
||||
selectAnimation();
|
||||
|
||||
// advance the animation cycle/s by the correct amount/s
|
||||
advanceAnimations();
|
||||
|
||||
// update the progress of any live transitions
|
||||
updateTransitions();
|
||||
|
||||
// apply translation and rotations
|
||||
renderMotion();
|
||||
|
||||
// save this frame's parameters
|
||||
motion.saveHistory();
|
||||
}
|
||||
});
|
||||
|
||||
// helper function for selectAnimation()
|
||||
function setTransition(nextAnimation, playTransitionReachPoses) {
|
||||
var lastTransition = motion.currentTransition;
|
||||
var lastAnimation = avatar.currentAnimation;
|
||||
|
||||
// if already transitioning from a blended walk need to maintain the previous walk's direction
|
||||
if (lastAnimation.lastDirection) {
|
||||
switch(lastAnimation.lastDirection) {
|
||||
|
||||
case FORWARDS:
|
||||
lastAnimation = avatar.selectedWalk;
|
||||
break;
|
||||
|
||||
case BACKWARDS:
|
||||
lastAnimation = avatar.selectedWalkBackwards;
|
||||
break;
|
||||
|
||||
case LEFT:
|
||||
lastAnimation = avatar.selectedSideStepLeft;
|
||||
break;
|
||||
|
||||
case RIGHT:
|
||||
lastAnimation = avatar.selectedSideStepRight;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
motion.currentTransition = new Transition(nextAnimation, lastAnimation, lastTransition, playTransitionReachPoses);
|
||||
avatar.currentAnimation = nextAnimation;
|
||||
|
||||
// reset default first footstep
|
||||
if (nextAnimation === avatar.selectedWalkBlend && lastTransition === nullTransition) {
|
||||
avatar.nextStep = RIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
// fly animation blending: smoothing / damping filters
|
||||
const FLY_BLEND_DAMPING = 50;
|
||||
var flyUpFilter = filter.createAveragingFilter(FLY_BLEND_DAMPING);
|
||||
var flyDownFilter = filter.createAveragingFilter(FLY_BLEND_DAMPING);
|
||||
var flyForwardFilter = filter.createAveragingFilter(FLY_BLEND_DAMPING);
|
||||
var flyBackwardFilter = filter.createAveragingFilter(FLY_BLEND_DAMPING);
|
||||
|
||||
// select / blend the appropriate animation for the current state of motion
|
||||
function selectAnimation() {
|
||||
var playTransitionReachPoses = true;
|
||||
|
||||
// select appropriate animation. create transitions where appropriate
|
||||
switch (motion.nextState) {
|
||||
case STATIC: {
|
||||
if (avatar.distanceFromSurface < ON_SURFACE_THRESHOLD &&
|
||||
avatar.currentAnimation !== avatar.selectedIdle) {
|
||||
setTransition(avatar.selectedIdle, playTransitionReachPoses);
|
||||
} else if (!(avatar.distanceFromSurface < ON_SURFACE_THRESHOLD) &&
|
||||
avatar.currentAnimation !== avatar.selectedHover) {
|
||||
setTransition(avatar.selectedHover, playTransitionReachPoses);
|
||||
}
|
||||
motion.state = STATIC;
|
||||
avatar.selectedWalkBlend.lastDirection = NONE;
|
||||
break;
|
||||
}
|
||||
|
||||
case SURFACE_MOTION: {
|
||||
// walk transition reach poses are currently only specified for starting to walk forwards
|
||||
playTransitionReachPoses = (motion.direction === FORWARDS);
|
||||
var isAlreadyWalking = (avatar.currentAnimation === avatar.selectedWalkBlend);
|
||||
|
||||
switch (motion.direction) {
|
||||
case FORWARDS:
|
||||
if (avatar.selectedWalkBlend.lastDirection !== FORWARDS) {
|
||||
animationOperations.deepCopy(avatar.selectedWalk, avatar.selectedWalkBlend);
|
||||
avatar.calibration.strideLength = avatar.selectedWalk.calibration.strideLength;
|
||||
}
|
||||
avatar.selectedWalkBlend.lastDirection = FORWARDS;
|
||||
break;
|
||||
|
||||
case BACKWARDS:
|
||||
if (avatar.selectedWalkBlend.lastDirection !== BACKWARDS) {
|
||||
animationOperations.deepCopy(avatar.selectedWalkBackwards, avatar.selectedWalkBlend);
|
||||
avatar.calibration.strideLength = avatar.selectedWalkBackwards.calibration.strideLength;
|
||||
}
|
||||
avatar.selectedWalkBlend.lastDirection = BACKWARDS;
|
||||
break;
|
||||
|
||||
case LEFT:
|
||||
animationOperations.deepCopy(avatar.selectedSideStepLeft, avatar.selectedWalkBlend);
|
||||
avatar.selectedWalkBlend.lastDirection = LEFT;
|
||||
avatar.calibration.strideLength = avatar.selectedSideStepLeft.calibration.strideLength;
|
||||
break
|
||||
|
||||
case RIGHT:
|
||||
animationOperations.deepCopy(avatar.selectedSideStepRight, avatar.selectedWalkBlend);
|
||||
avatar.selectedWalkBlend.lastDirection = RIGHT;
|
||||
avatar.calibration.strideLength = avatar.selectedSideStepRight.calibration.strideLength;
|
||||
break;
|
||||
|
||||
default:
|
||||
// condition occurs when the avi goes through the floor due to collision hull errors
|
||||
animationOperations.deepCopy(avatar.selectedWalk, avatar.selectedWalkBlend);
|
||||
avatar.selectedWalkBlend.lastDirection = FORWARDS;
|
||||
avatar.calibration.strideLength = avatar.selectedWalk.calibration.strideLength;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isAlreadyWalking && !motion.isComingToHalt) {
|
||||
setTransition(avatar.selectedWalkBlend, playTransitionReachPoses);
|
||||
}
|
||||
motion.state = SURFACE_MOTION;
|
||||
break;
|
||||
}
|
||||
|
||||
case AIR_MOTION: {
|
||||
// blend the up, down, forward and backward flying animations relative to motion speed and direction
|
||||
animationOperations.zeroAnimation(avatar.selectedFlyBlend);
|
||||
|
||||
// calculate influences based on velocity and direction
|
||||
var velocityMagnitude = Vec3.length(motion.velocity);
|
||||
var verticalProportion = motion.velocity.y / velocityMagnitude;
|
||||
var thrustProportion = motion.velocity.z / velocityMagnitude / 2;
|
||||
|
||||
// directional components
|
||||
var upComponent = motion.velocity.y > 0 ? verticalProportion : 0;
|
||||
var downComponent = motion.velocity.y < 0 ? -verticalProportion : 0;
|
||||
var forwardComponent = motion.velocity.z < 0 ? -thrustProportion : 0;
|
||||
var backwardComponent = motion.velocity.z > 0 ? thrustProportion : 0;
|
||||
|
||||
// smooth / damp directional components to add visual 'weight'
|
||||
upComponent = flyUpFilter.process(upComponent);
|
||||
downComponent = flyDownFilter.process(downComponent);
|
||||
forwardComponent = flyForwardFilter.process(forwardComponent);
|
||||
backwardComponent = flyBackwardFilter.process(backwardComponent);
|
||||
|
||||
// normalise directional components
|
||||
var normaliser = upComponent + downComponent + forwardComponent + backwardComponent;
|
||||
upComponent = upComponent / normaliser;
|
||||
downComponent = downComponent / normaliser;
|
||||
forwardComponent = forwardComponent / normaliser;
|
||||
backwardComponent = backwardComponent / normaliser;
|
||||
|
||||
// blend animations proportionally
|
||||
if (upComponent > 0) {
|
||||
animationOperations.blendAnimation(avatar.selectedFlyUp,
|
||||
avatar.selectedFlyBlend,
|
||||
upComponent);
|
||||
}
|
||||
if (downComponent > 0) {
|
||||
animationOperations.blendAnimation(avatar.selectedFlyDown,
|
||||
avatar.selectedFlyBlend,
|
||||
downComponent);
|
||||
}
|
||||
if (forwardComponent > 0) {
|
||||
animationOperations.blendAnimation(avatar.selectedFly,
|
||||
avatar.selectedFlyBlend,
|
||||
Math.abs(forwardComponent));
|
||||
}
|
||||
if (backwardComponent > 0) {
|
||||
animationOperations.blendAnimation(avatar.selectedFlyBackwards,
|
||||
avatar.selectedFlyBlend,
|
||||
Math.abs(backwardComponent));
|
||||
}
|
||||
|
||||
if (avatar.currentAnimation !== avatar.selectedFlyBlend) {
|
||||
setTransition(avatar.selectedFlyBlend, playTransitionReachPoses);
|
||||
}
|
||||
motion.state = AIR_MOTION;
|
||||
avatar.selectedWalkBlend.lastDirection = NONE;
|
||||
break;
|
||||
}
|
||||
} // end switch next state of motion
|
||||
}
|
||||
|
||||
// determine the length of stride. advance the frequency time wheels. advance frequency time wheels for any live transitions
|
||||
function advanceAnimations() {
|
||||
var wheelAdvance = 0;
|
||||
|
||||
// turn the frequency time wheel
|
||||
if (avatar.currentAnimation === avatar.selectedWalkBlend) {
|
||||
// Using technique described here: http://www.gdcvault.com/play/1020583/Animation-Bootcamp-An-Indie-Approach
|
||||
// wrap the stride length around a 'surveyor's wheel' twice and calculate the angular speed at the given (linear) speed
|
||||
// omega = v / r , where r = circumference / 2 PI and circumference = 2 * stride length
|
||||
var speed = Vec3.length(motion.velocity);
|
||||
motion.frequencyTimeWheelRadius = avatar.calibration.strideLength / Math.PI;
|
||||
var ftWheelAngularVelocity = speed / motion.frequencyTimeWheelRadius;
|
||||
// calculate the degrees turned (at this angular speed) since last frame
|
||||
wheelAdvance = filter.radToDeg(motion.deltaTime * ftWheelAngularVelocity);
|
||||
} else {
|
||||
// turn the frequency time wheel by the amount specified for this animation
|
||||
wheelAdvance = filter.radToDeg(avatar.currentAnimation.calibration.frequency * motion.deltaTime);
|
||||
}
|
||||
|
||||
if (motion.currentTransition !== nullTransition) {
|
||||
// the last animation is still playing so we turn it's frequency time wheel to maintain the animation
|
||||
if (motion.currentTransition.lastAnimation === motion.selectedWalkBlend) {
|
||||
// if at a stop angle (i.e. feet now under the avi) hold the wheel position for remainder of transition
|
||||
var tolerance = motion.currentTransition.lastFrequencyTimeIncrement + 0.1;
|
||||
if ((motion.currentTransition.lastFrequencyTimeWheelPos >
|
||||
(motion.currentTransition.stopAngle - tolerance)) &&
|
||||
(motion.currentTransition.lastFrequencyTimeWheelPos <
|
||||
(motion.currentTransition.stopAngle + tolerance))) {
|
||||
motion.currentTransition.lastFrequencyTimeIncrement = 0;
|
||||
}
|
||||
}
|
||||
motion.currentTransition.advancePreviousFrequencyTimeWheel(motion.deltaTime);
|
||||
}
|
||||
|
||||
// avoid unnaturally fast walking when landing at speed - simulates skimming / skidding
|
||||
if (Math.abs(wheelAdvance) > MAX_FT_WHEEL_INCREMENT) {
|
||||
wheelAdvance = 0;
|
||||
}
|
||||
|
||||
// advance the walk wheel the appropriate amount
|
||||
motion.advanceFrequencyTimeWheel(wheelAdvance);
|
||||
|
||||
// walking? then see if it's a good time to measure the stride length (needs to be at least 97% of max walking speed)
|
||||
const ALMOST_ONE = 0.97;
|
||||
if (avatar.currentAnimation === avatar.selectedWalkBlend &&
|
||||
(Vec3.length(motion.velocity) / MAX_WALK_SPEED > ALMOST_ONE)) {
|
||||
|
||||
var strideMaxAt = avatar.currentAnimation.calibration.strideMaxAt;
|
||||
const TOLERANCE = 1.0;
|
||||
|
||||
if (motion.frequencyTimeWheelPos < (strideMaxAt + TOLERANCE) &&
|
||||
motion.frequencyTimeWheelPos > (strideMaxAt - TOLERANCE) &&
|
||||
motion.currentTransition === nullTransition) {
|
||||
// measure and save stride length
|
||||
var footRPos = MyAvatar.getJointPosition("RightFoot");
|
||||
var footLPos = MyAvatar.getJointPosition("LeftFoot");
|
||||
avatar.calibration.strideLength = Vec3.distance(footRPos, footLPos);
|
||||
avatar.currentAnimation.calibration.strideLength = avatar.calibration.strideLength;
|
||||
} else {
|
||||
// use the previously saved value for stride length
|
||||
avatar.calibration.strideLength = avatar.currentAnimation.calibration.strideLength;
|
||||
}
|
||||
} // end get walk stride length
|
||||
}
|
||||
|
||||
// initialise a new transition. update progress of a live transition
|
||||
function updateTransitions() {
|
||||
|
||||
if (motion.currentTransition !== nullTransition) {
|
||||
// is this a new transition?
|
||||
if (motion.currentTransition.progress === 0) {
|
||||
// do we have overlapping transitions?
|
||||
if (motion.currentTransition.lastTransition !== nullTransition) {
|
||||
// is the last animation for the nested transition the same as the new animation?
|
||||
if (motion.currentTransition.lastTransition.lastAnimation === avatar.currentAnimation) {
|
||||
// then sync the nested transition's frequency time wheel for a smooth animation blend
|
||||
motion.frequencyTimeWheelPos = motion.currentTransition.lastTransition.lastFrequencyTimeWheelPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (motion.currentTransition.updateProgress() === TRANSITION_COMPLETE) {
|
||||
motion.currentTransition = nullTransition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helper function for renderMotion(). calculate the amount to lean forwards (or backwards) based on the avi's velocity
|
||||
var leanPitchSmoothingFilter = filter.createButterworthFilter();
|
||||
function getLeanPitch() {
|
||||
var leanProgress = 0;
|
||||
|
||||
if (motion.direction === DOWN ||
|
||||
motion.direction === FORWARDS ||
|
||||
motion.direction === BACKWARDS) {
|
||||
leanProgress = -motion.velocity.z / TOP_SPEED;
|
||||
}
|
||||
// use filters to shape the walking acceleration response
|
||||
leanProgress = leanPitchSmoothingFilter.process(leanProgress);
|
||||
return PITCH_MAX * leanProgress;
|
||||
}
|
||||
|
||||
// helper function for renderMotion(). calculate the angle at which to bank into corners whilst turning
|
||||
var leanRollSmoothingFilter = filter.createButterworthFilter();
|
||||
function getLeanRoll() {
|
||||
var leanRollProgress = 0;
|
||||
var linearContribution = 0;
|
||||
const LOG_SCALER = 8;
|
||||
|
||||
if (Vec3.length(motion.velocity) > 0) {
|
||||
linearContribution = (Math.log(Vec3.length(motion.velocity) / TOP_SPEED) + LOG_SCALER) / LOG_SCALER;
|
||||
}
|
||||
var angularContribution = Math.abs(motion.yawDelta) / DELTA_YAW_MAX;
|
||||
leanRollProgress = linearContribution;
|
||||
leanRollProgress *= angularContribution;
|
||||
// shape the response curve
|
||||
leanRollProgress = filter.bezier(leanRollProgress, {x: 1, y: 0}, {x: 1, y: 0});
|
||||
// which way to lean?
|
||||
var turnSign = (motion.yawDelta >= 0) ? 1 : -1;
|
||||
|
||||
if (motion.direction === BACKWARDS ||
|
||||
motion.direction === LEFT) {
|
||||
turnSign *= -1;
|
||||
}
|
||||
// filter progress
|
||||
leanRollProgress = leanRollSmoothingFilter.process(turnSign * leanRollProgress);
|
||||
return ROLL_MAX * leanRollProgress;
|
||||
}
|
||||
|
||||
// animate the avatar using sine waves, geometric waveforms and harmonic generators
|
||||
function renderMotion() {
|
||||
// leaning in response to speed and acceleration
|
||||
var leanPitch = motion.state === STATIC ? 0 : getLeanPitch();
|
||||
var leanRoll = motion.state === STATIC ? 0 : getLeanRoll();
|
||||
var lastDirection = motion.lastDirection;
|
||||
// hips translations from currently playing animations
|
||||
var hipsTranslations = {x:0, y:0, z:0};
|
||||
|
||||
if (motion.currentTransition !== nullTransition) {
|
||||
// maintain previous direction when transitioning from a walk
|
||||
if (motion.currentTransition.lastAnimation === avatar.selectedWalkBlend) {
|
||||
motion.lastDirection = motion.currentTransition.lastDirection;
|
||||
}
|
||||
hipsTranslations = motion.currentTransition.blendTranslations(motion.frequencyTimeWheelPos,
|
||||
motion.lastDirection);
|
||||
} else {
|
||||
hipsTranslations = animationOperations.calculateTranslations(avatar.currentAnimation,
|
||||
motion.frequencyTimeWheelPos,
|
||||
motion.direction);
|
||||
}
|
||||
// factor any leaning into the hips offset
|
||||
hipsTranslations.z += avatar.calibration.hipsToFeet * Math.sin(filter.degToRad(leanPitch));
|
||||
hipsTranslations.x += avatar.calibration.hipsToFeet * Math.sin(filter.degToRad(leanRoll));
|
||||
|
||||
// ensure skeleton offsets are within the 1m limit
|
||||
hipsTranslations.x = hipsTranslations.x > 1 ? 1 : hipsTranslations.x;
|
||||
hipsTranslations.x = hipsTranslations.x < -1 ? -1 : hipsTranslations.x;
|
||||
hipsTranslations.y = hipsTranslations.y > 1 ? 1 : hipsTranslations.y;
|
||||
hipsTranslations.y = hipsTranslations.y < -1 ? -1 : hipsTranslations.y;
|
||||
hipsTranslations.z = hipsTranslations.z > 1 ? 1 : hipsTranslations.z;
|
||||
hipsTranslations.z = hipsTranslations.z < -1 ? -1 : hipsTranslations.z;
|
||||
// apply translations
|
||||
MyAvatar.setSkeletonOffset(hipsTranslations);
|
||||
|
||||
// play footfall sound?
|
||||
var producingFootstepSounds = (avatar.currentAnimation === avatar.selectedWalkBlend) && avatar.makesFootStepSounds;
|
||||
|
||||
if (motion.currentTransition !== nullTransition && avatar.makesFootStepSounds) {
|
||||
if (motion.currentTransition.nextAnimation === avatar.selectedWalkBlend ||
|
||||
motion.currentTransition.lastAnimation === avatar.selectedWalkBlend) {
|
||||
producingFootstepSounds = true;
|
||||
}
|
||||
}
|
||||
if (producingFootstepSounds) {
|
||||
const QUARTER_CYCLE = 90;
|
||||
const THREE_QUARTER_CYCLE = 270;
|
||||
var ftWheelPosition = motion.frequencyTimeWheelPos;
|
||||
|
||||
if (motion.currentTransition !== nullTransition &&
|
||||
motion.currentTransition.lastAnimation === avatar.selectedWalkBlend) {
|
||||
ftWheelPosition = motion.currentTransition.lastFrequencyTimeWheelPos;
|
||||
}
|
||||
if (avatar.nextStep === LEFT && ftWheelPosition > THREE_QUARTER_CYCLE) {
|
||||
avatar.makeFootStepSound();
|
||||
} else if (avatar.nextStep === RIGHT && (ftWheelPosition < THREE_QUARTER_CYCLE && ftWheelPosition > QUARTER_CYCLE)) {
|
||||
avatar.makeFootStepSound();
|
||||
}
|
||||
}
|
||||
|
||||
// apply joint rotations
|
||||
for (jointName in avatar.currentAnimation.joints) {
|
||||
var joint = walkAssets.animationReference.joints[jointName];
|
||||
var jointRotations = undefined;
|
||||
|
||||
// ignore arms / head rotations if options are selected in the settings
|
||||
if (avatar.armsFree && (joint.IKChain === "LeftArm" || joint.IKChain === "RightArm")) {
|
||||
continue;
|
||||
}
|
||||
if (avatar.headFree && joint.IKChain === "Head") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if there's a live transition, blend the rotations with the last animation's rotations
|
||||
if (motion.currentTransition !== nullTransition) {
|
||||
jointRotations = motion.currentTransition.blendRotations(jointName,
|
||||
motion.frequencyTimeWheelPos,
|
||||
motion.lastDirection);
|
||||
} else {
|
||||
jointRotations = animationOperations.calculateRotations(jointName,
|
||||
avatar.currentAnimation,
|
||||
motion.frequencyTimeWheelPos,
|
||||
motion.direction);
|
||||
}
|
||||
|
||||
// apply angular velocity and speed induced leaning
|
||||
if (jointName === "Hips") {
|
||||
jointRotations.x += leanPitch;
|
||||
jointRotations.z += leanRoll;
|
||||
}
|
||||
|
||||
// apply rotations
|
||||
MyAvatar.setJointRotation(jointName, Quat.fromVec3Degrees(jointRotations));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue