From 556578b8dc83ef130935630410f3994b2fe361a0 Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Tue, 24 Jun 2014 09:30:11 -0700 Subject: [PATCH] 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