From 09d3c511205364a69c89f4dd5df16614e20dafab Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 19 Sep 2014 16:54:03 -0700 Subject: [PATCH 1/2] add multiply(float, Vec3) because multiply commutes --- libraries/script-engine/src/Vec3.cpp | 4 ++++ libraries/script-engine/src/Vec3.h | 1 + 2 files changed, 5 insertions(+) diff --git a/libraries/script-engine/src/Vec3.cpp b/libraries/script-engine/src/Vec3.cpp index 0cbb43f89a..7589ade3b6 100644 --- a/libraries/script-engine/src/Vec3.cpp +++ b/libraries/script-engine/src/Vec3.cpp @@ -25,6 +25,10 @@ glm::vec3 Vec3::multiply(const glm::vec3& v1, float f) { return v1 * f; } +glm::vec3 Vec3::multiply(float f, const glm::vec3& v1) { + return v1 * f; +} + glm::vec3 Vec3::multiplyQbyV(const glm::quat& q, const glm::vec3& v) { return q * v; } diff --git a/libraries/script-engine/src/Vec3.h b/libraries/script-engine/src/Vec3.h index 5a3eeca7be..598f9be432 100644 --- a/libraries/script-engine/src/Vec3.h +++ b/libraries/script-engine/src/Vec3.h @@ -28,6 +28,7 @@ public slots: glm::vec3 cross(const glm::vec3& v1, const glm::vec3& v2); float dot(const glm::vec3& v1, const glm::vec3& v2); glm::vec3 multiply(const glm::vec3& v1, float f); + glm::vec3 multiply(float, const glm::vec3& v1); glm::vec3 multiplyQbyV(const glm::quat& q, const glm::vec3& v); glm::vec3 sum(const glm::vec3& v1, const glm::vec3& v2); glm::vec3 subtract(const glm::vec3& v1, const glm::vec3& v2); From 57dd9fba285f7849bb0c3cf8accc5d8813d1ce84 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 19 Sep 2014 16:55:04 -0700 Subject: [PATCH 2/2] adding move.js, removing swissArmyJetpack.js --- examples/move.js | 171 ++++++++++++++++++++ examples/swissArmyJetpack.js | 294 ----------------------------------- 2 files changed, 171 insertions(+), 294 deletions(-) create mode 100755 examples/move.js delete mode 100644 examples/swissArmyJetpack.js diff --git a/examples/move.js b/examples/move.js new file mode 100755 index 0000000000..e3fc5806b6 --- /dev/null +++ b/examples/move.js @@ -0,0 +1,171 @@ +// +// move.js +// +// Created by AndrewMeadows, 2014.09.17 +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// +// The avatar can be controlled by setting two motor parameters: motorVelocity and motorTimescale. +// Once the motorVelocity is set the avatar will try to move in that direction and speed. The +// motorTimescale is the approximate amount of time it takes for the avatar to reach within 1/e of its +// motorVelocity, so a short timescale makes it ramp up fast, and a long timescale makes it slow. + + +// These parameters control the motor's speed and strength. +var MAX_MOTOR_TIMESCALE = 0.5; +var PUSHING_MOTOR_TIMESCALE = 0.25; +var BRAKING_MOTOR_TIMESCALE = 0.125; +var VERY_LONG_TIME = 1000000.0; + +var AVATAR_SPEED = 4.0; +var MIN_BRAKING_SPEED = 0.2; + + +var motorAccumulator = {x:0.0, y:0.0, z:0.0}; +var isBraking = false; + +// There is a bug in Qt-5.3.0 (and below) that prevents QKeyEvent.isAutoRepeat from being correctly set. +// This means we can't tell when a held button is actually released -- all we get are the repeated +// keyPress- (and keyRelease-) events. So what we have to do is keep a list of last press timestamps +// for buttons of interest, and then periodically scan that list for old timestamps and drop any that +// have expired (at which point we actually remove that buttons effect on the motor). As long as the +// check period is longer than the time between key repeats then things will be smooth, and as long +// as the expiry time is short enough then the stop won't feel too laggy. + +var MAX_AUTO_REPEAT_DELAY = 3; +var KEY_RELEASE_EXPIRY_MSEC = 100; + +// KeyInfo class contructor: +function KeyInfo(contribution) { + this.motorContribution = contribution; // Vec3 contribution of this key toward motorVelocity + this.releaseTime = new Date(); // time when this button was released + this.pressTime = new Date(); // time when this button was pressed + this.isPressed = false; +} + +// NOTE: the avatar's default orientation is such that "forward" is along the -zAxis, and "left" is along -xAxis. +var controlKeys = { + "UP" : new KeyInfo({x: 0.0, y: 0.0, z:-1.0}), + "DOWN" : new KeyInfo({x: 0.0, y: 0.0, z: 1.0}), + "SHIFT+LEFT" : new KeyInfo({x:-1.0, y: 0.0, z: 0.0}), + "SHIFT+RIGHT": new KeyInfo({x: 1.0, y: 0.0, z: 0.0}), + "w" : new KeyInfo({x: 0.0, y: 0.0, z:-1.0}), + "s" : new KeyInfo({x: 0.0, y: 0.0, z: 1.0}), + "e" : new KeyInfo({x: 0.0, y: 1.0, z: 0.0}), + "c" : new KeyInfo({x: 0.0, y:-1.0, z: 0.0}) +}; + +// list of last timestamps when various buttons were last pressed +var pressTimestamps = {}; + +function keyPressEvent(event) { + // NOTE: we're harvesting some of the same keyboard controls that are used by the default (scriptless) + // avatar control. The scriptless control can be disabled via the Menu, thereby allowing this script + // to be the ONLY controller of the avatar position. + + var keyName = event.text; + if (event.isShifted) { + keyName = "SHIFT+" + keyName; + } + + var key = controlKeys[keyName]; + if (key != undefined) { + key.pressTime = new Date(); + // set the last pressTimestap element to undefined (MUCH faster than removing from the list) + pressTimestamps[keyName] = undefined; + var msec = key.pressTime.valueOf() - key.releaseTime.valueOf(); + if (!key.isPressed) { + // add this key's effect to the motorAccumulator + motorAccumulator = Vec3.sum(motorAccumulator, key.motorContribution); + key.isPressed = true; + } + } +} + +function keyReleaseEvent(event) { + var keyName = event.text; + if (event.isShifted) { + keyName = "SHIFT+" + keyName; + } + + var key = controlKeys[keyName]; + if (key != undefined) { + // add key to pressTimestamps + pressTimestamps[keyName] = new Date(); + key.releaseTime = new Date(); + var msec = key.releaseTime.valueOf() - key.pressTime.valueOf(); + } +} + +function updateMotor(deltaTime) { + // remove expired pressTimestamps + var now = new Date(); + for (var keyName in pressTimestamps) { + var t = pressTimestamps[keyName]; + if (t != undefined) { + var msec = now.valueOf() - t.valueOf(); + if (msec > KEY_RELEASE_EXPIRY_MSEC) { + // the release of this key is now official, and we remove it from the motorAccumulator + motorAccumulator = Vec3.subtract(motorAccumulator, controlKeys[keyName].motorContribution); + controlKeys[keyName].isPressed = false; + // set the last pressTimestap element to undefined (MUCH faster than removing from the list) + pressTimestamps[keyName] = undefined; + } + } + } + + var motorVelocity = {x:0.0, y:0.0, z:0.0}; + + // figure out if we're pushing or braking + var accumulatorLength = Vec3.length(motorAccumulator); + var isPushing = false; + if (accumulatorLength == 0.0) { + if (!isBraking) { + isBraking = true; + } + isPushing = false; + } else { + isPushing = true; + motorVelocity = Vec3.multiply(AVATAR_SPEED / accumulatorLength, motorAccumulator); + } + + // compute the timescale + var motorTimescale = MAX_MOTOR_TIMESCALE; + if (isBraking) { + var speed = Vec3.length(MyAvatar.getVelocity()); + if (speed < MIN_BRAKING_SPEED) { + // we're going slow enough to turn off braking + // --> we'll drift to a halt, but not so stiffly that we can't be bumped + isBraking = false; + motorTimescale = MAX_MOTOR_TIMESCALE; + } else { + // we're still braking + motorTimescale = BRAKING_MOTOR_TIMESCALE; + } + } else if (isPushing) { + motorTimescale = PUSHING_MOTOR_TIMESCALE; + } + + // apply the motor parameters + MyAvatar.motorVelocity = motorVelocity; + MyAvatar.motorTimescale = motorTimescale; +} + +function scriptEnding() { + // disable the motor + MyAvatar.motorVelocity = {x:0.0, y:0.0, z:0.0} + MyAvatar.motorTimescale = VERY_LONG_TIME; +} + + +// init stuff +MyAvatar.motorReferenceFrame = "camera"; // "camera" is default, other options are "avatar" and "world" +MyAvatar.motorTimescale = VERY_LONG_TIME; + +// connect callbacks +Controller.keyPressEvent.connect(keyPressEvent); +Controller.keyReleaseEvent.connect(keyReleaseEvent); +Script.update.connect(updateMotor); +Script.scriptEnding.connect(scriptEnding) diff --git a/examples/swissArmyJetpack.js b/examples/swissArmyJetpack.js deleted file mode 100644 index 4cb68e9b64..0000000000 --- a/examples/swissArmyJetpack.js +++ /dev/null @@ -1,294 +0,0 @@ -// -// swissArmyJetpack.js -// examples -// -// Created by Andrew Meadows 2014.04.24 -// Copyright 2014 High Fidelity, Inc. -// -// This is a work in progress. It will eventually be able to move the avatar around, -// toggle collision groups, modify avatar movement options, and other stuff (maybe trigger animations). -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -// misc global constants -var NUMBER_OF_COLLISION_BUTTONS = 3; -var NUMBER_OF_BUTTONS = 4; -var DOWN = { x: 0.0, y: -1.0, z: 0.0 }; -var MAX_VOXEL_SCAN_DISTANCE = 30.0; - -// behavior transition thresholds -var MIN_FLYING_SPEED = 3.0; -var MIN_COLLISIONLESS_SPEED = 5.0; -var MAX_WALKING_SPEED = 30.0; -var MAX_COLLIDABLE_SPEED = 35.0; - -// button URL and geometry/UI tuning -var BUTTON_IMAGE_URL = "http://highfidelity-public.s3-us-west-1.amazonaws.com/images/testing-swatches.svg"; -var DISABLED_OFFSET_Y = 12; -var ENABLED_OFFSET_Y = 55 + 12; -var UI_BUFFER = 1; -var OFFSET_X = UI_BUFFER; -var OFFSET_Y = 200; -var BUTTON_WIDTH = 30; -var BUTTON_HEIGHT = 30; -var TEXT_OFFSET_X = OFFSET_X + BUTTON_WIDTH + UI_BUFFER; -var TEXT_HEIGHT = BUTTON_HEIGHT; -var TEXT_WIDTH = 210; - -var MSEC_PER_SECOND = 1000; -var RAYCAST_EXPIRY_PERIOD = MSEC_PER_SECOND / 16; -var COLLISION_EXPIRY_PERIOD = 2 * MSEC_PER_SECOND; -var GRAVITY_ON_EXPIRY_PERIOD = MSEC_PER_SECOND / 2; -var GRAVITY_OFF_EXPIRY_PERIOD = MSEC_PER_SECOND / 8; - -var dater = new Date(); -var raycastExpiry = dater.getTime() + RAYCAST_EXPIRY_PERIOD; -var gravityOnExpiry = dater.getTime() + GRAVITY_ON_EXPIRY_PERIOD; -var gravityOffExpiry = dater.getTime() + GRAVITY_OFF_EXPIRY_PERIOD; -var collisionOnExpiry = dater.getTime() + COLLISION_EXPIRY_PERIOD; - -// avatar state -var velocity = { x: 0.0, y: 0.0, z: 0.0 }; -var standing = false; - -// speedometer globals -var speed = 0.0; -var lastPosition = MyAvatar.position; -var speedometer = Overlays.addOverlay("text", { - x: OFFSET_X, - y: OFFSET_Y - BUTTON_HEIGHT, - width: BUTTON_WIDTH + UI_BUFFER + TEXT_WIDTH, - height: TEXT_HEIGHT, - color: { red: 0, green: 0, blue: 0 }, - textColor: { red: 255, green: 0, blue: 0}, - topMargin: 4, - leftMargin: 4, - text: "Speed: 0.0" - }); - -// collision group buttons -var buttons = new Array(); -var labels = new Array(); - -var labelContents = new Array(); -labelContents[0] = "Collide with Avatars"; -labelContents[1] = "Collide with Voxels"; -labelContents[2] = "Collide with Particles"; -labelContents[3] = "Use local gravity"; -var groupBits = 0; - -var enabledColors = new Array(); -enabledColors[0] = { red: 255, green: 0, blue: 0}; -enabledColors[1] = { red: 0, green: 255, blue: 0}; -enabledColors[2] = { red: 0, green: 0, blue: 255}; -enabledColors[3] = { red: 255, green: 255, blue: 0}; - -var disabledColors = new Array(); -disabledColors[0] = { red: 90, green: 75, blue: 75}; -disabledColors[1] = { red: 75, green: 90, blue: 75}; -disabledColors[2] = { red: 75, green: 75, blue: 90}; -disabledColors[3] = { red: 90, green: 90, blue: 75}; - -var buttonStates = new Array(); - -for (i = 0; i < NUMBER_OF_BUTTONS; i++) { - var offsetS = 12 - var offsetT = DISABLED_OFFSET_Y; - - buttons[i] = Overlays.addOverlay("image", { - x: OFFSET_X, - y: OFFSET_Y + (BUTTON_HEIGHT * i), - width: BUTTON_WIDTH, - height: BUTTON_HEIGHT, - subImage: { x: offsetS, y: offsetT, width: BUTTON_WIDTH, height: BUTTON_HEIGHT }, - imageURL: BUTTON_IMAGE_URL, - color: disabledColors[i], - alpha: 1, - }); - - labels[i] = Overlays.addOverlay("text", { - x: TEXT_OFFSET_X, - y: OFFSET_Y + (BUTTON_HEIGHT * i), - width: TEXT_WIDTH, - height: TEXT_HEIGHT, - color: { red: 0, green: 0, blue: 0}, - textColor: { red: 255, green: 0, blue: 0}, - topMargin: 4, - leftMargin: 4, - text: labelContents[i] - }); - - buttonStates[i] = false; -} - - -// functions - -function updateButton(i, enabled) { - var offsetY = DISABLED_OFFSET_Y; - var buttonColor = disabledColors[i]; - if (enabled) { - offsetY = ENABLED_OFFSET_Y; - buttonColor = enabledColors[i]; - if (i == 0) { - groupBits |= COLLISION_GROUP_AVATARS; - } else if (i == 1) { - groupBits |= COLLISION_GROUP_VOXELS; - } else if (i == 2) { - groupBits |= COLLISION_GROUP_PARTICLES; - } - } else { - if (i == 0) { - groupBits &= ~COLLISION_GROUP_AVATARS; - } else if (i == 1) { - groupBits &= ~COLLISION_GROUP_VOXELS; - } else if (i == 2) { - groupBits &= ~COLLISION_GROUP_PARTICLES; - } - } - if (groupBits != MyAvatar.collisionGroups) { - MyAvatar.collisionGroups = groupBits; - } - - Overlays.editOverlay(buttons[i], { subImage: { y: offsetY } } ); - Overlays.editOverlay(buttons[i], { color: buttonColor } ); - buttonStates[i] = enabled; -} - - -// When our script shuts down, we should clean up all of our overlays -function scriptEnding() { - for (i = 0; i < NUMBER_OF_BUTTONS; i++) { - Overlays.deleteOverlay(buttons[i]); - Overlays.deleteOverlay(labels[i]); - } - Overlays.deleteOverlay(speedometer); -} -Script.scriptEnding.connect(scriptEnding); - - -function updateSpeedometerDisplay() { - Overlays.editOverlay(speedometer, { text: "Speed: " + speed.toFixed(2) }); -} -Script.setInterval(updateSpeedometerDisplay, 100); - -function disableArtificialGravity() { - MyAvatar.motionBehaviors = MyAvatar.motionBehaviors & ~AVATAR_MOTION_OBEY_LOCAL_GRAVITY; - updateButton(3, false); -} -// call this immediately so that avatar doesn't fall before voxel data arrives -// Ideally we would only do this on LOGIN, not when starting the script -// in the middle of a session. -disableArtificialGravity(); - -function enableArtificialGravity() { - // NOTE: setting the gravity automatically sets the AVATAR_MOTION_OBEY_LOCAL_GRAVITY behavior bit. - MyAvatar.gravity = DOWN; - updateButton(3, true); - // also enable collisions with voxels - groupBits |= COLLISION_GROUP_VOXELS; - updateButton(1, groupBits & COLLISION_GROUP_VOXELS); -} - -// Our update() function is called at approximately 60fps, and we will use it to animate our various overlays -function update(deltaTime) { - if (groupBits != MyAvatar.collisionGroups) { - groupBits = MyAvatar.collisionGroups; - updateButton(0, groupBits & COLLISION_GROUP_AVATARS); - updateButton(1, groupBits & COLLISION_GROUP_VOXELS); - updateButton(2, groupBits & COLLISION_GROUP_PARTICLES); - } - - // measure speed - var distance = Vec3.distance(MyAvatar.position, lastPosition); - speed = 0.8 * speed + 0.2 * distance / deltaTime; - lastPosition = MyAvatar.position; - - dater = new Date(); - var now = dater.getTime(); - - // transition gravity - if (raycastExpiry < now) { - // scan for landing platform - ray = { origin: MyAvatar.position, direction: DOWN }; - var intersection = Voxels.findRayIntersection(ray); - // NOTE: it is possible for intersection.intersects to be false when it should be true - // (perhaps the raycast failed to lock the octree thread?). To workaround this problem - // we only transition on repeated failures. - - if (intersection.intersects) { - // compute distance to voxel - var v = intersection.voxel; - var maxCorner = Vec3.sum({ x: v.x, y: v.y, z: v.z }, {x: v.s, y: v.s, z: v.s }); - var distance = lastPosition.y - maxCorner.y; - - if (distance < MAX_VOXEL_SCAN_DISTANCE) { - if (speed < MIN_FLYING_SPEED && - gravityOnExpiry < now && - !(MyAvatar.motionBehaviors & AVATAR_MOTION_OBEY_LOCAL_GRAVITY)) { - enableArtificialGravity(); - } - if (speed < MAX_WALKING_SPEED) { - gravityOffExpiry = now + GRAVITY_OFF_EXPIRY_PERIOD; - } else if (gravityOffExpiry < now && MyAvatar.motionBehaviors & AVATAR_MOTION_OBEY_LOCAL_GRAVITY) { - disableArtificialGravity(); - } - } else { - // distance too far - if (gravityOffExpiry < now && MyAvatar.motionBehaviors & AVATAR_MOTION_OBEY_LOCAL_GRAVITY) { - disableArtificialGravity(); - } - gravityOnExpiry = now + GRAVITY_ON_EXPIRY_PERIOD; - } - } else { - // no intersection - if (gravityOffExpiry < now && MyAvatar.motionBehaviors & AVATAR_MOTION_OBEY_LOCAL_GRAVITY) { - disableArtificialGravity(); - } - gravityOnExpiry = now + GRAVITY_ON_EXPIRY_PERIOD; - } - } - if (speed > MAX_WALKING_SPEED && gravityOffExpiry < now) { - if (MyAvatar.motionBehaviors & AVATAR_MOTION_OBEY_LOCAL_GRAVITY) { - // turn off gravity - MyAvatar.motionBehaviors = MyAvatar.motionBehaviors & ~AVATAR_MOTION_OBEY_LOCAL_GRAVITY; - updateButton(3, false); - } - gravityOnExpiry = now + GRAVITY_ON_EXPIRY_PERIOD; - } - - // transition collidability with voxels - if (speed < MIN_COLLISIONLESS_SPEED) { - if (collisionOnExpiry < now && !(MyAvatar.collisionGroups & COLLISION_GROUP_VOXELS)) { - // TODO: check to make sure not already colliding - // enable collision with voxels - groupBits |= COLLISION_GROUP_VOXELS; - updateButton(1, groupBits & COLLISION_GROUP_VOXELS); - } - } else { - collisionOnExpiry = now + COLLISION_EXPIRY_PERIOD; - } - if (speed > MAX_COLLIDABLE_SPEED) { - if (MyAvatar.collisionGroups & COLLISION_GROUP_VOXELS) { - // disable collisions with voxels - groupBits &= ~COLLISION_GROUP_VOXELS; - updateButton(1, groupBits & COLLISION_GROUP_VOXELS); - } - } -} -Script.update.connect(update); - -// we also handle click detection in our mousePressEvent() -function mousePressEvent(event) { - var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); - for (i = 0; i < NUMBER_OF_COLLISION_BUTTONS; i++) { - if (clickedOverlay == buttons[i]) { - var enabled = !(buttonStates[i]); - updateButton(i, enabled); - } - } -} -Controller.mousePressEvent.connect(mousePressEvent); -