From 556578b8dc83ef130935630410f3994b2fe361a0 Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Tue, 24 Jun 2014 09:30:11 -0700 Subject: [PATCH 01/11] first pass - hair as vertlet strands --- examples/sit.js | 8 +- examples/squeezeHands.js | 4 +- interface/src/Application.cpp | 4 +- interface/src/Util.cpp | 1 + interface/src/avatar/Avatar.cpp | 192 ++++++++++++++++++++++++++++++ interface/src/avatar/Avatar.h | 24 ++++ interface/src/avatar/MyAvatar.cpp | 4 + 7 files changed, 229 insertions(+), 8 deletions(-) diff --git a/examples/sit.js b/examples/sit.js index d10c08c95a..87b4f232ca 100644 --- a/examples/sit.js +++ b/examples/sit.js @@ -49,12 +49,12 @@ var pose = [ {joint:"RightFoot", rotation: {x:30, y:15.0, z:0.0}}, {joint:"LeftUpLeg", rotation: {x:100.0, y:-15.0, z:0.0}}, {joint:"LeftLeg", rotation: {x:-130.0, y:-15.0, z:0.0}}, - {joint:"LeftFoot", rotation: {x:30, y:15.0, z:0.0}}, + {joint:"LeftFoot", rotation: {x:30, y:15.0, z:0.0}} - {joint:"Spine2", rotation: {x:20, y:0.0, z:0.0}}, + //{joint:"Spine2", rotation: {x:20, y:0.0, z:0.0}}, - {joint:"RightShoulder", rotation: {x:0.0, y:40.0, z:0.0}}, - {joint:"LeftShoulder", rotation: {x:0.0, y:-40.0, z:0.0}} + //{joint:"RightShoulder", rotation: {x:0.0, y:40.0, z:0.0}}, + //{joint:"LeftShoulder", rotation: {x:0.0, y:-40.0, z:0.0}} ]; diff --git a/examples/squeezeHands.js b/examples/squeezeHands.js index e53dd9569c..da720734e1 100644 --- a/examples/squeezeHands.js +++ b/examples/squeezeHands.js @@ -9,8 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var rightHandAnimation = "https://s3-us-west-1.amazonaws.com/highfidelity-public/animations/RightHandAnim.fbx"; -var leftHandAnimation = "https://s3-us-west-1.amazonaws.com/highfidelity-public/animations/LeftHandAnim.fbx"; +var rightHandAnimation = "https://s3-us-west-1.amazonaws.com/highfidelity-public/animations/RightHandAnimPhilip.fbx"; +var leftHandAnimation = "https://s3-us-west-1.amazonaws.com/highfidelity-public/animations/LeftHandAnimPhilip.fbx"; var LEFT = 0; var RIGHT = 1; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6b44503af4..9c257a7658 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -133,7 +133,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _nodeThread(new QThread(this)), _datagramProcessor(), _frameCount(0), - _fps(120.0f), + _fps(60.0f), _justStarted(true), _voxelImporter(NULL), _importSucceded(false), @@ -550,7 +550,7 @@ void Application::initializeGL() { } // update before the first render - update(0.0f); + update(1.f / _fps); InfoView::showFirstTime(); } diff --git a/interface/src/Util.cpp b/interface/src/Util.cpp index 07ca65b286..30fda645ca 100644 --- a/interface/src/Util.cpp +++ b/interface/src/Util.cpp @@ -66,6 +66,7 @@ void printVector(glm::vec3 vec) { qDebug("%4.2f, %4.2f, %4.2f", vec.x, vec.y, vec.z); } + // Return the azimuth angle (in radians) between two points. float azimuth_to(glm::vec3 head_pos, glm::vec3 source_pos) { return atan2(head_pos.x - source_pos.x, head_pos.z - source_pos.z); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index baf46605fd..95399062ae 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -48,6 +48,10 @@ Avatar::Avatar() : _skeletonModel(this), _bodyYawDelta(0.0f), _velocity(0.0f, 0.0f, 0.0f), + _lastVelocity(0.0f, 0.0f, 0.0f), + _acceleration(0.0f, 0.0f, 0.0f), + _angularVelocity(0.0f, 0.0f, 0.0f), + _lastOrientation(), _leanScale(0.5f), _scale(1.0f), _worldUpDirection(DEFAULT_UP_DIRECTION), @@ -76,6 +80,7 @@ void Avatar::init() { _skeletonModel.init(); _initialized = true; _shouldRenderBillboard = (getLODDistance() >= BILLBOARD_LOD_DISTANCE); + initializeHair(); } glm::vec3 Avatar::getChestPosition() const { @@ -134,10 +139,13 @@ void Avatar::simulate(float deltaTime) { head->setPosition(headPosition); head->setScale(_scale); head->simulate(deltaTime, false, _shouldRenderBillboard); + + simulateHair(deltaTime); } // update position by velocity, and subtract the change added earlier for gravity _position += _velocity * deltaTime; + updateAcceleration(deltaTime); // update animation for display name fade in/out if ( _displayNameTargetAlpha != _displayNameAlpha) { @@ -157,6 +165,17 @@ void Avatar::simulate(float deltaTime) { } } +void Avatar::updateAcceleration(float deltaTime) { + // Linear Component of Acceleration + _acceleration = (_velocity - _lastVelocity) * (1.f / deltaTime); + _lastVelocity = _velocity; + // Angular Component of Acceleration + glm::quat orientation = getOrientation(); + glm::quat delta = glm::inverse(_lastOrientation) * orientation; + _angularVelocity = safeEulerAngles(delta) * (1.f / deltaTime); + _lastOrientation = getOrientation(); +} + void Avatar::setMouseRay(const glm::vec3 &origin, const glm::vec3 &direction) { _mouseRayOrigin = origin; _mouseRayDirection = direction; @@ -361,6 +380,179 @@ void Avatar::renderBody(RenderMode renderMode, float glowLevel) { getHand()->render(false, modelRenderMode); } getHead()->render(1.0f, modelRenderMode); + renderHair(); +} + +const float HAIR_LENGTH = 0.5f; +const float HAIR_LINK_LENGTH = HAIR_LENGTH / HAIR_LINKS; +const float HAIR_DAMPING = 0.99f; +const float HEAD_RADIUS = 0.20f; +const float COLLISION_RELAXATION = 10.f; +const float CONSTRAINT_RELAXATION = 10.0f; +const float NOISE = 0.0f; // 0.1f; +const float NOISE_MAGNITUDE = 0.02f; +const glm::vec3 HAIR_GRAVITY(0.f, -0.05f, 0.f); +const float HAIR_ACCELERATION_COUPLING = 0.025f; +const float HAIR_ANGULAR_VELOCITY_COUPLING = 0.15f; +const float HAIR_MAX_LINEAR_ACCELERATION = 5.f; +const float HAIR_THICKNESS = 0.015f; +const glm::vec3 HAIR_COLOR1(0.98f, 0.92f, 0.843f); +const glm::vec3 HAIR_COLOR2(0.545f, 0.533f, 0.47f); + +void Avatar::renderHair() { + // + // Render the avatar's moveable hair + // + glm::vec3 headPosition = getHead()->getPosition(); + + glPushMatrix(); + glTranslatef(headPosition.x, headPosition.y, headPosition.z); + const glm::quat& rotation = getHead()->getFinalOrientationInWorldFrame(); + glm::vec3 axis = glm::axis(rotation); + glRotatef(glm::degrees(glm::angle(rotation)), axis.x, axis.y, axis.z); + + glBegin(GL_QUADS); + for (int strand = 0; strand < HAIR_STRANDS; strand++) { + for (int link = 0; link < HAIR_LINKS - 1; link++) { + int vertexIndex = strand * HAIR_LINKS + link; + glColor3fv(&_hairColors[vertexIndex].x); + glNormal3fv(&_hairNormals[vertexIndex].x); + glVertex3f(_hairPosition[vertexIndex].x - _hairQuadDelta[vertexIndex].x, + _hairPosition[vertexIndex].y - _hairQuadDelta[vertexIndex].y, + _hairPosition[vertexIndex].z - _hairQuadDelta[vertexIndex].z); + glVertex3f(_hairPosition[vertexIndex].x + _hairQuadDelta[vertexIndex].x, + _hairPosition[vertexIndex].y + _hairQuadDelta[vertexIndex].y, + _hairPosition[vertexIndex].z + _hairQuadDelta[vertexIndex].z); + + glVertex3f(_hairPosition[vertexIndex + 1].x + _hairQuadDelta[vertexIndex].x, + _hairPosition[vertexIndex + 1].y + _hairQuadDelta[vertexIndex].y, + _hairPosition[vertexIndex + 1].z + _hairQuadDelta[vertexIndex].z); + glVertex3f(_hairPosition[vertexIndex + 1].x - _hairQuadDelta[vertexIndex].x, + _hairPosition[vertexIndex + 1].y - _hairQuadDelta[vertexIndex].y, + _hairPosition[vertexIndex + 1].z - _hairQuadDelta[vertexIndex].z); + } + } + glEnd(); + + //glColor4f(1.0f, 0.0f, 0.0f, 0.5f); + //glutSolidSphere(HEAD_RADIUS, 20, 20); + glPopMatrix(); + +} + +void Avatar::simulateHair(float deltaTime) { + deltaTime = glm::clamp(deltaTime, 0.f, 1.f / 30.f); + glm::vec3 acceleration = getAcceleration(); + if (glm::length(acceleration) > HAIR_MAX_LINEAR_ACCELERATION) { + acceleration = glm::normalize(acceleration) * HAIR_MAX_LINEAR_ACCELERATION; + } + const glm::quat& rotation = getHead()->getFinalOrientationInWorldFrame(); + acceleration = acceleration * rotation; + glm::vec3 angularVelocity = getAngularVelocity() + getHead()->getAngularVelocity(); + + for (int strand = 0; strand < HAIR_STRANDS; strand++) { + for (int link = 0; link < HAIR_LINKS; link++) { + int vertexIndex = strand * HAIR_LINKS + link; + if (vertexIndex % HAIR_LINKS == 0) { + // Base Joint - no integration + if (randFloat() < NOISE) { + // Move base of hair + _hairPosition[vertexIndex] += randVector() * NOISE_MAGNITUDE; + } + } else { + // + // Vertlet Integration + // + // Add velocity from last position, with damping + glm::vec3 thisPosition = _hairPosition[vertexIndex]; + glm::vec3 diff = thisPosition - _hairLastPosition[vertexIndex]; + _hairPosition[vertexIndex] += diff * HAIR_DAMPING; + // Attempt to resolve collision with head sphere + if (glm::length(_hairPosition[vertexIndex]) < HEAD_RADIUS) { + _hairPosition[vertexIndex] += glm::normalize(_hairPosition[vertexIndex]) * + (HEAD_RADIUS - glm::length(_hairPosition[vertexIndex])) * COLLISION_RELAXATION * deltaTime; + } + // Add a little gravity + _hairPosition[vertexIndex] += HAIR_GRAVITY * rotation * deltaTime; + + // Add linear acceleration of the avatar body + _hairPosition[vertexIndex] -= acceleration * HAIR_ACCELERATION_COUPLING * deltaTime; + + const float ANGULAR_VELOCITY_MIN = 0.001f; + // Add angular acceleration of the avatar body + if (glm::length(angularVelocity) > ANGULAR_VELOCITY_MIN) { + glm::vec3 yawVector = _hairPosition[vertexIndex]; + yawVector.y = 0.f; + if (glm::length(yawVector) > EPSILON) { + float radius = glm::length(yawVector); + yawVector = glm::normalize(yawVector); + float angle = atan2f(yawVector.x, -yawVector.z) + PI; + glm::vec3 delta = glm::vec3(-1.f, 0.f, 0.f) * glm::angleAxis(angle, glm::vec3(0, 1, 0)); + _hairPosition[vertexIndex] -= delta * radius * angularVelocity.y * HAIR_ANGULAR_VELOCITY_COUPLING * deltaTime; + } + } + + // Iterate length constraints to other links + for (int link = 0; link < HAIR_MAX_CONSTRAINTS; link++) { + if (_hairConstraints[vertexIndex * HAIR_MAX_CONSTRAINTS + link] > -1) { + // If there is a constraint, try to enforce it + glm::vec3 vectorBetween = _hairPosition[_hairConstraints[vertexIndex * HAIR_MAX_CONSTRAINTS + link]] - _hairPosition[vertexIndex]; + _hairPosition[vertexIndex] += glm::normalize(vectorBetween) * (glm::length(vectorBetween) - HAIR_LINK_LENGTH) * CONSTRAINT_RELAXATION * deltaTime; + } + } + // Store start position for next vertlet pass + _hairLastPosition[vertexIndex] = thisPosition; + } + } + } +} + +void Avatar::initializeHair() { + const float FACE_WIDTH = 0.25f * PI; + glm::vec3 thisVertex; + for (int strand = 0; strand < HAIR_STRANDS; strand++) { + float strandAngle = randFloat() * PI; + float azimuth = randFloat() * 2.f * PI; + float elevation; + if ((azimuth > FACE_WIDTH) || (azimuth < -FACE_WIDTH)) { + elevation = randFloat() * PI_OVER_TWO; + } else { + elevation = (PI_OVER_TWO / 2.f) + randFloat() * (PI_OVER_TWO / 2.f); + } + glm::vec3 thisStrand(sinf(azimuth) * cosf(elevation), sinf(elevation), -cosf(azimuth) * cosf(elevation)); + thisStrand *= HEAD_RADIUS + 0.01f; + + for (int link = 0; link < HAIR_LINKS; link++) { + int vertexIndex = strand * HAIR_LINKS + link; + // Clear constraints + for (int link = 0; link < HAIR_MAX_CONSTRAINTS; link++) { + _hairConstraints[vertexIndex * HAIR_MAX_CONSTRAINTS + link] = -1; + } + if (vertexIndex % HAIR_LINKS == 0) { + // start of strand + thisVertex = thisStrand; + } else { + thisVertex+= glm::normalize(thisStrand) * HAIR_LINK_LENGTH; + // Set constraints to vertex before and maybe vertex after in strand + _hairConstraints[vertexIndex * HAIR_MAX_CONSTRAINTS] = vertexIndex - 1; + if (link < (HAIR_LINKS - 1)) { + _hairConstraints[vertexIndex * HAIR_MAX_CONSTRAINTS + 1] = vertexIndex + 1; + } + } + _hairPosition[vertexIndex] = thisVertex; + _hairLastPosition[vertexIndex] = _hairPosition[vertexIndex]; + + _hairQuadDelta[vertexIndex] = glm::vec3(cos(strandAngle) * HAIR_THICKNESS, 0.f, sin(strandAngle) * HAIR_THICKNESS); + _hairNormals[vertexIndex] = glm::normalize(randVector()); + if (randFloat() < elevation / PI_OVER_TWO) { + _hairColors[vertexIndex] = HAIR_COLOR1 * ((float)(link + 1) / (float)HAIR_LINKS); + } else { + _hairColors[vertexIndex] = HAIR_COLOR2 * ((float)(link + 1) / (float)HAIR_LINKS); + } + + } + } + qDebug() << "Initialize Hair"; } bool Avatar::shouldRenderHead(const glm::vec3& cameraPosition, RenderMode renderMode) const { diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index f928881068..cf45cdf07f 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -32,6 +32,10 @@ static const float RESCALING_TOLERANCE = .02f; extern const float CHAT_MESSAGE_SCALE; extern const float CHAT_MESSAGE_HEIGHT; +const int HAIR_STRANDS = 100; // Number of strands of hair +const int HAIR_LINKS = 10; // Number of links in a hair strand +const int HAIR_MAX_CONSTRAINTS = 2; + enum DriveKeys { FWD = 0, BACK, @@ -158,6 +162,9 @@ public: Q_INVOKABLE glm::quat getJointCombinedRotation(int index) const; Q_INVOKABLE glm::quat getJointCombinedRotation(const QString& name) const; + glm::vec3 getAcceleration() const { return _acceleration; } + glm::vec3 getAngularVelocity() const { return _angularVelocity; } + public slots: void updateCollisionGroups(); @@ -169,6 +176,10 @@ protected: QVector _attachmentModels; float _bodyYawDelta; glm::vec3 _velocity; + glm::vec3 _lastVelocity; + glm::vec3 _acceleration; + glm::vec3 _angularVelocity; + glm::quat _lastOrientation; float _leanScale; float _scale; glm::vec3 _worldUpDirection; @@ -185,6 +196,7 @@ protected: glm::vec3 getBodyFrontDirection() const { return getOrientation() * IDENTITY_FRONT; } glm::quat computeRotationFromBodyToWorldUp(float proportion = 1.0f) const; void setScale(float scale); + void updateAcceleration(float deltaTime); float getSkeletonHeight() const; float getHeadHeight() const; @@ -200,6 +212,17 @@ protected: virtual void renderAttachments(RenderMode renderMode); virtual void updateJointMappings(); + + glm::vec3 _hairPosition[HAIR_STRANDS * HAIR_LINKS]; + glm::vec3 _hairLastPosition[HAIR_STRANDS * HAIR_LINKS]; + glm::vec3 _hairQuadDelta[HAIR_STRANDS * HAIR_LINKS]; + glm::vec3 _hairNormals[HAIR_STRANDS * HAIR_LINKS]; + glm::vec3 _hairColors[HAIR_STRANDS * HAIR_LINKS]; + int _hairIsMoveable[HAIR_STRANDS * HAIR_LINKS]; + int _hairConstraints[HAIR_STRANDS * HAIR_LINKS * 2]; // Hair can link to two others + void renderHair(); + void simulateHair(float deltaTime); + void initializeHair(); private: @@ -211,6 +234,7 @@ private: void renderBillboard(); float getBillboardSize() const; + }; #endif // hifi_Avatar_h diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 54ed641d72..66f10e68c6 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -188,6 +188,8 @@ void MyAvatar::simulate(float deltaTime) { head->setScale(_scale); head->simulate(deltaTime, true); } + + simulateHair(deltaTime); // now that we're done stepping the avatar forward in time, compute new collisions if (_collisionGroups != 0) { @@ -814,6 +816,7 @@ void MyAvatar::renderBody(RenderMode renderMode, float glowLevel) { getHead()->render(1.0f, modelRenderMode); } getHand()->render(true, modelRenderMode); + renderHair(); } const float RENDER_HEAD_CUTOFF_DISTANCE = 0.50f; @@ -955,6 +958,7 @@ void MyAvatar::updatePosition(float deltaTime) { } else { _position += _velocity * deltaTime; } + updateAcceleration(deltaTime); } // update moving flag based on speed From 5bb2c3c62fdc96ff1104fff0ed00206b7716f900 Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Tue, 24 Jun 2014 16:14:40 -0700 Subject: [PATCH 02/11] Added in-world test tone JS file --- examples/inWorldTestTone.js | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 examples/inWorldTestTone.js diff --git a/examples/inWorldTestTone.js b/examples/inWorldTestTone.js new file mode 100644 index 0000000000..e4f34d87cd --- /dev/null +++ b/examples/inWorldTestTone.js @@ -0,0 +1,38 @@ +// +// inWorldTestTone.js +// +// +// Created by Philip Rosedale on 5/29/14. +// Copyright 2014 High Fidelity, Inc. +// +// This example script plays a test tone that is useful for debugging audio dropout. 220Hz test tone played at the domain origin. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var sound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/220Sine.wav"); + +var soundPlaying = false; + +function update(deltaTime) { + if (!Audio.isInjectorPlaying(soundPlaying)) { + var options = new AudioInjectionOptions(); + options.position = { x:0, y:0, z:0 }; + options.volume = 1.0; + options.loop = true; + soundPlaying = Audio.playSound(sound, options); + print("Started sound loop"); + } +} + +function scriptEnding() { + if (Audio.isInjectorPlaying(soundPlaying)) { + Audio.stopInjector(soundPlaying); + print("Stopped sound loop"); + } +} + +Script.update.connect(update); +Script.scriptEnding.connect(scriptEnding); + From 9eb91e2b4498ad0d07e37cf387258df82e4398a0 Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Tue, 24 Jun 2014 16:15:16 -0700 Subject: [PATCH 03/11] tweaking params --- interface/src/avatar/Avatar.cpp | 31 ++++++++++++++++++++++++++++--- interface/src/avatar/Avatar.h | 2 +- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 95399062ae..14e01536e2 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -383,10 +383,10 @@ void Avatar::renderBody(RenderMode renderMode, float glowLevel) { renderHair(); } -const float HAIR_LENGTH = 0.5f; +const float HAIR_LENGTH = 0.4f; const float HAIR_LINK_LENGTH = HAIR_LENGTH / HAIR_LINKS; const float HAIR_DAMPING = 0.99f; -const float HEAD_RADIUS = 0.20f; +const float HEAD_RADIUS = 0.25f; const float COLLISION_RELAXATION = 10.f; const float CONSTRAINT_RELAXATION = 10.0f; const float NOISE = 0.0f; // 0.1f; @@ -395,9 +395,10 @@ const glm::vec3 HAIR_GRAVITY(0.f, -0.05f, 0.f); const float HAIR_ACCELERATION_COUPLING = 0.025f; const float HAIR_ANGULAR_VELOCITY_COUPLING = 0.15f; const float HAIR_MAX_LINEAR_ACCELERATION = 5.f; -const float HAIR_THICKNESS = 0.015f; +const float HAIR_THICKNESS = 0.025f; const glm::vec3 HAIR_COLOR1(0.98f, 0.92f, 0.843f); const glm::vec3 HAIR_COLOR2(0.545f, 0.533f, 0.47f); +const glm::vec3 WIND_DIRECTION(1.0f, -1.0f, 0.f); void Avatar::renderHair() { // @@ -450,6 +451,8 @@ void Avatar::simulateHair(float deltaTime) { acceleration = acceleration * rotation; glm::vec3 angularVelocity = getAngularVelocity() + getHead()->getAngularVelocity(); + float windIntensity = randFloat() * 0.02f; + for (int strand = 0; strand < HAIR_STRANDS; strand++) { for (int link = 0; link < HAIR_LINKS; link++) { int vertexIndex = strand * HAIR_LINKS + link; @@ -478,6 +481,10 @@ void Avatar::simulateHair(float deltaTime) { // Add linear acceleration of the avatar body _hairPosition[vertexIndex] -= acceleration * HAIR_ACCELERATION_COUPLING * deltaTime; + // Add some wind + glm::vec3 wind = WIND_DIRECTION * windIntensity; + _hairPosition[vertexIndex] += wind * deltaTime; + const float ANGULAR_VELOCITY_MIN = 0.001f; // Add angular acceleration of the avatar body if (glm::length(angularVelocity) > ANGULAR_VELOCITY_MIN) { @@ -490,6 +497,24 @@ void Avatar::simulateHair(float deltaTime) { glm::vec3 delta = glm::vec3(-1.f, 0.f, 0.f) * glm::angleAxis(angle, glm::vec3(0, 1, 0)); _hairPosition[vertexIndex] -= delta * radius * angularVelocity.y * HAIR_ANGULAR_VELOCITY_COUPLING * deltaTime; } + glm::vec3 pitchVector = _hairPosition[vertexIndex]; + pitchVector.x = 0.f; + if (glm::length(pitchVector) > EPSILON) { + float radius = glm::length(pitchVector); + pitchVector = glm::normalize(pitchVector); + float angle = atan2f(pitchVector.y, -pitchVector.z) + PI; + glm::vec3 delta = glm::vec3(0.0f, 1.0f, 0.f) * glm::angleAxis(angle, glm::vec3(1, 0, 0)); + _hairPosition[vertexIndex] -= delta * radius * angularVelocity.x * HAIR_ANGULAR_VELOCITY_COUPLING * deltaTime; + } + glm::vec3 rollVector = _hairPosition[vertexIndex]; + rollVector.z = 0.f; + if (glm::length(rollVector) > EPSILON) { + float radius = glm::length(rollVector); + pitchVector = glm::normalize(rollVector); + float angle = atan2f(rollVector.x, rollVector.y) + PI; + glm::vec3 delta = glm::vec3(-1.0f, 0.0f, 0.f) * glm::angleAxis(angle, glm::vec3(0, 0, 1)); + _hairPosition[vertexIndex] -= delta * radius * angularVelocity.z * HAIR_ANGULAR_VELOCITY_COUPLING * deltaTime; + } } // Iterate length constraints to other links diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index cf45cdf07f..bd4416f70d 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -32,7 +32,7 @@ static const float RESCALING_TOLERANCE = .02f; extern const float CHAT_MESSAGE_SCALE; extern const float CHAT_MESSAGE_HEIGHT; -const int HAIR_STRANDS = 100; // Number of strands of hair +const int HAIR_STRANDS = 200; // Number of strands of hair const int HAIR_LINKS = 10; // Number of links in a hair strand const int HAIR_MAX_CONSTRAINTS = 2; From 60c33c18c397c70bc753fc8ba4329c414ee235e5 Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Tue, 24 Jun 2014 20:32:41 -0700 Subject: [PATCH 04/11] Expose mode shift period to JS, remove default value --- examples/inspect.js | 2 ++ interface/src/Camera.cpp | 2 -- interface/src/Camera.h | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/inspect.js b/examples/inspect.js index b292d5f609..a4ff405c3f 100644 --- a/examples/inspect.js +++ b/examples/inspect.js @@ -195,6 +195,8 @@ function keyReleaseEvent(event) { } } + + function mousePressEvent(event) { if (alt && !isActive) { mouseLastX = event.x; diff --git a/interface/src/Camera.cpp b/interface/src/Camera.cpp index 0e33e14f32..4490b60fc9 100644 --- a/interface/src/Camera.cpp +++ b/interface/src/Camera.cpp @@ -317,8 +317,6 @@ void CameraScriptableObject::setMode(const QString& mode) { } if (currentMode != targetMode) { _camera->setMode(targetMode); - const float DEFAULT_MODE_SHIFT_PERIOD = 0.5f; // half second - _camera->setModeShiftPeriod(DEFAULT_MODE_SHIFT_PERIOD); } } diff --git a/interface/src/Camera.h b/interface/src/Camera.h index 5e189c1111..2bbbf0e751 100644 --- a/interface/src/Camera.h +++ b/interface/src/Camera.h @@ -42,9 +42,8 @@ public: void setTargetPosition(const glm::vec3& t); void setTightness(float t) { _tightness = t; } void setTargetRotation(const glm::quat& rotation); - - void setMode(CameraMode m); void setModeShiftPeriod(float r); + void setMode(CameraMode m); void setFieldOfView(float f); void setAspectRatio(float a); void setNearClip(float n); @@ -130,6 +129,7 @@ public: public slots: QString getMode() const; void setMode(const QString& mode); + void setModeShiftPeriod(float r) {_camera->setModeShiftPeriod(r); } void setPosition(const glm::vec3& value) { _camera->setTargetPosition(value);} glm::vec3 getPosition() const { return _camera->getPosition(); } From 88c01266abd1c9190e34e6a7a1d26322c2f6ab8b Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Wed, 25 Jun 2014 03:29:08 -0700 Subject: [PATCH 05/11] =?UTF-8?q?more=20hair=20tweaks,=20don=E2=80=99t=20r?= =?UTF-8?q?ender=20in=201P,=20improved=20toy=20ball?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/toyball.js | 41 +++++++++++++++++++++---------- interface/src/avatar/Avatar.cpp | 39 +++++++++++++++-------------- interface/src/avatar/Avatar.h | 1 + interface/src/avatar/MyAvatar.cpp | 2 +- 4 files changed, 50 insertions(+), 33 deletions(-) diff --git a/examples/toyball.js b/examples/toyball.js index d312c1bc94..5fd522ef4a 100644 --- a/examples/toyball.js +++ b/examples/toyball.js @@ -26,14 +26,21 @@ var RIGHT_TIP = 3; var RIGHT_BUTTON_FWD = 11; var RIGHT_BUTTON_3 = 9; +var BALL_RADIUS = 0.08; +var GRAVITY_STRENGTH = 0.5; + +var HELD_COLOR = { red: 240, green: 0, blue: 0 }; +var THROWN_COLOR = { red: 128, green: 0, blue: 0 }; + var leftBallAlreadyInHand = false; var rightBallAlreadyInHand = false; var leftHandParticle; var rightHandParticle; -var throwSound = new Sound("https://dl.dropboxusercontent.com/u/1864924/hifi-sounds/throw.raw"); +var newSound = new Sound("https://dl.dropboxusercontent.com/u/1864924/hifi-sounds/throw.raw"); var catchSound = new Sound("https://dl.dropboxusercontent.com/u/1864924/hifi-sounds/catch.raw"); -var targetRadius = 0.25; +var throwSound = new Sound("http://highfidelity-public.s3-us-west-1.amazonaws.com/sounds/Switches%20and%20sliders/slider%20-%20whoosh1.raw"); +var targetRadius = 1.0; var wantDebugging = false; @@ -54,7 +61,7 @@ function getBallHoldPosition(whichSide) { tipPosition = Controller.getSpatialControlPosition(RIGHT_TIP); } - var BALL_FORWARD_OFFSET = 0.08; // put the ball a bit forward of fingers + var BALL_FORWARD_OFFSET = 0.15; // put the ball a bit forward of fingers position = { x: BALL_FORWARD_OFFSET * normal.x, y: BALL_FORWARD_OFFSET * normal.y, z: BALL_FORWARD_OFFSET * normal.z }; @@ -69,6 +76,7 @@ function getBallHoldPosition(whichSide) { function checkControllerSide(whichSide) { var BUTTON_FWD; var BUTTON_3; + var TRIGGER; var palmPosition; var ballAlreadyInHand; var handMessage; @@ -76,18 +84,20 @@ function checkControllerSide(whichSide) { if (whichSide == LEFT_PALM) { BUTTON_FWD = LEFT_BUTTON_FWD; BUTTON_3 = LEFT_BUTTON_3; + TRIGGER = 0; palmPosition = Controller.getSpatialControlPosition(LEFT_PALM); ballAlreadyInHand = leftBallAlreadyInHand; handMessage = "LEFT"; } else { BUTTON_FWD = RIGHT_BUTTON_FWD; BUTTON_3 = RIGHT_BUTTON_3; + TRIGGER = 1; palmPosition = Controller.getSpatialControlPosition(RIGHT_PALM); ballAlreadyInHand = rightBallAlreadyInHand; handMessage = "RIGHT"; } - - var grabButtonPressed = (Controller.isButtonPressed(BUTTON_FWD) || Controller.isButtonPressed(BUTTON_3)); + + var grabButtonPressed = (Controller.isButtonPressed(BUTTON_FWD) || Controller.isButtonPressed(BUTTON_3) || (Controller.getTriggerValue(TRIGGER) > 0.5)); // If I don't currently have a ball in my hand, then try to catch closest one if (!ballAlreadyInHand && grabButtonPressed) { @@ -107,8 +117,11 @@ function checkControllerSide(whichSide) { var ballPosition = getBallHoldPosition(whichSide); var properties = { position: { x: ballPosition.x, y: ballPosition.y, - z: ballPosition.z }, - velocity : { x: 0, y: 0, z: 0}, inHand: true }; + z: ballPosition.z }, + color: HELD_COLOR, + velocity : { x: 0, y: 0, z: 0}, + lifetime : 600, + inHand: true }; Particles.editParticle(closestParticle, properties); var options = new AudioInjectionOptions(); @@ -127,7 +140,7 @@ function checkControllerSide(whichSide) { //} // If '3' is pressed, and not holding a ball, make a new one - if (Controller.isButtonPressed(BUTTON_3) && !ballAlreadyInHand) { + if (grabButtonPressed && !ballAlreadyInHand) { var ballPosition = getBallHoldPosition(whichSide); var properties = { position: { x: ballPosition.x, y: ballPosition.y, @@ -135,11 +148,11 @@ function checkControllerSide(whichSide) { velocity: { x: 0, y: 0, z: 0}, gravity: { x: 0, y: 0, z: 0}, inHand: true, - radius: 0.05, + radius: BALL_RADIUS, damping: 0.999, - color: { red: 255, green: 0, blue: 0 }, + color: HELD_COLOR, - lifetime: 10 // 10 seconds - same as default, not needed but here as an example + lifetime: 600 // 10 seconds - same as default, not needed but here as an example }; newParticle = Particles.addParticle(properties); @@ -155,7 +168,7 @@ function checkControllerSide(whichSide) { var options = new AudioInjectionOptions(); options.position = ballPosition; options.volume = 1.0; - Audio.playSound(catchSound, options); + Audio.playSound(newSound, options); return; // exit early } @@ -188,7 +201,9 @@ function checkControllerSide(whichSide) { y: tipVelocity.y * THROWN_VELOCITY_SCALING, z: tipVelocity.z * THROWN_VELOCITY_SCALING } , inHand: false, - gravity: { x: 0, y: -2, z: 0}, + color: THROWN_COLOR, + lifetime: 10, + gravity: { x: 0, y: -GRAVITY_STRENGTH, z: 0}, }; Particles.editParticle(handParticle, properties); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 2f4717dd3b..ee65b9c170 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -379,22 +379,22 @@ void Avatar::renderBody(RenderMode renderMode, float glowLevel) { renderHair(); } -const float HAIR_LENGTH = 0.4f; +const float HAIR_LENGTH = 0.2f; const float HAIR_LINK_LENGTH = HAIR_LENGTH / HAIR_LINKS; const float HAIR_DAMPING = 0.99f; -const float HEAD_RADIUS = 0.25f; +const float HEAD_RADIUS = 0.15f; const float COLLISION_RELAXATION = 10.f; const float CONSTRAINT_RELAXATION = 10.0f; -const float NOISE = 0.0f; // 0.1f; -const float NOISE_MAGNITUDE = 0.02f; -const glm::vec3 HAIR_GRAVITY(0.f, -0.05f, 0.f); +const glm::vec3 HAIR_GRAVITY(0.f, -0.005f, 0.f); const float HAIR_ACCELERATION_COUPLING = 0.025f; -const float HAIR_ANGULAR_VELOCITY_COUPLING = 0.15f; -const float HAIR_MAX_LINEAR_ACCELERATION = 5.f; +const float HAIR_ANGULAR_VELOCITY_COUPLING = 0.10f; +const float HAIR_MAX_LINEAR_ACCELERATION = 4.f; const float HAIR_THICKNESS = 0.025f; +const float HAIR_STIFFNESS = 0.002f; const glm::vec3 HAIR_COLOR1(0.98f, 0.92f, 0.843f); const glm::vec3 HAIR_COLOR2(0.545f, 0.533f, 0.47f); -const glm::vec3 WIND_DIRECTION(1.0f, -1.0f, 0.f); +const glm::vec3 WIND_DIRECTION(0.5f, -1.0f, 0.f); +const float MAX_WIND_STRENGTH = 0.01f; void Avatar::renderHair() { // @@ -447,18 +447,14 @@ void Avatar::simulateHair(float deltaTime) { acceleration = acceleration * rotation; glm::vec3 angularVelocity = getAngularVelocity() + getHead()->getAngularVelocity(); - float windIntensity = randFloat() * 0.02f; + float windIntensity = randFloat() * MAX_WIND_STRENGTH; for (int strand = 0; strand < HAIR_STRANDS; strand++) { for (int link = 0; link < HAIR_LINKS; link++) { int vertexIndex = strand * HAIR_LINKS + link; if (vertexIndex % HAIR_LINKS == 0) { // Base Joint - no integration - if (randFloat() < NOISE) { - // Move base of hair - _hairPosition[vertexIndex] += randVector() * NOISE_MAGNITUDE; - } - } else { + } else { // // Vertlet Integration // @@ -477,6 +473,10 @@ void Avatar::simulateHair(float deltaTime) { // Add linear acceleration of the avatar body _hairPosition[vertexIndex] -= acceleration * HAIR_ACCELERATION_COUPLING * deltaTime; + // Add stiffness (product) + _hairPosition[vertexIndex] += (_hairOriginalPosition[vertexIndex] - _hairPosition[vertexIndex]) + * powf(1.f - link / HAIR_LINKS, 2.f) * HAIR_STIFFNESS; + // Add some wind glm::vec3 wind = WIND_DIRECTION * windIntensity; _hairPosition[vertexIndex] += wind * deltaTime; @@ -529,17 +529,17 @@ void Avatar::simulateHair(float deltaTime) { } void Avatar::initializeHair() { - const float FACE_WIDTH = 0.25f * PI; + const float FACE_WIDTH = PI / 4.0f; glm::vec3 thisVertex; for (int strand = 0; strand < HAIR_STRANDS; strand++) { float strandAngle = randFloat() * PI; - float azimuth = randFloat() * 2.f * PI; - float elevation; - if ((azimuth > FACE_WIDTH) || (azimuth < -FACE_WIDTH)) { + float azimuth = FACE_WIDTH / 2.0f + (randFloat() * (2.0 * PI - FACE_WIDTH)); + float elevation = PI_OVER_TWO - (randFloat() * 0.75 * PI); + /*if ((azimuth > FACE_WIDTH) || (azimuth < -FACE_WIDTH)) { elevation = randFloat() * PI_OVER_TWO; } else { elevation = (PI_OVER_TWO / 2.f) + randFloat() * (PI_OVER_TWO / 2.f); - } + }*/ glm::vec3 thisStrand(sinf(azimuth) * cosf(elevation), sinf(elevation), -cosf(azimuth) * cosf(elevation)); thisStrand *= HEAD_RADIUS + 0.01f; @@ -562,6 +562,7 @@ void Avatar::initializeHair() { } _hairPosition[vertexIndex] = thisVertex; _hairLastPosition[vertexIndex] = _hairPosition[vertexIndex]; + _hairOriginalPosition[vertexIndex] = _hairPosition[vertexIndex]; _hairQuadDelta[vertexIndex] = glm::vec3(cos(strandAngle) * HAIR_THICKNESS, 0.f, sin(strandAngle) * HAIR_THICKNESS); _hairNormals[vertexIndex] = glm::normalize(randVector()); diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index bb38cab5c5..40f92b468c 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -201,6 +201,7 @@ protected: virtual void updateJointMappings(); glm::vec3 _hairPosition[HAIR_STRANDS * HAIR_LINKS]; + glm::vec3 _hairOriginalPosition[HAIR_STRANDS * HAIR_LINKS]; glm::vec3 _hairLastPosition[HAIR_STRANDS * HAIR_LINKS]; glm::vec3 _hairQuadDelta[HAIR_STRANDS * HAIR_LINKS]; glm::vec3 _hairNormals[HAIR_STRANDS * HAIR_LINKS]; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 6938e20313..bbdf041341 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -833,9 +833,9 @@ void MyAvatar::renderBody(RenderMode renderMode, float glowLevel) { // Render head so long as the camera isn't inside it if (shouldRenderHead(Application::getInstance()->getCamera()->getPosition(), renderMode)) { getHead()->render(1.0f, modelRenderMode); + renderHair(); } getHand()->render(true, modelRenderMode); - renderHair(); } const float RENDER_HEAD_CUTOFF_DISTANCE = 0.50f; From 40dee3b39eb01b1adf69b8d56f3eae1212ff8c21 Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Wed, 25 Jun 2014 05:24:22 -0700 Subject: [PATCH 06/11] Added JS calls for left/right estimated palm position on skeleton model, improved toy ball to use them --- examples/toyball.js | 17 +------- interface/src/avatar/Avatar.cpp | 65 ++++++++++++++++++++++++++----- interface/src/avatar/MyAvatar.cpp | 19 +++++++++ interface/src/avatar/MyAvatar.h | 5 ++- 4 files changed, 80 insertions(+), 26 deletions(-) diff --git a/examples/toyball.js b/examples/toyball.js index 5fd522ef4a..e03fd67a5d 100644 --- a/examples/toyball.js +++ b/examples/toyball.js @@ -51,25 +51,12 @@ function debugPrint(message) { } function getBallHoldPosition(whichSide) { - var normal; - var tipPosition; if (whichSide == LEFT_PALM) { - normal = Controller.getSpatialControlNormal(LEFT_PALM); - tipPosition = Controller.getSpatialControlPosition(LEFT_TIP); + position = MyAvatar.getLeftPalmPosition(); } else { - normal = Controller.getSpatialControlNormal(RIGHT_PALM); - tipPosition = Controller.getSpatialControlPosition(RIGHT_TIP); + position = MyAvatar.getRightPalmPosition(); } - var BALL_FORWARD_OFFSET = 0.15; // put the ball a bit forward of fingers - position = { x: BALL_FORWARD_OFFSET * normal.x, - y: BALL_FORWARD_OFFSET * normal.y, - z: BALL_FORWARD_OFFSET * normal.z }; - - position.x += tipPosition.x; - position.y += tipPosition.y; - position.z += tipPosition.z; - return position; } diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index ee65b9c170..ee8e7266cb 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -382,25 +382,46 @@ void Avatar::renderBody(RenderMode renderMode, float glowLevel) { const float HAIR_LENGTH = 0.2f; const float HAIR_LINK_LENGTH = HAIR_LENGTH / HAIR_LINKS; const float HAIR_DAMPING = 0.99f; -const float HEAD_RADIUS = 0.15f; -const float COLLISION_RELAXATION = 10.f; -const float CONSTRAINT_RELAXATION = 10.0f; -const glm::vec3 HAIR_GRAVITY(0.f, -0.005f, 0.f); +const float HEAD_RADIUS = 0.18f; +const float CONSTRAINT_RELAXATION = 20.0f; +const glm::vec3 HAIR_GRAVITY(0.f, -0.015f, 0.f); const float HAIR_ACCELERATION_COUPLING = 0.025f; const float HAIR_ANGULAR_VELOCITY_COUPLING = 0.10f; const float HAIR_MAX_LINEAR_ACCELERATION = 4.f; -const float HAIR_THICKNESS = 0.025f; -const float HAIR_STIFFNESS = 0.002f; +const float HAIR_THICKNESS = 0.020f; +const float HAIR_STIFFNESS = 0.0006f; const glm::vec3 HAIR_COLOR1(0.98f, 0.92f, 0.843f); const glm::vec3 HAIR_COLOR2(0.545f, 0.533f, 0.47f); const glm::vec3 WIND_DIRECTION(0.5f, -1.0f, 0.f); const float MAX_WIND_STRENGTH = 0.01f; +const float FINGER_LENGTH = 0.25; +const float FINGER_RADIUS = 0.10; void Avatar::renderHair() { // // Render the avatar's moveable hair // + glm::vec3 headPosition = getHead()->getPosition(); + /* + glm::vec3 leftHandPosition, rightHandPosition; + getSkeletonModel().getLeftHandPosition(leftHandPosition); + getSkeletonModel().getRightHandPosition(rightHandPosition); + glm::quat leftRotation, rightRotation; + getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getRightHandJointIndex(), rightRotation); + rightHandPosition += glm::vec3(0.0f, FINGER_LENGTH, 0.f) * glm::inverse(rightRotation); + + glPushMatrix(); + glTranslatef(leftHandPosition.x, leftHandPosition.y, leftHandPosition.z); + glColor4f(1.0f, 0.0f, 0.0f, 0.5f); + glutSolidSphere(FINGER_RADIUS, 20, 20); + glPopMatrix(); + glPushMatrix(); + glTranslatef(rightHandPosition.x, rightHandPosition.y, rightHandPosition.z); + glColor4f(1.0f, 0.0f, 0.0f, 0.5f); + glutSolidSphere(FINGER_RADIUS, 20, 20); + glPopMatrix(); + */ glPushMatrix(); glTranslatef(headPosition.x, headPosition.y, headPosition.z); @@ -431,13 +452,12 @@ void Avatar::renderHair() { } glEnd(); - //glColor4f(1.0f, 0.0f, 0.0f, 0.5f); - //glutSolidSphere(HEAD_RADIUS, 20, 20); glPopMatrix(); } void Avatar::simulateHair(float deltaTime) { + deltaTime = glm::clamp(deltaTime, 0.f, 1.f / 30.f); glm::vec3 acceleration = getAcceleration(); if (glm::length(acceleration) > HAIR_MAX_LINEAR_ACCELERATION) { @@ -447,6 +467,20 @@ void Avatar::simulateHair(float deltaTime) { acceleration = acceleration * rotation; glm::vec3 angularVelocity = getAngularVelocity() + getHead()->getAngularVelocity(); + // Get hand positions to allow touching hair + glm::vec3 leftHandPosition, rightHandPosition; + getSkeletonModel().getLeftHandPosition(leftHandPosition); + getSkeletonModel().getRightHandPosition(rightHandPosition); + leftHandPosition -= getHead()->getPosition(); + rightHandPosition -= getHead()->getPosition(); + glm::quat leftRotation, rightRotation; + getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getLeftHandJointIndex(), leftRotation); + getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getRightHandJointIndex(), rightRotation); + leftHandPosition += glm::vec3(0.0f, FINGER_LENGTH, 0.f) * glm::inverse(leftRotation); + rightHandPosition += glm::vec3(0.0f, FINGER_LENGTH, 0.f) * glm::inverse(rightRotation); + leftHandPosition = leftHandPosition * rotation; + rightHandPosition = rightHandPosition * rotation; + float windIntensity = randFloat() * MAX_WIND_STRENGTH; for (int strand = 0; strand < HAIR_STRANDS; strand++) { @@ -462,11 +496,22 @@ void Avatar::simulateHair(float deltaTime) { glm::vec3 thisPosition = _hairPosition[vertexIndex]; glm::vec3 diff = thisPosition - _hairLastPosition[vertexIndex]; _hairPosition[vertexIndex] += diff * HAIR_DAMPING; - // Attempt to resolve collision with head sphere + // Resolve collision with head sphere if (glm::length(_hairPosition[vertexIndex]) < HEAD_RADIUS) { _hairPosition[vertexIndex] += glm::normalize(_hairPosition[vertexIndex]) * - (HEAD_RADIUS - glm::length(_hairPosition[vertexIndex])) * COLLISION_RELAXATION * deltaTime; + (HEAD_RADIUS - glm::length(_hairPosition[vertexIndex])); // * COLLISION_RELAXATION * deltaTime; } + // Collide with hands + if (glm::length(_hairPosition[vertexIndex] - leftHandPosition) < FINGER_RADIUS) { + _hairPosition[vertexIndex] += glm::normalize(_hairPosition[vertexIndex] - leftHandPosition) * + (FINGER_RADIUS - glm::length(_hairPosition[vertexIndex] - leftHandPosition)); + } + if (glm::length(_hairPosition[vertexIndex] - rightHandPosition) < FINGER_RADIUS) { + _hairPosition[vertexIndex] += glm::normalize(_hairPosition[vertexIndex] - rightHandPosition) * + (FINGER_RADIUS - glm::length(_hairPosition[vertexIndex] - rightHandPosition)); + } + + // Add a little gravity _hairPosition[vertexIndex] += HAIR_GRAVITY * rotation * deltaTime; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index bbdf041341..5d6b381296 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -422,6 +422,25 @@ void MyAvatar::renderHeadMouse(int screenWidth, int screenHeight) const { } } +const glm::vec3 HAND_TO_PALM_OFFSET(0.0f, 0.12f, 0.08f); + +glm::vec3 MyAvatar::getLeftPalmPosition() { + glm::vec3 leftHandPosition; + getSkeletonModel().getLeftHandPosition(leftHandPosition); + glm::quat leftRotation; + getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getLeftHandJointIndex(), leftRotation); + leftHandPosition += HAND_TO_PALM_OFFSET * glm::inverse(leftRotation); + return leftHandPosition; +} +glm::vec3 MyAvatar::getRightPalmPosition() { + glm::vec3 rightHandPosition; + getSkeletonModel().getRightHandPosition(rightHandPosition); + glm::quat rightRotation; + getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getRightHandJointIndex(), rightRotation); + rightHandPosition += HAND_TO_PALM_OFFSET * glm::inverse(rightRotation); + return rightHandPosition; +} + void MyAvatar::setLocalGravity(glm::vec3 gravity) { _motionBehaviors |= AVATAR_MOTION_OBEY_LOCAL_GRAVITY; // Environmental and Local gravities are incompatible. Since Local is being set here diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index e7023b45a1..0ee76c6b45 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -139,7 +139,10 @@ public slots: void setThrust(glm::vec3 newThrust) { _thrust = newThrust; } void updateMotionBehaviorsFromMenu(); - + + glm::vec3 getLeftPalmPosition(); + glm::vec3 getRightPalmPosition(); + signals: void transformChanged(); From 58d460da0927c9b7a2f2012619e047fb32943c59 Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Wed, 25 Jun 2014 15:14:17 -0700 Subject: [PATCH 07/11] option to turn on/off string hair --- examples/concertCamera.js | 67 ++++++++++++++++++++++++++++ interface/src/Menu.cpp | 1 + interface/src/Menu.h | 1 + interface/src/avatar/Avatar.cpp | 72 ++++++++++++------------------- interface/src/avatar/Avatar.h | 6 +-- interface/src/avatar/MyAvatar.cpp | 12 +++++- 6 files changed, 110 insertions(+), 49 deletions(-) create mode 100644 examples/concertCamera.js diff --git a/examples/concertCamera.js b/examples/concertCamera.js new file mode 100644 index 0000000000..0d26fd8ae0 --- /dev/null +++ b/examples/concertCamera.js @@ -0,0 +1,67 @@ +// +// concertCamera.js +// +// Created by Philip Rosedale on June 24, 2014 +// Copyright 2014 High Fidelity, Inc. +// +// Move a camera through a series of pre-set locations by pressing number keys +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var oldMode; +var avatarPosition; + +var cameraNumber = 0; +var freeCamera = false; + +var cameraLocations = [ {x: 7972.0, y: 241.6, z: 7304.1}, {x: 7973.7, y: 241.6, z: 7304.1}, {x: 7975.5, y: 241.6, z: 7304.1}, {x: 7972.3, y: 241.6, z: 7303.3}, {x: 7971.4, y: 241.6, z: 7304.3} ]; +var cameraLookAts = [ {x: 7971.1, y: 241.6, z: 7304.1}, {x: 7972.1, y: 241.6, z: 7304.1}, {x: 7972.1, y: 241.6, z: 7304.1}, {x: 7972.1, y: 241.6, z: 7304.1}, {x: 7972.1, y: 241.6, z: 7304.1} ]; + +function saveCameraState() { + oldMode = Camera.getMode(); + avatarPosition = MyAvatar.position; + Camera.setModeShiftPeriod(0.0); + Camera.setMode("independent"); +} + +function restoreCameraState() { + Camera.stopLooking(); + Camera.setMode(oldMode); +} + +function update(deltaTime) { + if (freeCamera) { + var delta = Vec3.subtract(MyAvatar.position, avatarPosition); + if (Vec3.length(delta) > 0.05) { + cameraNumber = 0; + freeCamera = false; + restoreCameraState(); + } + } +} + +function keyPressEvent(event) { + + var choice = parseInt(event.text); + + if ((choice > 0) && (choice <= cameraLocations.length)) { + print("camera " + choice); + if (!freeCamera) { + saveCameraState(); + freeCamera = true; + } + Camera.setMode("independent"); + Camera.setPosition(cameraLocations[choice - 1]); + Camera.keepLookingAt(cameraLookAts[choice - 1]); + } + if (event.text == "0") { + // Show camera location in log + var cameraLocation = Camera.getPosition(); + print(cameraLocation.x + ", " + cameraLocation.y + ", " + cameraLocation.z); + } +} + +Script.update.connect(update); +Controller.keyPressEvent.connect(keyPressEvent); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 7d4c7d109d..c0c798799a 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -345,6 +345,7 @@ Menu::Menu() : addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Metavoxels, 0, true); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::BuckyBalls, 0, false); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::StringHair, 0, false); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Particles, 0, true); addActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::LodTools, Qt::SHIFT | Qt::Key_L, this, SLOT(lodTools())); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 5b479773b6..3665f5838f 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -319,6 +319,7 @@ namespace MenuOption { const QString Bandwidth = "Bandwidth Display"; const QString BandwidthDetails = "Bandwidth Details"; const QString BuckyBalls = "Bucky Balls"; + const QString StringHair = "String Hair"; const QString CascadedShadows = "Cascaded"; const QString Chat = "Chat..."; const QString ChatCircling = "Chat Circling"; diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index ee8e7266cb..5a294bb2a5 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -140,7 +140,9 @@ void Avatar::simulate(float deltaTime) { head->setScale(_scale); head->simulate(deltaTime, false, _shouldRenderBillboard); - simulateHair(deltaTime); + if (Menu::getInstance()->isOptionChecked(MenuOption::StringHair)) { + simulateHair(deltaTime); + } } // update position by velocity, and subtract the change added earlier for gravity @@ -376,26 +378,32 @@ void Avatar::renderBody(RenderMode renderMode, float glowLevel) { getHand()->render(false, modelRenderMode); } getHead()->render(1.0f, modelRenderMode); - renderHair(); + if (Menu::getInstance()->isOptionChecked(MenuOption::StringHair)) { + renderHair(); + } } +// +// Constants for the Hair Simulation +// + const float HAIR_LENGTH = 0.2f; const float HAIR_LINK_LENGTH = HAIR_LENGTH / HAIR_LINKS; const float HAIR_DAMPING = 0.99f; -const float HEAD_RADIUS = 0.18f; -const float CONSTRAINT_RELAXATION = 20.0f; -const glm::vec3 HAIR_GRAVITY(0.f, -0.015f, 0.f); +const float HEAD_RADIUS = 0.21f; +const float CONSTRAINT_RELAXATION = 10.0f; +const glm::vec3 HAIR_GRAVITY(0.0f, -0.007f, 0.0f); const float HAIR_ACCELERATION_COUPLING = 0.025f; const float HAIR_ANGULAR_VELOCITY_COUPLING = 0.10f; -const float HAIR_MAX_LINEAR_ACCELERATION = 4.f; -const float HAIR_THICKNESS = 0.020f; -const float HAIR_STIFFNESS = 0.0006f; +const float HAIR_MAX_LINEAR_ACCELERATION = 4.0f; +const float HAIR_THICKNESS = 0.015f; +const float HAIR_STIFFNESS = 0.0000f; const glm::vec3 HAIR_COLOR1(0.98f, 0.92f, 0.843f); const glm::vec3 HAIR_COLOR2(0.545f, 0.533f, 0.47f); -const glm::vec3 WIND_DIRECTION(0.5f, -1.0f, 0.f); -const float MAX_WIND_STRENGTH = 0.01f; -const float FINGER_LENGTH = 0.25; -const float FINGER_RADIUS = 0.10; +const glm::vec3 WIND_DIRECTION(0.5f, -1.0f, 0.0f); +const float MAX_WIND_STRENGTH = 0.02f; +const float FINGER_LENGTH = 0.25f; +const float FINGER_RADIUS = 0.10f; void Avatar::renderHair() { // @@ -403,26 +411,6 @@ void Avatar::renderHair() { // glm::vec3 headPosition = getHead()->getPosition(); - /* - glm::vec3 leftHandPosition, rightHandPosition; - getSkeletonModel().getLeftHandPosition(leftHandPosition); - getSkeletonModel().getRightHandPosition(rightHandPosition); - glm::quat leftRotation, rightRotation; - getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getRightHandJointIndex(), rightRotation); - rightHandPosition += glm::vec3(0.0f, FINGER_LENGTH, 0.f) * glm::inverse(rightRotation); - - glPushMatrix(); - glTranslatef(leftHandPosition.x, leftHandPosition.y, leftHandPosition.z); - glColor4f(1.0f, 0.0f, 0.0f, 0.5f); - glutSolidSphere(FINGER_RADIUS, 20, 20); - glPopMatrix(); - glPushMatrix(); - glTranslatef(rightHandPosition.x, rightHandPosition.y, rightHandPosition.z); - glColor4f(1.0f, 0.0f, 0.0f, 0.5f); - glutSolidSphere(FINGER_RADIUS, 20, 20); - glPopMatrix(); - */ - glPushMatrix(); glTranslatef(headPosition.x, headPosition.y, headPosition.z); const glm::quat& rotation = getHead()->getFinalOrientationInWorldFrame(); @@ -458,7 +446,7 @@ void Avatar::renderHair() { void Avatar::simulateHair(float deltaTime) { - deltaTime = glm::clamp(deltaTime, 0.f, 1.f / 30.f); + deltaTime = glm::clamp(deltaTime, 0.0f, 1.0f / 30.0f); glm::vec3 acceleration = getAcceleration(); if (glm::length(acceleration) > HAIR_MAX_LINEAR_ACCELERATION) { acceleration = glm::normalize(acceleration) * HAIR_MAX_LINEAR_ACCELERATION; @@ -476,8 +464,8 @@ void Avatar::simulateHair(float deltaTime) { glm::quat leftRotation, rightRotation; getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getLeftHandJointIndex(), leftRotation); getSkeletonModel().getJointRotationInWorldFrame(getSkeletonModel().getRightHandJointIndex(), rightRotation); - leftHandPosition += glm::vec3(0.0f, FINGER_LENGTH, 0.f) * glm::inverse(leftRotation); - rightHandPosition += glm::vec3(0.0f, FINGER_LENGTH, 0.f) * glm::inverse(rightRotation); + leftHandPosition += glm::vec3(0.0f, FINGER_LENGTH, 0.0f) * glm::inverse(leftRotation); + rightHandPosition += glm::vec3(0.0f, FINGER_LENGTH, 0.0f) * glm::inverse(rightRotation); leftHandPosition = leftHandPosition * rotation; rightHandPosition = rightHandPosition * rotation; @@ -499,9 +487,9 @@ void Avatar::simulateHair(float deltaTime) { // Resolve collision with head sphere if (glm::length(_hairPosition[vertexIndex]) < HEAD_RADIUS) { _hairPosition[vertexIndex] += glm::normalize(_hairPosition[vertexIndex]) * - (HEAD_RADIUS - glm::length(_hairPosition[vertexIndex])); // * COLLISION_RELAXATION * deltaTime; + (HEAD_RADIUS - glm::length(_hairPosition[vertexIndex])); } - // Collide with hands + // Resolve collision with hands if (glm::length(_hairPosition[vertexIndex] - leftHandPosition) < FINGER_RADIUS) { _hairPosition[vertexIndex] += glm::normalize(_hairPosition[vertexIndex] - leftHandPosition) * (FINGER_RADIUS - glm::length(_hairPosition[vertexIndex] - leftHandPosition)); @@ -518,7 +506,7 @@ void Avatar::simulateHair(float deltaTime) { // Add linear acceleration of the avatar body _hairPosition[vertexIndex] -= acceleration * HAIR_ACCELERATION_COUPLING * deltaTime; - // Add stiffness (product) + // Add stiffness (like hair care products do) _hairPosition[vertexIndex] += (_hairOriginalPosition[vertexIndex] - _hairPosition[vertexIndex]) * powf(1.f - link / HAIR_LINKS, 2.f) * HAIR_STIFFNESS; @@ -580,13 +568,8 @@ void Avatar::initializeHair() { float strandAngle = randFloat() * PI; float azimuth = FACE_WIDTH / 2.0f + (randFloat() * (2.0 * PI - FACE_WIDTH)); float elevation = PI_OVER_TWO - (randFloat() * 0.75 * PI); - /*if ((azimuth > FACE_WIDTH) || (azimuth < -FACE_WIDTH)) { - elevation = randFloat() * PI_OVER_TWO; - } else { - elevation = (PI_OVER_TWO / 2.f) + randFloat() * (PI_OVER_TWO / 2.f); - }*/ glm::vec3 thisStrand(sinf(azimuth) * cosf(elevation), sinf(elevation), -cosf(azimuth) * cosf(elevation)); - thisStrand *= HEAD_RADIUS + 0.01f; + thisStrand *= HEAD_RADIUS; for (int link = 0; link < HAIR_LINKS; link++) { int vertexIndex = strand * HAIR_LINKS + link; @@ -610,6 +593,7 @@ void Avatar::initializeHair() { _hairOriginalPosition[vertexIndex] = _hairPosition[vertexIndex]; _hairQuadDelta[vertexIndex] = glm::vec3(cos(strandAngle) * HAIR_THICKNESS, 0.f, sin(strandAngle) * HAIR_THICKNESS); + _hairQuadDelta[vertexIndex] *= 1.f - (link / HAIR_LINKS); _hairNormals[vertexIndex] = glm::normalize(randVector()); if (randFloat() < elevation / PI_OVER_TWO) { _hairColors[vertexIndex] = HAIR_COLOR1 * ((float)(link + 1) / (float)HAIR_LINKS); diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 40f92b468c..f20db1019d 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -32,9 +32,9 @@ static const float RESCALING_TOLERANCE = .02f; extern const float CHAT_MESSAGE_SCALE; extern const float CHAT_MESSAGE_HEIGHT; -const int HAIR_STRANDS = 200; // Number of strands of hair -const int HAIR_LINKS = 10; // Number of links in a hair strand -const int HAIR_MAX_CONSTRAINTS = 2; +const int HAIR_STRANDS = 150; // Number of strands of hair +const int HAIR_LINKS = 10; // Number of links in a hair strand +const int HAIR_MAX_CONSTRAINTS = 2; // Hair verlet is connected to at most how many others enum DriveKeys { FWD = 0, diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 5d6b381296..b853f3ff8f 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -195,7 +195,12 @@ void MyAvatar::simulate(float deltaTime) { head->simulate(deltaTime, true); } - simulateHair(deltaTime); + { + PerformanceTimer perfTimer("MyAvatar::simulate/hair Simulate"); + if (Menu::getInstance()->isOptionChecked(MenuOption::StringHair)) { + simulateHair(deltaTime); + } + } { PerformanceTimer perfTimer("MyAvatar::simulate/ragdoll"); @@ -370,6 +375,7 @@ void MyAvatar::render(const glm::vec3& cameraPosition, RenderMode renderMode) { if (!_shouldRender) { return; // exit early } + Avatar::render(cameraPosition, renderMode); // don't display IK constraints in shadow mode @@ -852,7 +858,9 @@ void MyAvatar::renderBody(RenderMode renderMode, float glowLevel) { // Render head so long as the camera isn't inside it if (shouldRenderHead(Application::getInstance()->getCamera()->getPosition(), renderMode)) { getHead()->render(1.0f, modelRenderMode); - renderHair(); + if (Menu::getInstance()->isOptionChecked(MenuOption::StringHair)) { + renderHair(); + } } getHand()->render(true, modelRenderMode); } From 9af2b2b4dc331cf8fd3b9c91b7ebd68c9cda53b6 Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Wed, 25 Jun 2014 15:24:21 -0700 Subject: [PATCH 08/11] removed commented code --- examples/sit.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/examples/sit.js b/examples/sit.js index 87b4f232ca..32d2eebf4d 100644 --- a/examples/sit.js +++ b/examples/sit.js @@ -50,12 +50,6 @@ var pose = [ {joint:"LeftUpLeg", rotation: {x:100.0, y:-15.0, z:0.0}}, {joint:"LeftLeg", rotation: {x:-130.0, y:-15.0, z:0.0}}, {joint:"LeftFoot", rotation: {x:30, y:15.0, z:0.0}} - - //{joint:"Spine2", rotation: {x:20, y:0.0, z:0.0}}, - - //{joint:"RightShoulder", rotation: {x:0.0, y:40.0, z:0.0}}, - //{joint:"LeftShoulder", rotation: {x:0.0, y:-40.0, z:0.0}} - ]; var startPoseAndTransition = []; From 93b131d520ee80fa6ed538eae500ed373fe25252 Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Wed, 25 Jun 2014 15:41:47 -0700 Subject: [PATCH 09/11] Fix build warning from wrong init order --- interface/src/ui/ApplicationOverlay.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 9be556cf62..342a145953 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -39,9 +39,10 @@ inline float min(float a, float b) { ApplicationOverlay::ApplicationOverlay() : _framebufferObject(NULL), _textureFov(DEFAULT_OCULUS_UI_ANGULAR_SIZE * RADIANS_PER_DEGREE), - _crosshairTexture(0), _alpha(1.0f), - _active(true) { + _active(true), + _crosshairTexture(0) +{ memset(_reticleActive, 0, sizeof(_reticleActive)); memset(_magActive, 0, sizeof(_reticleActive)); From c72e41dcd298c60026a6fb8b45b77df3eef9a819 Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Wed, 25 Jun 2014 22:27:54 -0700 Subject: [PATCH 10/11] added Oculus angular velocity, moved sit icon up --- examples/sit.js | 2 +- interface/src/avatar/MyAvatar.cpp | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/examples/sit.js b/examples/sit.js index 32d2eebf4d..77a626c63f 100644 --- a/examples/sit.js +++ b/examples/sit.js @@ -19,7 +19,7 @@ var buttonHeight = 46; var buttonPadding = 10; var buttonPositionX = windowDimensions.x - buttonPadding - buttonWidth; -var buttonPositionY = (windowDimensions.y - buttonHeight) / 2 ; +var buttonPositionY = (windowDimensions.y - buttonHeight) / 2 - (buttonHeight + buttonPadding); var sitDownButton = Overlays.addOverlay("image", { x: buttonPositionX, y: buttonPositionY, width: buttonWidth, height: buttonHeight, diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index b853f3ff8f..7367f64d73 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -911,11 +911,19 @@ void MyAvatar::updateOrientation(float deltaTime) { float yaw, pitch, roll; OculusManager::getEulerAngles(yaw, pitch, roll); // ... so they need to be converted to degrees before we do math... - + yaw *= DEGREES_PER_RADIAN; + pitch *= DEGREES_PER_RADIAN; + roll *= DEGREES_PER_RADIAN; + + // Record the angular velocity Head* head = getHead(); - head->setBaseYaw(yaw * DEGREES_PER_RADIAN); - head->setBasePitch(pitch * DEGREES_PER_RADIAN); - head->setBaseRoll(roll * DEGREES_PER_RADIAN); + glm::vec3 angularVelocity(yaw - head->getBaseYaw(), pitch - head->getBasePitch(), roll - head->getBaseRoll()); + head->setAngularVelocity(angularVelocity); + + head->setBaseYaw(yaw); + head->setBasePitch(pitch); + head->setBaseRoll(roll); + } // update the euler angles From 64c89df5c1ce25aaeeaba7be6c669889b24b949e Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Thu, 26 Jun 2014 10:52:37 -0700 Subject: [PATCH 11/11] Added another concert view --- examples/concertCamera.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/concertCamera.js b/examples/concertCamera.js index 0d26fd8ae0..7280958bc6 100644 --- a/examples/concertCamera.js +++ b/examples/concertCamera.js @@ -16,8 +16,8 @@ var avatarPosition; var cameraNumber = 0; var freeCamera = false; -var cameraLocations = [ {x: 7972.0, y: 241.6, z: 7304.1}, {x: 7973.7, y: 241.6, z: 7304.1}, {x: 7975.5, y: 241.6, z: 7304.1}, {x: 7972.3, y: 241.6, z: 7303.3}, {x: 7971.4, y: 241.6, z: 7304.3} ]; -var cameraLookAts = [ {x: 7971.1, y: 241.6, z: 7304.1}, {x: 7972.1, y: 241.6, z: 7304.1}, {x: 7972.1, y: 241.6, z: 7304.1}, {x: 7972.1, y: 241.6, z: 7304.1}, {x: 7972.1, y: 241.6, z: 7304.1} ]; +var cameraLocations = [ {x: 7972.2, y: 241.6, z: 7304.1}, {x: 7973.0, y: 241.6, z: 7304.1}, {x: 7975.5, y: 241.6, z: 7304.1}, {x: 7972.3, y: 241.6, z: 7303.3}, {x: 7971.4, y: 241.6, z: 7304.3}, {x: 7973.5, y: 240.6, z: 7302.5} ]; +var cameraLookAts = [ {x: 7971.1, y: 241.6, z: 7304.1}, {x: 7972.1, y: 241.6, z: 7304.1}, {x: 7972.1, y: 241.6, z: 7304.1}, {x: 7972.1, y: 241.6, z: 7304.1}, {x: 7972.1, y: 241.6, z: 7304.1}, {x: 7972.1, y: 241., z: 7304.1} ]; function saveCameraState() { oldMode = Camera.getMode();