diff --git a/examples/concertCamera.js b/examples/concertCamera.js new file mode 100644 index 0000000000..7280958bc6 --- /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.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(); + 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/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); + 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/examples/sit.js b/examples/sit.js index d10c08c95a..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, @@ -49,13 +49,7 @@ 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:"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:"LeftFoot", rotation: {x:30, y:15.0, z:0.0}} ]; var startPoseAndTransition = []; 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/examples/toyball.js b/examples/toyball.js index d312c1bc94..e03fd67a5d 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; @@ -44,31 +51,19 @@ 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.08; // 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; } function checkControllerSide(whichSide) { var BUTTON_FWD; var BUTTON_3; + var TRIGGER; var palmPosition; var ballAlreadyInHand; var handMessage; @@ -76,18 +71,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 +104,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 +127,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 +135,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 +155,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 +188,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/Application.cpp b/interface/src/Application.cpp index 67de203fa2..9bc08aa188 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -134,7 +134,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), @@ -553,7 +553,7 @@ void Application::initializeGL() { } // update before the first render - update(0.0f); + update(1.f / _fps); InfoView::showFirstTime(); } 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(); } diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 5ba1ee8762..6269d4a8c0 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -346,6 +346,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 6c985ce626..7417f953c6 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -326,6 +326,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/Util.cpp b/interface/src/Util.cpp index 5ce1435bd6..4dbb015459 100644 --- a/interface/src/Util.cpp +++ b/interface/src/Util.cpp @@ -68,6 +68,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 57452f687f..5a294bb2a5 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,15 @@ void Avatar::simulate(float deltaTime) { head->setPosition(headPosition); head->setScale(_scale); head->simulate(deltaTime, false, _shouldRenderBillboard); + + if (Menu::getInstance()->isOptionChecked(MenuOption::StringHair)) { + 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 +167,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; @@ -357,6 +378,232 @@ void Avatar::renderBody(RenderMode renderMode, float glowLevel) { getHand()->render(false, modelRenderMode); } getHead()->render(1.0f, modelRenderMode); + 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.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.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.0f); +const float MAX_WIND_STRENGTH = 0.02f; +const float FINGER_LENGTH = 0.25f; +const float FINGER_RADIUS = 0.10f; + +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(); + + glPopMatrix(); + +} + +void Avatar::simulateHair(float deltaTime) { + + 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; + } + const glm::quat& rotation = getHead()->getFinalOrientationInWorldFrame(); + 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.0f) * glm::inverse(leftRotation); + rightHandPosition += glm::vec3(0.0f, FINGER_LENGTH, 0.0f) * glm::inverse(rightRotation); + leftHandPosition = leftHandPosition * rotation; + rightHandPosition = rightHandPosition * rotation; + + 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 + } 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; + // Resolve collision with head sphere + if (glm::length(_hairPosition[vertexIndex]) < HEAD_RADIUS) { + _hairPosition[vertexIndex] += glm::normalize(_hairPosition[vertexIndex]) * + (HEAD_RADIUS - glm::length(_hairPosition[vertexIndex])); + } + // 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)); + } + 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; + + // Add linear acceleration of the avatar body + _hairPosition[vertexIndex] -= acceleration * HAIR_ACCELERATION_COUPLING * deltaTime; + + // Add stiffness (like hair care products do) + _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; + + 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; + } + 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 + 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 = PI / 4.0f; + glm::vec3 thisVertex; + for (int strand = 0; strand < HAIR_STRANDS; strand++) { + 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); + glm::vec3 thisStrand(sinf(azimuth) * cosf(elevation), sinf(elevation), -cosf(azimuth) * cosf(elevation)); + thisStrand *= HEAD_RADIUS; + + 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]; + _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); + } 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 a0e41d7122..f20db1019d 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 = 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, BACK, @@ -145,6 +149,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(); @@ -156,6 +163,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; @@ -172,6 +183,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; @@ -187,6 +199,18 @@ protected: virtual void renderAttachments(RenderMode renderMode); 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]; + 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: @@ -198,6 +222,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 a90a65b5ed..7367f64d73 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -194,6 +194,13 @@ void MyAvatar::simulate(float deltaTime) { head->setScale(_scale); head->simulate(deltaTime, true); } + + { + PerformanceTimer perfTimer("MyAvatar::simulate/hair Simulate"); + if (Menu::getInstance()->isOptionChecked(MenuOption::StringHair)) { + simulateHair(deltaTime); + } + } { PerformanceTimer perfTimer("MyAvatar::simulate/ragdoll"); @@ -368,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 @@ -420,6 +428,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 @@ -831,6 +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); + if (Menu::getInstance()->isOptionChecked(MenuOption::StringHair)) { + renderHair(); + } } getHand()->render(true, modelRenderMode); } @@ -881,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 @@ -974,6 +1012,7 @@ void MyAvatar::updatePosition(float deltaTime) { } else { _position += _velocity * deltaTime; } + updateAcceleration(deltaTime); } // update moving flag based on speed 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(); 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));