diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 6d37f66563..1b41636874 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -36,11 +36,8 @@ public: public slots: void run(); - void readPendingDatagrams(); -signals: - void willSendAudioDataCallback(); - void willSendVisualDataCallback(); + private: ScriptEngine _scriptEngine; VoxelEditPacketSender _voxelEditSender; diff --git a/domain-server/resources/web/assignment/placeholder.js b/domain-server/resources/web/assignment/placeholder.js index 7c84767f31..46a706999f 100644 --- a/domain-server/resources/web/assignment/placeholder.js +++ b/domain-server/resources/web/assignment/placeholder.js @@ -114,7 +114,7 @@ function sendNextCells() { var sentFirstBoard = false; -function step() { +function step(deltaTime) { if (sentFirstBoard) { // we've already sent the first full board, perform a step in time updateCells(); @@ -127,5 +127,5 @@ function step() { } -Script.willSendVisualDataCallback.connect(step); +Script.update.connect(step); Voxels.setPacketsPerSecond(200); \ No newline at end of file diff --git a/examples/audioBall.js b/examples/audioBall.js index 676b9118b3..0889d9eb31 100644 --- a/examples/audioBall.js +++ b/examples/audioBall.js @@ -18,7 +18,7 @@ var FACTOR = 0.75; var countParticles = 0; // the first time around we want to create the particle and thereafter to modify it. var particleID; -function updateParticle() { +function updateParticle(deltaTime) { // the particle should be placed in front of the user's avatar var avatarFront = Quat.getFront(MyAvatar.orientation); @@ -62,7 +62,7 @@ function updateParticle() { } // register the call back so it fires before each data send -Script.willSendVisualDataCallback.connect(updateParticle); +Script.update.connect(updateParticle); // register our scriptEnding callback Script.scriptEnding.connect(function scriptEnding() {}); diff --git a/examples/bot.js b/examples/bot.js index 162dde9dae..5fd4785b76 100644 --- a/examples/bot.js +++ b/examples/bot.js @@ -109,7 +109,7 @@ Agent.isAvatar = true; Avatar.position = firstPosition; printVector("New bot, position = ", Avatar.position); -function updateBehavior() { +function updateBehavior(deltaTime) { if (Math.random() < CHANCE_OF_SOUND) { playRandomSound(Avatar.position); } @@ -149,7 +149,7 @@ function updateBehavior() { } } } -Script.willSendVisualDataCallback.connect(updateBehavior); +Script.update.connect(updateBehavior); function loadSounds() { sounds.push(new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Cocktail+Party+Snippets/Raws/AB1.raw")); diff --git a/examples/cameraExample.js b/examples/cameraExample.js index d55f376b76..ddfff15935 100644 --- a/examples/cameraExample.js +++ b/examples/cameraExample.js @@ -20,9 +20,8 @@ var joysticksCaptured = false; var THRUST_CONTROLLER = 0; var VIEW_CONTROLLER = 1; -function checkCamera() { +function checkCamera(deltaTime) { if (Camera.getMode() == "independent") { - var deltaTime = 1/60; // approximately our FPS - maybe better to be elapsed time since last call var THRUST_MAG_UP = 800.0; var THRUST_MAG_DOWN = 300.0; var THRUST_MAG_FWD = 500.0; @@ -80,7 +79,7 @@ function checkCamera() { } } -Script.willSendVisualDataCallback.connect(checkCamera); +Script.update.connect(checkCamera); function mouseMoveEvent(event) { print("mouseMoveEvent event.x,y=" + event.x + ", " + event.y); diff --git a/examples/clap.js b/examples/clap.js index 81ccda64b7..ef8b61f05a 100644 --- a/examples/clap.js +++ b/examples/clap.js @@ -28,7 +28,7 @@ var clapping = new Array(); clapping[0] = false; clapping[1] = false; -function maybePlaySound() { +function maybePlaySound(deltaTime) { // Set the location and other info for the sound to play var palm1Position = Controller.getSpatialControlPosition(0); var palm2Position = Controller.getSpatialControlPosition(2); @@ -62,4 +62,4 @@ function maybePlaySound() { } // Connect a call back that happens every frame -Script.willSendVisualDataCallback.connect(maybePlaySound); \ No newline at end of file +Script.update.connect(maybePlaySound); \ No newline at end of file diff --git a/examples/collidingParticles.js b/examples/collidingParticles.js index 81ccfe108b..2d2cf4fecc 100644 --- a/examples/collidingParticles.js +++ b/examples/collidingParticles.js @@ -30,8 +30,9 @@ var gravity = { var damping = 0.1; var scriptA = " " + - " function collisionWithParticle(other) { " + + " function collisionWithParticle(other, penetration) { " + " print('collisionWithParticle(other.getID()=' + other.getID() + ')...'); " + + " Vec3.print('penetration=', penetration); " + " print('myID=' + Particle.getID() + '\\n'); " + " var colorBlack = { red: 0, green: 0, blue: 0 };" + " var otherColor = other.getColor();" + @@ -45,8 +46,9 @@ var scriptA = " " + " "; var scriptB = " " + - " function collisionWithParticle(other) { " + + " function collisionWithParticle(other, penetration) { " + " print('collisionWithParticle(other.getID()=' + other.getID() + ')...'); " + + " Vec3.print('penetration=', penetration); " + " print('myID=' + Particle.getID() + '\\n'); " + " Particle.setScript('Particle.setShouldDie(true);'); " + " } " + @@ -58,7 +60,7 @@ var color = { green: 255, blue: 0 }; -function draw() { +function draw(deltaTime) { print("hello... draw()... currentIteration=" + currentIteration + "\n"); // on the first iteration, setup a single particle that's slowly moving @@ -150,5 +152,5 @@ function draw() { // register the call back so it fires before each data send print("here...\n"); Particles.setPacketsPerSecond(40000); -Script.willSendVisualDataCallback.connect(draw); +Script.update.connect(draw); print("and here...\n"); diff --git a/examples/controllerExample.js b/examples/controllerExample.js index 43a7b12231..ebb013913e 100644 --- a/examples/controllerExample.js +++ b/examples/controllerExample.js @@ -16,7 +16,7 @@ for (t = 0; t < numberOfTriggers; t++) { triggerPulled[t] = false; } -function checkController() { +function checkController(deltaTime) { var numberOfTriggers = Controller.getNumberOfTriggers(); var numberOfSpatialControls = Controller.getNumberOfSpatialControls(); var controllersPerTrigger = numberOfSpatialControls / numberOfTriggers; @@ -48,7 +48,7 @@ function checkController() { } // register the call back so it fires before each data send -Script.willSendVisualDataCallback.connect(checkController); +Script.update.connect(checkController); function printKeyEvent(eventName, event) { print(eventName); diff --git a/examples/count.js b/examples/count.js index 29799a8271..e04bc2c94b 100644 --- a/examples/count.js +++ b/examples/count.js @@ -10,8 +10,8 @@ var count = 0; -function displayCount() { - print("count =" + count); +function displayCount(deltaTime) { + print("count =" + count + " deltaTime=" + deltaTime); count++; } @@ -20,7 +20,7 @@ function scriptEnding() { } // register the call back so it fires before each data send -Script.willSendVisualDataCallback.connect(displayCount); +Script.update.connect(displayCount); // register our scriptEnding callback Script.scriptEnding.connect(scriptEnding); diff --git a/examples/drumStick.js b/examples/drumStick.js index 78de8351db..9b0a8ccbca 100644 --- a/examples/drumStick.js +++ b/examples/drumStick.js @@ -31,7 +31,7 @@ var strokeSpeed = new Array(); strokeSpeed[0] = 0.0; strokeSpeed[1] = 0.0; -function checkSticks() { +function checkSticks(deltaTime) { for (var palm = 0; palm < 2; palm++) { var palmVelocity = Controller.getSpatialControlVelocity(palm * 2 + 1); var speed = length(palmVelocity); @@ -69,4 +69,4 @@ function checkSticks() { } // Connect a call back that happens every frame -Script.willSendVisualDataCallback.connect(checkSticks); \ No newline at end of file +Script.update.connect(checkSticks); \ No newline at end of file diff --git a/examples/editParticleExample.js b/examples/editParticleExample.js index 152bb18fca..b632e0229b 100644 --- a/examples/editParticleExample.js +++ b/examples/editParticleExample.js @@ -42,7 +42,7 @@ var positionDelta = { x: 0.05, y: 0, z: 0 }; var particleID = Particles.addParticle(originalProperties); -function moveParticle() { +function moveParticle(deltaTime) { if (count >= moveUntil) { // delete it... @@ -85,5 +85,5 @@ function moveParticle() { // register the call back so it fires before each data send -Script.willSendVisualDataCallback.connect(moveParticle); +Script.update.connect(moveParticle); diff --git a/examples/editVoxels.js b/examples/editVoxels.js index 9a014639f0..4a194c0f5d 100644 --- a/examples/editVoxels.js +++ b/examples/editVoxels.js @@ -1329,7 +1329,7 @@ function checkControllers() { } } -function update() { +function update(deltaTime) { var newWindowDimensions = Controller.getViewportDimensions(); if (newWindowDimensions.x != windowDimensions.x || newWindowDimensions.y != windowDimensions.y) { windowDimensions = newWindowDimensions; @@ -1399,6 +1399,6 @@ function scriptEnding() { } Script.scriptEnding.connect(scriptEnding); -Script.willSendVisualDataCallback.connect(update); +Script.update.connect(update); setupMenus(); diff --git a/examples/findParticleExample.js b/examples/findParticleExample.js index 5eb257d502..4a0e9b832a 100644 --- a/examples/findParticleExample.js +++ b/examples/findParticleExample.js @@ -60,7 +60,7 @@ function printProperties(properties) { } } -function findParticles() { +function findParticles(deltaTime) { // run for a while, then clean up // stop it... @@ -122,7 +122,7 @@ function findParticles() { // register the call back so it fires before each data send -Script.willSendVisualDataCallback.connect(findParticles); +Script.update.connect(findParticles); // register our scriptEnding callback Script.scriptEnding.connect(scriptEnding); diff --git a/examples/flockingBirds.js b/examples/flockingBirds.js new file mode 100644 index 0000000000..12b402ab40 --- /dev/null +++ b/examples/flockingBirds.js @@ -0,0 +1,464 @@ +// +// flockingBirds.js +// hifi +// +// Created by Brad Hefta-Gaub on 3/4/14. +// Copyright (c) 2014 HighFidelity, Inc. All rights reserved. +// +// This is an example script that generates particles that act like flocking birds +// +// All birds, even flying solo... +// birds don't like to fall too fast +// if they fall to fast, they will go into a state of thrusting up, until they reach some max upward velocity, then +// go back to gliding +// birds don't like to be below a certain altitude +// if they are below that altitude they will keep thrusting up, until they get ove + +// flocking +// try to align your velocity with velocity of other birds +// try to fly toward center of flock +// but dont get too close +// + +var birdsInFlock = 40; + +var birdLifetime = 60; // 2 minutes +var count=0; // iterations + +var enableFlyTowardPoints = true; // some birds have a point they want to fly to +var enabledClustedFlyTowardPoints = true; // the flyToward points will be generally near each other +var flyToFrames = 10; // number of frames the bird would like to attempt to fly to it's flyTo point +var PROBABILITY_OF_FLY_TOWARD_CHANGE = 0.01; // chance the bird will decide to change its flyTo point +var PROBABILITY_EACH_BIRD_WILL_FLY_TOWARD = 0.2; // chance the bird will decide to flyTo, otherwise it follows +var flyingToCount = 0; // count of birds currently flying to someplace +var flyToCluster = { }; // the point that the cluster of flyTo points is based on when in enabledClustedFlyTowardPoints + +// Bird behaviors +var enableAvoidDropping = true; // birds will resist falling too fast, and will thrust up if falling +var enableAvoidMinHeight = true; // birds will resist being below a certain height and thrust up to get above it +var enableAvoidMaxHeight = true; // birds will resist being above a certain height and will glide to get below it +var enableMatchFlockVelocity = true; // birds will thrust to match the flocks average velocity +var enableThrustTowardCenter = true; // birds will thrust to try to move toward the center of the flock +var enableAvoidOtherBirds = true; // birds will thrust away from all other birds +var startWithVelocity = true; +var flockGravity = { x: 0, y: -1, z: 0}; + +// NOTE: these two features don't seem to be very interesting, they cause odd behaviors +var enableRandomXZThrust = false; // leading birds randomly decide to thrust in some random direction. +var enableSomeBirdsLead = false; // birds randomly decide not fly toward flock, causing other birds to follow +var leaders = 0; // number of birds leading +var PROBABILITY_TO_LEAD = 0.1; // probabolity a bird will choose to lead + +var birds = new Array(); // array of bird state data + + +var flockStartPosition = { x: 100, y: 10, z: 100}; +var flockStartVelocity = { x: 0, y: 0, z: 0}; +var flockStartThrust = { x: 0, y: 0, z: 0}; // slightly upward against gravity +var INITIAL_XY_VELOCITY_SCALE = 2; +var birdRadius = 0.0625; +var baseBirdColor = { red: 0, green: 255, blue: 255 }; +var glidingColor = { red: 255, green: 0, blue: 0 }; +var thrustUpwardColor = { red: 0, green: 255, blue: 0 }; +var thrustXYColor = { red: 128, green: 0, blue: 128 }; // will be added to any other color +var leadingOrflyToColor = { red: 200, green: 200, blue: 255 }; + +var tooClose = birdRadius * 3; // how close birds are willing to be to each other +var droppingTooFast = -1; // birds don't like to fall too fast +var risingTooFast = 1; // birds don't like to climb too fast +var upwardThrustAgainstGravity = -10; // how hard a bird will attempt to thrust up to avoid downward motion +var droppingAdjustFrames = 5; // birds try to correct their min height in only a couple frames +var minHeight = 10; // meters off the ground +var maxHeight = 50; // meters off the ground +var adjustFrames = 10; // typical number of frames a bird will assume for it's adjustments +var PROBABILITY_OF_RANDOM_XZ_THRUST = 0.25; +var RANDOM_XZ_THRUST_SCALE = 5; // random -SCALE...to...+SCALE in X or Z direction +var MAX_XY_VELOCITY = 10; + +// These are multiplied by every frame since a change. so after 50 frames, there's a 50% probability of change +var PROBABILITY_OF_STARTING_XZ_THRUST = 0.0025; +var PROBABILITY_OF_STOPPING_XZ_THRUST = 0.01; + +var FLY_TOWARD_XZ_DISTANCE = 100; +var FLY_TOWARD_Y_DISTANCE = 0; +var FLY_TOWARD_XZ_CLUSTER_DELTA = 5; +var FLY_TOWARD_Y_CLUSTER_DELTA = 0; + +function createBirds() { + if (enabledClustedFlyTowardPoints) { + flyToCluster = { x: flockStartPosition.x + Math.random() * FLY_TOWARD_XZ_DISTANCE, + y: flockStartPosition.y + Math.random() * FLY_TOWARD_Y_DISTANCE, + z: flockStartPosition.z + Math.random() * FLY_TOWARD_XZ_DISTANCE}; + } + + for(var i =0; i < birdsInFlock; i++) { + var velocity; + + position = Vec3.sum(flockStartPosition, { x: Math.random(), y: Math.random(), z: Math.random() }); // add random + + var flyingToward = position; + var isFlyingToward = false; + if (enableFlyTowardPoints) { + if (Math.random() < PROBABILITY_EACH_BIRD_WILL_FLY_TOWARD) { + flyingToCount++; + isFlyingToward = true; + if (enabledClustedFlyTowardPoints) { + flyingToward = { x: flyToCluster.x + Math.random() * FLY_TOWARD_XZ_CLUSTER_DELTA, + y: flyToCluster.y + Math.random() * FLY_TOWARD_Y_CLUSTER_DELTA, + z: flyToCluster.z + Math.random() * FLY_TOWARD_XZ_CLUSTER_DELTA}; + } else { + flyingToward = { x: flockStartPosition.x + Math.random() * FLY_TOWARD_XZ_DISTANCE, + y: flockStartPosition.y + Math.random() * FLY_TOWARD_Y_DISTANCE, + z: flockStartPosition.z + Math.random() * FLY_TOWARD_XZ_DISTANCE}; + } + } + + Vec3.print("birds["+i+"].flyingToward=",flyingToward); + } + + birds[i] = { + particle: {}, + properties: {}, + thrust: Vec3.sum(flockStartThrust, { x:0, y: 0, z: 0 }), + gliding: true, + xzThrust: { x:0, y: 0, z: 0}, + xzthrustCount: 0, + isLeading: false, + flyingToward: flyingToward, + isFlyingToward: isFlyingToward, + }; + + if (enableSomeBirdsLead) { + if (Math.random() < PROBABILITY_TO_LEAD) { + birds[i].isLeading = true; + } + if (leaders == 0 && i == (birdsInFlock-1)) { + birds[i].isLeading = true; + } + if (birds[i].isLeading) { + leaders++; + velocity = { x: 2, y: 0, z: 2}; + print(">>>>>>THIS BIRD LEADS!!!! i="+i); + } + } + + if (startWithVelocity) { + velocity = Vec3.sum(flockStartVelocity, { x: (Math.random() * INITIAL_XY_VELOCITY_SCALE), + y: 0, + z: (Math.random() * INITIAL_XY_VELOCITY_SCALE) }); // add random + } else { + velocity = { x: 0, y: 0, z: 0}; + } + birds[i].particle = Particles.addParticle({ + position: position, + velocity: velocity, + gravity: flockGravity, + damping: 0, + radius: birdRadius, + color: baseBirdColor, + lifetime: birdLifetime + }); + + } + print("flyingToCount=" + flyingToCount); +} + +var wantDebug = false; +function updateBirds(deltaTime) { + count++; + + // get all our bird properties, and calculate the current flock velocity + var averageVelocity = { x: 0, y: 0, z: 0}; + var averagePosition = { x: 0, y: 0, z: 0}; + var knownBirds = 0; + for(var i =0; i < birdsInFlock; i++) { + // identifyParticle() will check to see that the particle handle we have is in sync with the domain/server + // context. If the handle is for a created particle that now has a known ID it will be updated to be a + // handle with a known ID. + birds[i].particle = Particles.identifyParticle(birds[i].particle); + + if (birds[i].particle.isKnownID) { + birds[i].properties = Particles.getParticleProperties(birds[i].particle); + if (birds[i].properties.isKnownID) { + knownBirds++; + averageVelocity = Vec3.sum(averageVelocity, birds[i].properties.velocity); + averagePosition = Vec3.sum(averagePosition, birds[i].properties.position); + } + } + } + + if (knownBirds == 0 && count > 100) { + Script.stop(); + return; + } + averageVelocity = Vec3.multiply(averageVelocity, (1 / Math.max(1, knownBirds))); + averagePosition = Vec3.multiply(averagePosition, (1 / Math.max(1, knownBirds))); + + if (wantDebug) { + Vec3.print("averagePosition=",averagePosition); + Vec3.print("averageVelocity=",averageVelocity); + print("knownBirds="+knownBirds); + } + + var flyToClusterChanged = false; + if (enabledClustedFlyTowardPoints) { + if (Math.random() < PROBABILITY_OF_FLY_TOWARD_CHANGE) { + flyToClusterChanged = true; + flyToCluster = { x: averagePosition.x + (Math.random() * FLY_TOWARD_XZ_DISTANCE) - FLY_TOWARD_XZ_DISTANCE/2, + y: averagePosition.y + (Math.random() * FLY_TOWARD_Y_DISTANCE) - FLY_TOWARD_Y_DISTANCE/2, + z: averagePosition.z + (Math.random() * FLY_TOWARD_XZ_DISTANCE) - FLY_TOWARD_XZ_DISTANCE/2}; + } + } + + // iterate all birds again, adjust their thrust for various goals + for(var i =0; i < birdsInFlock; i++) { + + birds[i].thrust = { x: 0, y: 0, z: 0 }; // assume no thrust... + + if (birds[i].particle.isKnownID) { + + if (enableFlyTowardPoints) { + // if we're flying toward clusters, and the cluster changed, and this bird is flyingToward + // then we need to update it's flyingToward + if (enabledClustedFlyTowardPoints && flyToClusterChanged && birds[i].isFlyingToward) { + flyingToward = { x: flyToCluster.x + (Math.random() * FLY_TOWARD_XZ_CLUSTER_DELTA) - FLY_TOWARD_XZ_CLUSTER_DELTA/2, + y: flyToCluster.y + (Math.random() * FLY_TOWARD_Y_CLUSTER_DELTA) - FLY_TOWARD_Y_CLUSTER_DELTA/2, + z: flyToCluster.z + (Math.random() * FLY_TOWARD_XZ_CLUSTER_DELTA) - FLY_TOWARD_XZ_CLUSTER_DELTA/2}; + birds[i].flyingToward = flyingToward; + } + + // there a random chance this bird will decide to change it's flying toward state + if (Math.random() < PROBABILITY_OF_FLY_TOWARD_CHANGE) { + var wasFlyingTo = birds[i].isFlyingToward; + + // there's some chance it will decide it should be flying toward + if (Math.random() < PROBABILITY_EACH_BIRD_WILL_FLY_TOWARD) { + + // if we're flying toward clustered points, then we randomize from the cluster, otherwise we pick + // completely random places based on flocks current averagePosition + if (enabledClustedFlyTowardPoints) { + flyingToward = { x: flyToCluster.x + (Math.random() * FLY_TOWARD_XZ_CLUSTER_DELTA) - FLY_TOWARD_XZ_CLUSTER_DELTA/2, + y: flyToCluster.y + (Math.random() * FLY_TOWARD_Y_CLUSTER_DELTA) - FLY_TOWARD_Y_CLUSTER_DELTA/2, + z: flyToCluster.z + (Math.random() * FLY_TOWARD_XZ_CLUSTER_DELTA) - FLY_TOWARD_XZ_CLUSTER_DELTA/2}; + } else { + flyingToward = { x: averagePosition.x + (Math.random() * FLY_TOWARD_XZ_DISTANCE) - FLY_TOWARD_XZ_DISTANCE/2, + y: averagePosition.y + (Math.random() * FLY_TOWARD_Y_DISTANCE) - FLY_TOWARD_Y_DISTANCE/2, + z: averagePosition.z + (Math.random() * FLY_TOWARD_XZ_DISTANCE) - FLY_TOWARD_XZ_DISTANCE/2}; + } + birds[i].flyingToward = flyingToward; + birds[i].isFlyingToward = true; + } else { + birds[i].flyingToward = {}; + birds[i].isFlyingToward = false; + } + + // keep track of our bookkeeping + if (!wasFlyingTo && birds[i].isFlyingToward) { + flyingToCount++; + } + if (wasFlyingTo && !birds[i].isFlyingToward) { + flyingToCount--; + } + print(">>>> CHANGING flyingToCount="+flyingToCount); + if (birds[i].isFlyingToward) { + Vec3.print("... now birds["+i+"].flyingToward=", birds[i].flyingToward); + } + } + + // actually apply the thrust after all that + if (birds[i].isFlyingToward) { + var flyTowardDelta = Vec3.subtract(birds[i].flyingToward, birds[i].properties.position); + var thrustTowardFlyTo = Vec3.multiply(flyTowardDelta, 1/flyToFrames); + birds[i].thrust = Vec3.sum(birds[i].thrust, thrustTowardFlyTo); + } + } + + + // adjust thrust to avoid dropping to fast + if (enableAvoidDropping) { + if (birds[i].gliding) { + if (birds[i].properties.velocity.y < droppingTooFast) { + birds[i].gliding = false; // leave thrusting against gravity till it gets too high + //print("birdGliding["+i+"]=false <<<< try to conteract gravity <<<<<<<<<<<<<<<<<<<<"); + } + } + } + + // if the bird is currently not gliding, check to see if it's rising too fast + if (!birds[i].gliding && birds[i].properties.velocity.y > risingTooFast) { + //Vec3.print("bird rising too fast will glide bird["+i+"]=",birds[i].properties.velocity.y); + birds[i].gliding = true; + } + + // adjust thrust to avoid minHeight, we don't care about rising too fast in this case, so we do it + // after the rising too fast check + if (enableAvoidMinHeight) { + if (birds[i].properties.position.y < minHeight) { + //Vec3.print("**** enableAvoidMinHeight... enable thrust against gravity... bird["+i+"].position=",birds[i].properties.position); + birds[i].gliding = false; + } + } + + // adjust thrust to avoid maxHeight + if (enableAvoidMaxHeight) { + if (birds[i].properties.position.y > maxHeight) { + //Vec3.print("********************* bird above max height will glide bird["+i+"].position=",birds[i].properties.position); + birds[i].gliding = true; + } + } + + // if the bird is currently not gliding, then it is applying a thrust upward against gravity + if (!birds[i].gliding) { + // as long as we're not rising too fast, keep thrusting... + if (birds[i].properties.velocity.y < risingTooFast) { + var thrustAdjust = {x: 0, y: (flockGravity.y * upwardThrustAgainstGravity), z: 0}; + //Vec3.print("bird fighting gravity thrustAdjust for bird["+i+"]=",thrustAdjust); + birds[i].thrust = Vec3.sum(birds[i].thrust, thrustAdjust); + } else { + //print("%%% non-gliding bird, thrusting too much..."); + } + } + + if (enableRandomXZThrust && birds[i].isLeading) { + birds[i].xzThrustCount++; + + // we will randomly decide to enable XY thrust, in which case we will set the thrust and leave it + // that way till we randomly shut it off. + + // if we don't have a thrust, check against probability of starting it, and create a random thrust if + // probability occurs + if (Vec3.length(birds[i].xzThrust) == 0) { + var probabilityToStart = (PROBABILITY_OF_STARTING_XZ_THRUST * birds[i].xzThrustCount); + //print("probabilityToStart=" + probabilityToStart); + if (Math.random() < probabilityToStart) { + var xThrust = (Math.random() * (RANDOM_XZ_THRUST_SCALE * 2)) - RANDOM_XZ_THRUST_SCALE; + var zThrust = (Math.random() * (RANDOM_XZ_THRUST_SCALE * 2)) - RANDOM_XZ_THRUST_SCALE; + + birds[i].xzThrust = { x: zThrust, y: 0, z: zThrust }; + birds[i].xzThrustCount = 0; + //Vec3.print(">>>>>>>>>> STARTING XY THRUST birdXYthrust["+i+"]=", birds[i].xzThrust); + } + } + + // if we're thrusting... then check for probability of stopping + if (Vec3.length(birds[i].xzThrust)) { + var probabilityToStop = (PROBABILITY_OF_STOPPING_XZ_THRUST * birds[i].xzThrustCount); + //print("probabilityToStop=" + probabilityToStop); + if (Math.random() < probabilityToStop) { + birds[i].xzThrust = { x: 0, y: 0, z: 0}; + //Vec3.print(">>>>>>>>>> STOPPING XY THRUST birdXYthrust["+i+"]=", birds[i].xzThrust); + birds[i].xzThrustCount = 0; + } + + if (birds[i].properties.velocity.x > MAX_XY_VELOCITY) { + birds[i].xzThrust.x = 0; + //Vec3.print(">>>>>>>>>> CLEARING X THRUST birdXYthrust["+i+"]=", birds[i].xzThrust); + } + if (birds[i].properties.velocity.z > MAX_XY_VELOCITY) { + birds[i].xzThrust.z = 0; + //Vec3.print(">>>>>>>>>> CLEARING Y THRUST birdXYthrust["+i+"]=", birds[i].xzThrust); + } + + if (Vec3.length(birds[i].xzThrust)) { + birds[i].thrust = Vec3.sum(birds[i].thrust, birds[i].xzThrust); + } + } + } + + // adjust thrust to move their velocity toward average flock velocity + if (enableMatchFlockVelocity) { + if (birds[i].isLeading) { + print("this bird is leading... i="+i); + } else { + var velocityDelta = Vec3.subtract(averageVelocity, birds[i].properties.velocity); + var thrustAdjust = velocityDelta; + birds[i].thrust = Vec3.sum(birds[i].thrust, thrustAdjust); + } + } + + // adjust thrust to move their velocity toward average flock position + if (enableThrustTowardCenter) { + if (birds[i].isLeading) { + print("this bird is leading... i="+i); + } else { + var positionDelta = Vec3.subtract(averagePosition, birds[i].properties.position); + var thrustTowardCenter = Vec3.multiply(positionDelta, 1/adjustFrames); + birds[i].thrust = Vec3.sum(birds[i].thrust, thrustTowardCenter); + } + } + + + // adjust thrust to avoid other birds + if (enableAvoidOtherBirds) { + var sumThrustThisBird = { x: 0, y: 0, z: 0 }; + + for(var j =0; j < birdsInFlock; j++) { + + // if this is not me, and a known bird, then check our position + if (birds[i].properties.isKnownID && j != i) { + var positionMe = birds[i].properties.position; + var positionYou = birds[j].properties.position; + var awayFromYou = Vec3.subtract(positionMe, positionYou); // vector pointing away from "you" + var distance = Vec3.length(awayFromYou); + if (distance < tooClose) { + // NOTE: this was Philip's recommendation for "avoiding" other birds... + // Vme -= Vec3.multiply(Vme, normalize(PositionMe - PositionYou)) + // + // But it doesn't seem to work... Here's my JS implementation... + // + // var velocityMe = birds[i].properties.velocity; + // var thrustAdjust = Vec3.cross(velocityMe, Vec3.normalize(awayFromYou)); + // sumThrustThisBird = Vec3.sum(sumThrustThisBird, thrustAdjust); + // + // Instead I just apply a thrust equal to the vector away from all the birds + sumThrustThisBird = Vec3.sum(sumThrustThisBird, awayFromYou); + } + } + } + birds[i].thrust = Vec3.sum(birds[i].thrust, sumThrustThisBird); + } + } + } + + + // iterate all birds again, apply their thrust + for(var i =0; i < birdsInFlock; i++) { + if (birds[i].particle.isKnownID) { + + var color; + if (birds[i].gliding) { + color = glidingColor; + } else { + color = thrustUpwardColor; + } + if (Vec3.length(birds[i].xzThrust)) { + color = Vec3.sum(color, thrustXYColor); + } + + var velocityMe = birds[i].properties.velocity; + // add thrust to velocity + var newVelocity = Vec3.sum(velocityMe, Vec3.multiply(birds[i].thrust, deltaTime)); + + if (birds[i].isLeading || birds[i].isFlyingToward) { + Vec3.print("this bird is leading/flying toward... i="+i+" velocity=",newVelocity); + color = leadingOrflyToColor; + } + + if (wantDebug) { + Vec3.print("birds["+i+"].position=", birds[i].properties.position); + Vec3.print("birds["+i+"].oldVelocity=", velocityMe); + Vec3.print("birdThrusts["+i+"]=", birds[i].thrust); + Vec3.print("birds["+i+"].newVelocity=", newVelocity); + } + + birds[i].particle = Particles.editParticle(birds[i].particle,{ velocity: newVelocity, color: color }); + + } + } +} + + +createBirds(); +// register the call back for simulation loop +Script.update.connect(updateBirds); + diff --git a/examples/fountain.js b/examples/fountain.js index 3c943d72a0..8816dab09a 100644 --- a/examples/fountain.js +++ b/examples/fountain.js @@ -41,7 +41,7 @@ var position = { x: 5.0, y: 0.6, z: 5.0 }; Voxels.setVoxel(position.x, 0, position.z, 0.5, 0, 0, 255); var totalParticles = 0; -function makeFountain() { +function makeFountain(deltaTime) { if (Math.random() < 0.10) { //print("Made particle!\n"); var properties = { @@ -64,4 +64,4 @@ function makeFountain() { } } // register the call back so it fires before each data send -Script.willSendVisualDataCallback.connect(makeFountain); \ No newline at end of file +Script.update.connect(makeFountain); \ No newline at end of file diff --git a/examples/gameoflife.js b/examples/gameoflife.js index d72a72c2de..c0122c604a 100644 --- a/examples/gameoflife.js +++ b/examples/gameoflife.js @@ -114,7 +114,7 @@ function sendNextCells() { var sentFirstBoard = false; -function step() { +function step(deltaTime) { if (sentFirstBoard) { // we've already sent the first full board, perform a step in time updateCells(); @@ -127,6 +127,6 @@ function step() { } print("here"); -Script.willSendVisualDataCallback.connect(step); +Script.update.connect(step); Voxels.setPacketsPerSecond(200); print("now here"); diff --git a/examples/globalCollisionsExample.js b/examples/globalCollisionsExample.js index 4db4c808e5..266823f564 100644 --- a/examples/globalCollisionsExample.js +++ b/examples/globalCollisionsExample.js @@ -10,17 +10,23 @@ // -function particleCollisionWithVoxel(particle, voxel) { +print("hello..."); + +function particleCollisionWithVoxel(particle, voxel, penetration) { print("particleCollisionWithVoxel().."); print(" particle.getID()=" + particle.id); print(" voxel color...=" + voxel.red + ", " + voxel.green + ", " + voxel.blue); + Vec3.print('penetration=', penetration); } -function particleCollisionWithParticle(particleA, particleB) { +function particleCollisionWithParticle(particleA, particleB, penetration) { print("particleCollisionWithParticle().."); print(" particleA.getID()=" + particleA.id); print(" particleB.getID()=" + particleB.id); + Vec3.print('penetration=', penetration); } Particles.particleCollisionWithVoxel.connect(particleCollisionWithVoxel); Particles.particleCollisionWithParticle.connect(particleCollisionWithParticle); + +print("here... hello..."); diff --git a/examples/gun.js b/examples/gun.js index b50a8f64d8..dee01fb84d 100644 --- a/examples/gun.js +++ b/examples/gun.js @@ -61,7 +61,8 @@ function shootBullet(position, velocity) { Audio.playSound(fireSound, audioOptions); } -function particleCollisionWithVoxel(particle, voxel) { +function particleCollisionWithVoxel(particle, voxel, penetration) { + Vec3.print('particleCollisionWithVoxel() ... penetration=', penetration); var HOLE_SIZE = 0.125; var particleProperties = Particles.getParticleProperties(particle); var position = particleProperties.position; @@ -73,7 +74,7 @@ function particleCollisionWithVoxel(particle, voxel) { Audio.playSound(impactSound, audioOptions); } -function update() { +function update(deltaTime) { // Check for mouseLook movement, update rotation // rotate body yaw for yaw received from mouse @@ -178,7 +179,7 @@ function scriptEnding() { Particles.particleCollisionWithVoxel.connect(particleCollisionWithVoxel); Script.scriptEnding.connect(scriptEnding); -Script.willSendVisualDataCallback.connect(update); +Script.update.connect(update); Controller.mousePressEvent.connect(mousePressEvent); Controller.mouseReleaseEvent.connect(mouseReleaseEvent); Controller.mouseMoveEvent.connect(mouseMoveEvent); diff --git a/examples/hydraMove.js b/examples/hydraMove.js index 6ce0ad884e..1f9634a8e6 100644 --- a/examples/hydraMove.js +++ b/examples/hydraMove.js @@ -29,7 +29,6 @@ var grabbingWithLeftHand = false; var wasGrabbingWithLeftHand = false; var EPSILON = 0.000001; var velocity = { x: 0, y: 0, z: 0}; -var deltaTime = 1/60; // approximately our FPS - maybe better to be elapsed time since last call var THRUST_MAG_UP = 800.0; var THRUST_MAG_DOWN = 300.0; var THRUST_MAG_FWD = 500.0; @@ -77,7 +76,7 @@ function getAndResetGrabRotation() { } // handles all the grab related behavior: position (crawl), velocity (flick), and rotate (twist) -function handleGrabBehavior() { +function handleGrabBehavior(deltaTime) { // check for and handle grab behaviors grabbingWithRightHand = Controller.isButtonPressed(RIGHT_BUTTON_4); grabbingWithLeftHand = Controller.isButtonPressed(LEFT_BUTTON_4); @@ -156,7 +155,7 @@ function handleGrabBehavior() { } // Main update function that handles flying and grabbing behaviort -function flyWithHydra() { +function flyWithHydra(deltaTime) { var thrustJoystickPosition = Controller.getJoystickPosition(THRUST_CONTROLLER); if (thrustJoystickPosition.x != 0 || thrustJoystickPosition.y != 0) { @@ -193,10 +192,10 @@ function flyWithHydra() { var newPitch = MyAvatar.headPitch + (viewJoystickPosition.y * JOYSTICK_PITCH_MAG * deltaTime); MyAvatar.headPitch = newPitch; } - handleGrabBehavior(); + handleGrabBehavior(deltaTime); } -Script.willSendVisualDataCallback.connect(flyWithHydra); +Script.update.connect(flyWithHydra); Controller.captureJoystick(THRUST_CONTROLLER); Controller.captureJoystick(VIEW_CONTROLLER); diff --git a/examples/lookAtExample.js b/examples/lookAtExample.js index 6340feda00..46e0863231 100644 --- a/examples/lookAtExample.js +++ b/examples/lookAtExample.js @@ -45,13 +45,13 @@ function releaseMovementKeys() { } var cameraPosition = Camera.getPosition(); -function moveCamera() { +function moveCamera(update) { if (lookingAtSomething) { Camera.setPosition(cameraPosition); } } -Script.willSendVisualDataCallback.connect(moveCamera); +Script.update.connect(moveCamera); function mousePressEvent(event) { diff --git a/examples/lookWithMouse.js b/examples/lookWithMouse.js index 460663c054..878813a94a 100644 --- a/examples/lookWithMouse.js +++ b/examples/lookWithMouse.js @@ -49,7 +49,7 @@ function mouseMoveEvent(event) { } } -function update() { +function update(deltaTime) { if (wantDebugging) { print("update()..."); } @@ -91,5 +91,5 @@ MyAvatar.bodyPitch = 0; MyAvatar.bodyRoll = 0; // would be nice to change to update -Script.willSendVisualDataCallback.connect(update); +Script.update.connect(update); Script.scriptEnding.connect(scriptEnding); diff --git a/examples/lookWithTouch.js b/examples/lookWithTouch.js index bb54a055c4..ddc42c04c0 100644 --- a/examples/lookWithTouch.js +++ b/examples/lookWithTouch.js @@ -43,7 +43,7 @@ function touchUpdateEvent(event) { lastY = event.y; } -function update() { +function update(deltaTime) { // rotate body yaw for yaw received from mouse var newOrientation = Quat.multiply(MyAvatar.orientation, Quat.fromVec3( { x: 0, y: yawFromMouse, z: 0 } )); if (wantDebugging) { @@ -82,5 +82,5 @@ MyAvatar.bodyPitch = 0; MyAvatar.bodyRoll = 0; // would be nice to change to update -Script.willSendVisualDataCallback.connect(update); +Script.update.connect(update); Script.scriptEnding.connect(scriptEnding); diff --git a/examples/movingVoxel.js b/examples/movingVoxel.js index 14a7e671c6..0ddce48334 100644 --- a/examples/movingVoxel.js +++ b/examples/movingVoxel.js @@ -12,7 +12,7 @@ var colorEdge = { r:255, g:250, b:175 }; var frame = 0; var thisColor = color; -function moveVoxel() { +function moveVoxel(deltaTime) { frame++; if (frame % 3 == 0) { // Get a new position @@ -41,4 +41,4 @@ function moveVoxel() { Voxels.setPacketsPerSecond(300); // Connect a call back that happens every frame -Script.willSendVisualDataCallback.connect(moveVoxel); \ No newline at end of file +Script.update.connect(moveVoxel); \ No newline at end of file diff --git a/examples/multitouchExample.js b/examples/multitouchExample.js index 448d7c3f80..1041651c7b 100644 --- a/examples/multitouchExample.js +++ b/examples/multitouchExample.js @@ -90,7 +90,7 @@ Controller.touchEndEvent.connect(touchEndEvent); -function update() { +function update(deltaTime) { // rotate body yaw for yaw received from multitouch rotate var newOrientation = Quat.multiply(MyAvatar.orientation, Quat.fromVec3( { x: 0, y: yawFromMultiTouch, z: 0 } )); if (wantDebugging) { @@ -110,7 +110,7 @@ function update() { MyAvatar.headPitch = newPitch; pitchFromMultiTouch = 0; } -Script.willSendVisualDataCallback.connect(update); +Script.update.connect(update); function scriptEnding() { diff --git a/examples/overlaysExample.js b/examples/overlaysExample.js index d97ec9e8fd..60f924338f 100644 --- a/examples/overlaysExample.js +++ b/examples/overlaysExample.js @@ -186,7 +186,7 @@ var toolAVisible = false; var count = 0; // Our update() function is called at approximately 60fps, and we will use it to animate our various overlays -function update() { +function update(deltaTime) { count++; // every second or so, toggle the visibility our our blinking tool @@ -226,7 +226,7 @@ function update() { // update our 3D line to go from origin to our avatar's position Overlays.editOverlay(line3d, { end: MyAvatar.position } ); } -Script.willSendVisualDataCallback.connect(update); +Script.update.connect(update); // The slider is handled in the mouse event callbacks. diff --git a/examples/paintGun.js b/examples/paintGun.js index 56e916a183..6b6e78b43e 100644 --- a/examples/paintGun.js +++ b/examples/paintGun.js @@ -13,7 +13,7 @@ for (t = 0; t < numberOfTriggers; t++) { triggerPulled[t] = false; } -function checkController() { +function checkController(deltaTime) { var numberOfTriggers = Controller.getNumberOfTriggers(); var numberOfSpatialControls = Controller.getNumberOfSpatialControls(); var controllersPerTrigger = numberOfSpatialControls / numberOfTriggers; @@ -62,8 +62,9 @@ function checkController() { // This is the script for the particles that this gun shoots. var script = - " function collisionWithVoxel(voxel) { " + + " function collisionWithVoxel(voxel, penetration) { " + " print('collisionWithVoxel(voxel)... '); " + + " Vec3.print('penetration=', penetration); " + " print('myID=' + Particle.getID() + '\\n'); " + " var voxelColor = { red: voxel.red, green: voxel.green, blue: voxel.blue };" + " var voxelAt = { x: voxel.x, y: voxel.y, z: voxel.z };" + @@ -93,4 +94,4 @@ function checkController() { // register the call back so it fires before each data send -Script.willSendVisualDataCallback.connect(checkController); +Script.update.connect(checkController); diff --git a/examples/particleBird.js b/examples/particleBird.js index 440a91166c..5cd068bd60 100644 --- a/examples/particleBird.js +++ b/examples/particleBird.js @@ -99,7 +99,7 @@ var properties = { var range = 1.0; // Distance around avatar where I can move // Create the actual bird var particleID = Particles.addParticle(properties); -function moveBird() { +function moveBird(deltaTime) { // check to see if we've been running long enough that our bird is dead var nowTimeInSeconds = new Date().getTime() / 1000; @@ -194,4 +194,4 @@ function moveBird() { } // register the call back so it fires before each data send -Script.willSendVisualDataCallback.connect(moveBird); +Script.update.connect(moveBird); diff --git a/examples/particleModelExample.js b/examples/particleModelExample.js index 2f36445d1a..c43956cd3e 100644 --- a/examples/particleModelExample.js +++ b/examples/particleModelExample.js @@ -47,7 +47,7 @@ var modelAParticleID = Particles.addParticle(modelPropertiesA); var modelBParticleID = Particles.addParticle(modelPropertiesB); var ballParticleID = Particles.addParticle(ballProperties); -function endAfterAWhile() { +function endAfterAWhile(deltaTime) { // stop it... if (count >= stopAfter) { print("calling Script.stop()"); @@ -60,5 +60,5 @@ function endAfterAWhile() { // register the call back so it fires before each data send -Script.willSendVisualDataCallback.connect(endAfterAWhile); +Script.update.connect(endAfterAWhile); diff --git a/examples/playSound.js b/examples/playSound.js index 657f052aa5..18857826ea 100644 --- a/examples/playSound.js +++ b/examples/playSound.js @@ -5,7 +5,7 @@ // First, load the clap sound from a URL var clap = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Animals/bushtit_1.raw"); -function maybePlaySound() { +function maybePlaySound(deltaTime) { if (Math.random() < 0.01) { // Set the location and other info for the sound to play var options = new AudioInjectionOptions();
 @@ -17,4 +17,4 @@ function maybePlaySound() { } // Connect a call back that happens every frame -Script.willSendVisualDataCallback.connect(maybePlaySound); \ No newline at end of file +Script.update.connect(maybePlaySound); \ No newline at end of file diff --git a/examples/ribbon.js b/examples/ribbon.js index 7858c744fe..a6e1edfa4a 100644 --- a/examples/ribbon.js +++ b/examples/ribbon.js @@ -122,7 +122,7 @@ var velocity; var hueAngle = 0; var smoothedOffset; -function step() { +function step(deltaTime) { if (stateHistory.length === 0) { // start at a random position within the bounds, with a random velocity position = randomVector(BOUNDS_MIN, BOUNDS_MAX); @@ -170,4 +170,4 @@ function step() { hueAngle = (hueAngle + 1) % MAX_HUE_ANGLE; } -Script.willSendVisualDataCallback.connect(step); +Script.update.connect(step); diff --git a/examples/rideAlongWithAParticleExample.js b/examples/rideAlongWithAParticleExample.js index b2b6627063..1148b96b4d 100644 --- a/examples/rideAlongWithAParticleExample.js +++ b/examples/rideAlongWithAParticleExample.js @@ -22,7 +22,7 @@ var particleA = Particles.addParticle( lifetime: (lengthOfRide * 60) + 1 }); -function rideWithParticle() { +function rideWithParticle(deltaTime) { if (iteration <= lengthOfRide) { @@ -46,5 +46,5 @@ function rideWithParticle() { // register the call back so it fires before each data send -Script.willSendVisualDataCallback.connect(rideWithParticle); +Script.update.connect(rideWithParticle); diff --git a/examples/seeingVoxelsExample.js b/examples/seeingVoxelsExample.js index 93f605755f..62ebd599ee 100644 --- a/examples/seeingVoxelsExample.js +++ b/examples/seeingVoxelsExample.js @@ -31,7 +31,7 @@ function init() { } } -function keepLooking() { +function keepLooking(deltaTime) { //print("count =" + count); if (count == 0) { @@ -63,7 +63,7 @@ function scriptEnding() { } // register the call back so it fires before each data send -Script.willSendVisualDataCallback.connect(keepLooking); +Script.update.connect(keepLooking); // register our scriptEnding callback Script.scriptEnding.connect(scriptEnding); diff --git a/examples/spaceInvadersExample.js b/examples/spaceInvadersExample.js index c817afcdd4..61ff93fc8f 100644 --- a/examples/spaceInvadersExample.js +++ b/examples/spaceInvadersExample.js @@ -207,7 +207,7 @@ function displayGameOver() { print("Game over..."); } -function update() { +function update(deltaTime) { if (!gameOver) { //print("updating space invaders... iteration="+iteration); iteration++; @@ -257,7 +257,7 @@ function update() { } // register the call back so it fires before each data send -Script.willSendVisualDataCallback.connect(update); +Script.update.connect(update); function cleanupGame() { print("cleaning up game..."); @@ -392,8 +392,9 @@ function deleteIfInvader(possibleInvaderParticle) { } } -function particleCollisionWithParticle(particleA, particleB) { +function particleCollisionWithParticle(particleA, particleB, penetration) { print("particleCollisionWithParticle() a.id="+particleA.id + " b.id=" + particleB.id); + Vec3.print('particleCollisionWithParticle() penetration=', penetration); if (missileFired) { myMissile = Particles.identifyParticle(myMissile); if (myMissile.id == particleA.id) { diff --git a/examples/toyball.js b/examples/toyball.js index c5672877f7..36f1aa11e5 100644 --- a/examples/toyball.js +++ b/examples/toyball.js @@ -208,7 +208,7 @@ function checkControllerSide(whichSide) { } -function checkController() { +function checkController(deltaTime) { var numberOfButtons = Controller.getNumberOfButtons(); var numberOfTriggers = Controller.getNumberOfTriggers(); var numberOfSpatialControls = Controller.getNumberOfSpatialControls(); @@ -226,4 +226,4 @@ function checkController() { // register the call back so it fires before each data send -Script.willSendVisualDataCallback.connect(checkController); +Script.update.connect(checkController); diff --git a/examples/voxelBird.js b/examples/voxelBird.js index 254f93c21e..1e33851ff6 100644 --- a/examples/voxelBird.js +++ b/examples/voxelBird.js @@ -73,7 +73,7 @@ var moved = true; var CHANCE_OF_MOVING = 0.05; var CHANCE_OF_TWEETING = 0.05; -function moveBird() { +function moveBird(deltaTime) { frame++; if (frame % 3 == 0) { // Tweeting behavior @@ -130,4 +130,4 @@ function moveBird() { Voxels.setPacketsPerSecond(10000); // Connect a call back that happens every frame -Script.willSendVisualDataCallback.connect(moveBird); \ No newline at end of file +Script.update.connect(moveBird); \ No newline at end of file diff --git a/examples/voxelDrumming.js b/examples/voxelDrumming.js index a2acb05958..178c6734d8 100644 --- a/examples/voxelDrumming.js +++ b/examples/voxelDrumming.js @@ -70,14 +70,13 @@ function clamp(valueToClamp, minValue, maxValue) { return Math.max(minValue, Math.min(maxValue, valueToClamp)); } -function produceCollisionSound(palm, voxelDetail) { +function produceCollisionSound(deltaTime, palm, voxelDetail) { // Collision between finger and a voxel plays sound var palmVelocity = Controller.getSpatialControlVelocity(palm * 2); var speed = Vec3.length(palmVelocity); var fingerTipPosition = Controller.getSpatialControlPosition(palm * 2 + 1); - var deltaTime = 1/60; //close enough var LOWEST_FREQUENCY = 100.0; var HERTZ_PER_RGB = 3.0; var DECAY_PER_SAMPLE = 0.0005; @@ -97,9 +96,7 @@ function produceCollisionSound(palm, voxelDetail) { Audio.startDrumSound(volume, frequency, DURATION_MAX, DECAY_PER_SAMPLE, audioOptions); } -function update() { - var deltaTime = 1/60; //close enough - +function update(deltaTime) { // Voxel Drumming with fingertips if enabled if (Menu.isOptionChecked("Voxel Drumming")) { @@ -111,7 +108,7 @@ function update() { if (!isColliding[palm]) { // Collision has just started isColliding[palm] = true; - produceCollisionSound(palm, voxel); + produceCollisionSound(deltaTime, palm, voxel); // Set highlight voxel Overlays.editOverlay(highlightVoxel, @@ -156,7 +153,7 @@ function update() { } // palm loop } // menu item check } -Script.willSendVisualDataCallback.connect(update); +Script.update.connect(update); function scriptEnding() { Overlays.deleteOverlay(highlightVoxel); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1e9d206462..6f3e745f21 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1562,14 +1562,14 @@ void Application::init() { // connect the _particleCollisionSystem to our script engine's ParticleScriptingInterface connect(&_particleCollisionSystem, - SIGNAL(particleCollisionWithVoxel(const ParticleID&, const VoxelDetail&)), + SIGNAL(particleCollisionWithVoxel(const ParticleID&, const VoxelDetail&, const glm::vec3&)), ScriptEngine::getParticlesScriptingInterface(), - SLOT(forwardParticleCollisionWithVoxel(const ParticleID&, const VoxelDetail&))); + SLOT(forwardParticleCollisionWithVoxel(const ParticleID&, const VoxelDetail&, const glm::vec3&))); connect(&_particleCollisionSystem, - SIGNAL(particleCollisionWithParticle(const ParticleID&, const ParticleID&)), + SIGNAL(particleCollisionWithParticle(const ParticleID&, const ParticleID&, const glm::vec3&)), ScriptEngine::getParticlesScriptingInterface(), - SLOT(forwardParticleCollisionWithParticle(const ParticleID&, const ParticleID&))); + SLOT(forwardParticleCollisionWithParticle(const ParticleID&, const ParticleID&, const glm::vec3&))); _pieMenu.init("./resources/images/hifi-interface-tools-v2-pie.svg", _glWidget->width(), @@ -2383,8 +2383,8 @@ void Application::displaySide(Camera& whichCamera, bool selfAvatarOnly) { glMaterialfv(GL_FRONT, GL_SPECULAR, WHITE_SPECULAR_COLOR); } - bool forceRenderMyHead = (whichCamera.getInterpolatedMode() == CAMERA_MODE_MIRROR); - _avatarManager.renderAvatars(forceRenderMyHead, selfAvatarOnly); + bool mirrorMode = (whichCamera.getInterpolatedMode() == CAMERA_MODE_MIRROR); + _avatarManager.renderAvatars(mirrorMode, selfAvatarOnly); if (!selfAvatarOnly) { // Render the world box diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index ef6975d223..389791e89b 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -201,7 +201,7 @@ static TextRenderer* textRenderer(TextRendererType type) { return displayNameRenderer; } -void Avatar::render() { +void Avatar::render(bool forShadowMap) { glm::vec3 toTarget = _position - Application::getInstance()->getAvatar()->getPosition(); float lengthToTarget = glm::length(toTarget); @@ -209,7 +209,7 @@ void Avatar::render() { // glow when moving in the distance const float GLOW_DISTANCE = 5.0f; - Glower glower(_moving && lengthToTarget > GLOW_DISTANCE ? 1.0f : 0.0f); + Glower glower(_moving && lengthToTarget > GLOW_DISTANCE && !forShadowMap ? 1.0f : 0.0f); // render body if (Menu::getInstance()->isOptionChecked(MenuOption::RenderSkeletonCollisionProxies)) { @@ -233,7 +233,7 @@ void Avatar::render() { float angle = abs(angleBetween(toTarget + delta, toTarget - delta)); float sphereRadius = getHead()->getAverageLoudness() * SPHERE_LOUDNESS_SCALING; - if ((sphereRadius > MIN_SPHERE_SIZE) && (angle < MAX_SPHERE_ANGLE) && (angle > MIN_SPHERE_ANGLE)) { + if (!forShadowMap && (sphereRadius > MIN_SPHERE_SIZE) && (angle < MAX_SPHERE_ANGLE) && (angle > MIN_SPHERE_ANGLE)) { glColor4f(SPHERE_COLOR[0], SPHERE_COLOR[1], SPHERE_COLOR[2], 1.f - angle / MAX_SPHERE_ANGLE); glPushMatrix(); glTranslatef(_position.x, _position.y, _position.z); @@ -243,7 +243,10 @@ void Avatar::render() { } } const float DISPLAYNAME_DISTANCE = 10.0f; - setShowDisplayName(lengthToTarget < DISPLAYNAME_DISTANCE); + setShowDisplayName(!forShadowMap && lengthToTarget < DISPLAYNAME_DISTANCE); + if (forShadowMap) { + return; + } renderDisplayName(); if (!_chatMessage.empty()) { @@ -484,10 +487,19 @@ bool Avatar::findRayIntersection(const glm::vec3& origin, const glm::vec3& direc bool Avatar::findSphereCollisions(const glm::vec3& penetratorCenter, float penetratorRadius, CollisionList& collisions, int skeletonSkipIndex) { - // Temporarily disabling collisions against the skeleton because the collision proxies up - // near the neck are bad and prevent the hand from hitting the face. - //return _skeletonModel.findSphereCollisions(penetratorCenter, penetratorRadius, collisions, 1.0f, skeletonSkipIndex); - return getHead()->getFaceModel().findSphereCollisions(penetratorCenter, penetratorRadius, collisions); + return _skeletonModel.findSphereCollisions(penetratorCenter, penetratorRadius, collisions, skeletonSkipIndex); + // Temporarily disabling collisions against the head because most of its collision proxies are bad. + //return getHead()->getFaceModel().findSphereCollisions(penetratorCenter, penetratorRadius, collisions); +} + +bool Avatar::findCollisions(const QVector& shapes, CollisionList& collisions) { + _skeletonModel.updateShapePositions(); + bool collided = _skeletonModel.findCollisions(shapes, collisions); + + Model& headModel = getHead()->getFaceModel(); + headModel.updateShapePositions(); + collided = headModel.findCollisions(shapes, collisions); + return collided; } bool Avatar::findParticleCollisions(const glm::vec3& particleCenter, float particleRadius, CollisionList& collisions) { diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 8de5da8d50..9e0ec8100f 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -74,7 +74,7 @@ public: void init(); void simulate(float deltaTime); - void render(); + void render(bool forShadowMap = false); //setters void setDisplayingLookatVectors(bool displayingLookatVectors) { getHead()->setRenderLookatVectors(displayingLookatVectors); } @@ -99,6 +99,11 @@ public: bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance) const; + /// \param shapes list of shapes to collide against avatar + /// \param collisions list to store collision results + /// \return true if at least one shape collided with avatar + bool findCollisions(const QVector& shapes, CollisionList& collisions); + /// Checks for penetration between the described sphere and the avatar. /// \param penetratorCenter the center of the penetration test sphere /// \param penetratorRadius the radius of the penetration test sphere diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 847baf782f..57e800a401 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -69,7 +69,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { simulateAvatarFades(deltaTime); } -void AvatarManager::renderAvatars(bool forceRenderMyHead, bool selfAvatarOnly) { +void AvatarManager::renderAvatars(bool forShadowMapOrMirror, bool selfAvatarOnly) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::renderAvatars()"); bool renderLookAtVectors = Menu::getInstance()->isOptionChecked(MenuOption::LookAtVectors); @@ -83,16 +83,16 @@ void AvatarManager::renderAvatars(bool forceRenderMyHead, bool selfAvatarOnly) { avatar->init(); } if (avatar == static_cast(_myAvatar.data())) { - _myAvatar->render(forceRenderMyHead); + _myAvatar->render(forShadowMapOrMirror); } else { - avatar->render(); + avatar->render(forShadowMapOrMirror); } avatar->setDisplayingLookatVectors(renderLookAtVectors); } - renderAvatarFades(); + renderAvatarFades(forShadowMapOrMirror); } else { // just render myAvatar - _myAvatar->render(forceRenderMyHead, true); + _myAvatar->render(forShadowMapOrMirror); _myAvatar->setDisplayingLookatVectors(renderLookAtVectors); } } @@ -115,13 +115,13 @@ void AvatarManager::simulateAvatarFades(float deltaTime) { } } -void AvatarManager::renderAvatarFades() { +void AvatarManager::renderAvatarFades(bool forShadowMap) { // render avatar fades - Glower glower; + Glower glower(forShadowMap ? 0.0f : 1.0f); foreach(const AvatarSharedPointer& fadingAvatar, _avatarFades) { Avatar* avatar = static_cast(fadingAvatar.data()); - avatar->render(); + avatar->render(forShadowMap); } } diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index db24d5bf4e..c690dfa966 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -29,7 +29,7 @@ public: MyAvatar* getMyAvatar() { return _myAvatar.data(); } void updateOtherAvatars(float deltaTime); - void renderAvatars(bool forceRenderMyHead, bool selfAvatarOnly = false); + void renderAvatars(bool forShadowMapOrMirror = false, bool selfAvatarOnly = false); void clearOtherAvatars(); @@ -45,7 +45,7 @@ private: void processKillAvatar(const QByteArray& datagram); void simulateAvatarFades(float deltaTime); - void renderAvatarFades(); + void renderAvatarFades(bool forShadowMap); // virtual override AvatarHash::iterator erase(const AvatarHash::iterator& iterator); diff --git a/interface/src/avatar/Hand.cpp b/interface/src/avatar/Hand.cpp index 29e46fc816..ce2f2a242e 100644 --- a/interface/src/avatar/Hand.cpp +++ b/interface/src/avatar/Hand.cpp @@ -96,7 +96,7 @@ void Hand::playSlaps(PalmData& palm, Avatar* avatar) const float MAX_COLLISIONS_PER_AVATAR = 32; static CollisionList handCollisions(MAX_COLLISIONS_PER_AVATAR); -void Hand::collideAgainstAvatar(Avatar* avatar, bool isMyHand) { +void Hand::collideAgainstAvatarOld(Avatar* avatar, bool isMyHand) { if (!avatar || avatar == _owningAvatar) { // don't collide with our own hands (that is done elsewhere) return; @@ -117,17 +117,9 @@ void Hand::collideAgainstAvatar(Avatar* avatar, bool isMyHand) { for (int j = 0; j < handCollisions.size(); ++j) { CollisionInfo* collision = handCollisions.getCollision(j); if (isMyHand) { - if (!avatar->collisionWouldMoveAvatar(*collision)) { - // we resolve the hand from collision when it belongs to MyAvatar AND the other Avatar is - // not expected to respond to the collision (hand hit unmovable part of their Avatar) - totalPenetration = addPenetrations(totalPenetration, collision->_penetration); - } - } else { - // when !isMyHand then avatar is MyAvatar and we apply the collision - // which might not do anything (hand hit unmovable part of MyAvatar) however - // we don't resolve the hand's penetration because we expect the remote - // simulation to do the right thing. - avatar->applyCollision(*collision); + // we resolve the hand from collision when it belongs to MyAvatar AND the other Avatar is + // not expected to respond to the collision (hand hit unmovable part of their Avatar) + totalPenetration = addPenetrations(totalPenetration, collision->_penetration); } } } @@ -138,6 +130,66 @@ void Hand::collideAgainstAvatar(Avatar* avatar, bool isMyHand) { } } +void Hand::collideAgainstAvatar(Avatar* avatar, bool isMyHand) { + if (!avatar || avatar == _owningAvatar) { + // don't collide hands against ourself (that is done elsewhere) + return; + } + + // 2 = NUM_HANDS + int palmIndices[2]; + getLeftRightPalmIndices(*palmIndices, *(palmIndices + 1)); + + const SkeletonModel& skeletonModel = _owningAvatar->getSkeletonModel(); + int jointIndices[2]; + jointIndices[0] = skeletonModel.getLeftHandJointIndex(); + jointIndices[1] = skeletonModel.getRightHandJointIndex(); + + palmIndices[1] = -1; // adebug temporarily disable right hand + jointIndices[1] = -1; // adebug temporarily disable right hand + + for (size_t i = 0; i < 1; i++) { + int palmIndex = palmIndices[i]; + int jointIndex = jointIndices[i]; + if (palmIndex == -1 || jointIndex == -1) { + continue; + } + PalmData& palm = _palms[palmIndex]; + if (!palm.isActive()) { + continue; + } + if (isMyHand && Menu::getInstance()->isOptionChecked(MenuOption::PlaySlaps)) { + playSlaps(palm, avatar); + } + + handCollisions.clear(); + QVector shapes; + skeletonModel.getHandShapes(jointIndex, shapes); + bool collided = isMyHand ? avatar->findCollisions(shapes, handCollisions) : avatar->findCollisions(shapes, handCollisions); + if (collided) { + //if (avatar->findCollisions(shapes, handCollisions)) { + glm::vec3 averagePenetration; + glm::vec3 averageContactPoint; + for (int j = 0; j < handCollisions.size(); ++j) { + CollisionInfo* collision = handCollisions.getCollision(j); + averagePenetration += collision->_penetration; + averageContactPoint += collision->_contactPoint; + } + averagePenetration /= float(handCollisions.size()); + if (isMyHand) { + // our hand against other avatar + // for now we resolve it to test shapes/collisions + // TODO: only partially resolve this penetration + palm.addToPosition(-averagePenetration); + } else { + // someone else's hand against MyAvatar + // TODO: submit collision info to MyAvatar which should lean accordingly + averageContactPoint /= float(handCollisions.size()); + } + } + } +} + void Hand::collideAgainstOurself() { if (!Menu::getInstance()->isOptionChecked(MenuOption::HandsCollideWithSelf)) { return; @@ -147,27 +199,27 @@ void Hand::collideAgainstOurself() { getLeftRightPalmIndices(leftPalmIndex, rightPalmIndex); float scaledPalmRadius = PALM_COLLISION_RADIUS * _owningAvatar->getScale(); + const Model& skeletonModel = _owningAvatar->getSkeletonModel(); for (size_t i = 0; i < getNumPalms(); i++) { PalmData& palm = getPalms()[i]; if (!palm.isActive()) { continue; - } - const Model& skeletonModel = _owningAvatar->getSkeletonModel(); + } // ignoring everything below the parent of the parent of the last free joint int skipIndex = skeletonModel.getParentJointIndex(skeletonModel.getParentJointIndex( skeletonModel.getLastFreeJointIndex((i == leftPalmIndex) ? skeletonModel.getLeftHandJointIndex() : (i == rightPalmIndex) ? skeletonModel.getRightHandJointIndex() : -1))); handCollisions.clear(); - glm::vec3 totalPenetration; if (_owningAvatar->findSphereCollisions(palm.getPosition(), scaledPalmRadius, handCollisions, skipIndex)) { + glm::vec3 totalPenetration; for (int j = 0; j < handCollisions.size(); ++j) { CollisionInfo* collision = handCollisions.getCollision(j); totalPenetration = addPenetrations(totalPenetration, collision->_penetration); } + // resolve penetration + palm.addToPosition(-totalPenetration); } - // resolve penetration - palm.addToPosition(-totalPenetration); } } diff --git a/interface/src/avatar/Hand.h b/interface/src/avatar/Hand.h index bf335d1bdd..a1b1875424 100755 --- a/interface/src/avatar/Hand.h +++ b/interface/src/avatar/Hand.h @@ -56,6 +56,7 @@ public: const glm::vec3& getLeapFingerTipBallPosition (int ball) const { return _leapFingerTipBalls [ball].position;} const glm::vec3& getLeapFingerRootBallPosition(int ball) const { return _leapFingerRootBalls[ball].position;} + void collideAgainstAvatarOld(Avatar* avatar, bool isMyHand); void collideAgainstAvatar(Avatar* avatar, bool isMyHand); void collideAgainstOurself(); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 51fcad20ae..e981078892 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -452,24 +452,24 @@ void MyAvatar::renderDebugBodyPoints() { } -void MyAvatar::render(bool forceRenderHead, bool avatarOnly) { +void MyAvatar::render(bool forShadowMapOrMirror) { // don't render if we've been asked to disable local rendering if (!_shouldRender) { return; // exit early } + if (Menu::getInstance()->isOptionChecked(MenuOption::Avatars)) { + renderBody(forShadowMapOrMirror); + } // render body if (Menu::getInstance()->isOptionChecked(MenuOption::RenderSkeletonCollisionProxies)) { - _skeletonModel.renderCollisionProxies(1.f); + _skeletonModel.renderCollisionProxies(0.8f); } if (Menu::getInstance()->isOptionChecked(MenuOption::RenderHeadCollisionProxies)) { - _skeletonModel.renderCollisionProxies(1.f); + getHead()->getFaceModel().renderCollisionProxies(0.8f); } - if (Menu::getInstance()->isOptionChecked(MenuOption::Avatars)) { - renderBody(forceRenderHead); - } - setShowDisplayName(!avatarOnly); - if (avatarOnly) { + setShowDisplayName(!forShadowMapOrMirror); + if (forShadowMapOrMirror) { return; } renderDisplayName(); @@ -941,6 +941,11 @@ void MyAvatar::updateCollisionWithAvatars(float deltaTime) { } float theirBoundingRadius = avatar->getBoundingRadius(); if (distance < myBoundingRadius + theirBoundingRadius) { + _skeletonModel.updateShapePositions(); + Model& headModel = getHead()->getFaceModel(); + headModel.updateShapePositions(); + + /* TODO: Andrew to fix Avatar-Avatar body collisions Extents theirStaticExtents = _skeletonModel.getStaticExtents(); glm::vec3 staticScale = theirStaticExtents.maximum - theirStaticExtents.minimum; float theirCapsuleRadius = 0.25f * (staticScale.x + staticScale.z); @@ -952,6 +957,7 @@ void MyAvatar::updateCollisionWithAvatars(float deltaTime) { // move the avatar out by half the penetration setPosition(_position - 0.5f * penetration); } + */ // collide our hands against them getHand()->collideAgainstAvatar(avatar, true); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index aeb9e6d968..e9dfbd8bd0 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -35,7 +35,7 @@ public: void simulate(float deltaTime); void updateFromGyros(float deltaTime); - void render(bool forceRenderHead, bool avatarOnly = false); + void render(bool forShadowMapOrMirror = false); void renderDebugBodyPoints(); void renderHeadMouse() const; diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index edb11a631e..7c7202566a 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -73,6 +73,17 @@ bool SkeletonModel::render(float alpha) { return true; } +void SkeletonModel::getHandShapes(int jointIndex, QVector& shapes) const { + if (jointIndex == -1) { + return; + } + if (jointIndex == getLeftHandJointIndex() + || jointIndex == getRightHandJointIndex()) { + // TODO: also add fingers and other hand-parts + shapes.push_back(_shapes[jointIndex]); + } +} + class IndexValue { public: int index; diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 6f80d77edc..3d95d805ea 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -24,6 +24,10 @@ public: void simulate(float deltaTime, bool delayLoad = false); bool render(float alpha); + + /// \param jointIndex index of hand joint + /// \param shapes[out] list in which is stored pointers to hand shapes + void getHandShapes(int jointIndex, QVector& shapes) const; protected: diff --git a/interface/src/renderer/FBXReader.cpp b/interface/src/renderer/FBXReader.cpp index bd1e8a291c..f4c198dce2 100644 --- a/interface/src/renderer/FBXReader.cpp +++ b/interface/src/renderer/FBXReader.cpp @@ -6,6 +6,7 @@ // Copyright (c) 2013 High Fidelity, Inc. All rights reserved. // +#include #include #include #include @@ -21,6 +22,7 @@ #include #include +#include #include #include "FBXReader.h" @@ -28,6 +30,22 @@ using namespace std; +void Extents::reset() { + minimum = glm::vec3(FLT_MAX); + maximum = glm::vec3(-FLT_MAX); +} + +bool Extents::containsPoint(const glm::vec3& point) const { + return (point.x >= minimum.x && point.x <= maximum.x + && point.y >= minimum.y && point.y <= maximum.y + && point.z >= minimum.z && point.z <= maximum.z); +} + +void Extents::addPoint(const glm::vec3& point) { + minimum = glm::min(minimum, point); + maximum = glm::max(maximum, point); +} + static int fbxGeometryMetaTypeId = qRegisterMetaType(); template QVariant readBinaryArray(QDataStream& in) { @@ -538,8 +556,9 @@ public: FBXBlendshape blendshape; }; -void printNode(const FBXNode& node, int indent) { - QByteArray spaces(indent, ' '); +void printNode(const FBXNode& node, int indentLevel) { + int indentLength = 2; + QByteArray spaces(indentLevel * indentLength, ' '); QDebug nodeDebug = qDebug(); nodeDebug.nospace() << spaces.data() << node.name.data() << ": "; @@ -548,7 +567,7 @@ void printNode(const FBXNode& node, int indent) { } foreach (const FBXNode& child, node.children) { - printNode(child, indent + 1); + printNode(child, indentLevel + 1); } } @@ -842,6 +861,21 @@ QString getString(const QVariant& value) { return list.isEmpty() ? value.toString() : list.at(0).toString(); } +class JointShapeInfo { +public: + JointShapeInfo() : numVertices(0), numProjectedVertices(0), averageVertex(0.f), boneBegin(0.f), averageRadius(0.f) { + extents.reset(); + } + + // NOTE: the points here are in the "joint frame" which has the "jointEnd" at the origin + int numVertices; // num vertices from contributing meshes + int numProjectedVertices; // num vertices that successfully project onto bone axis + Extents extents; // max and min extents of mesh vertices (in joint frame) + glm::vec3 averageVertex; // average of all mesh vertices (in joint frame) + glm::vec3 boneBegin; // parent joint location (in joint frame) + float averageRadius; // average distance from mesh points to averageVertex +}; + FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) { QHash meshes; QVector blendshapes; @@ -1250,9 +1284,15 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) } joint.boneRadius = 0.0f; joint.inverseBindRotation = joint.inverseDefaultRotation; + joint.name = model.name; + joint.shapePosition = glm::vec3(0.f); + joint.shapeType = Shape::UNKNOWN_SHAPE; geometry.joints.append(joint); geometry.jointIndices.insert(model.name, geometry.joints.size()); } + // for each joint we allocate a JointShapeInfo in which we'll store collision shape info + QVector jointShapeInfos; + jointShapeInfos.resize(geometry.joints.size()); // find our special joints geometry.leftEyeJointIndex = modelIDs.indexOf(jointEyeLeftID); @@ -1274,12 +1314,9 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) geometry.neckPivot = glm::vec3(transform[3][0], transform[3][1], transform[3][2]); } - geometry.bindExtents.minimum = glm::vec3(FLT_MAX, FLT_MAX, FLT_MAX); - geometry.bindExtents.maximum = glm::vec3(-FLT_MAX, -FLT_MAX, -FLT_MAX); - geometry.staticExtents.minimum = glm::vec3(FLT_MAX, FLT_MAX, FLT_MAX); - geometry.staticExtents.maximum = glm::vec3(-FLT_MAX, -FLT_MAX, -FLT_MAX); - geometry.meshExtents.minimum = glm::vec3(FLT_MAX, FLT_MAX, FLT_MAX); - geometry.meshExtents.maximum = glm::vec3(-FLT_MAX, -FLT_MAX, -FLT_MAX); + geometry.bindExtents.reset(); + geometry.staticExtents.reset(); + geometry.meshExtents.reset(); QVariantHash springs = mapping.value("spring").toHash(); QVariant defaultSpring = springs.value("default"); @@ -1402,8 +1439,7 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) // update the bind pose extents glm::vec3 bindTranslation = extractTranslation(geometry.offset * joint.bindTransform); - geometry.bindExtents.minimum = glm::min(geometry.bindExtents.minimum, bindTranslation); - geometry.bindExtents.maximum = glm::max(geometry.bindExtents.maximum, bindTranslation); + geometry.bindExtents.addPoint(bindTranslation); } } @@ -1416,11 +1452,6 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) cluster.jointIndex = 0; } extracted.mesh.clusters.append(cluster); - // BUG: joints that fall into this context do not get their bindTransform and - // inverseBindRotation data members properly set. This causes bad boneRadius - // and boneLength calculations for collision proxies. Affected joints are usually: - // hair, teeth, tongue. I tried to figure out how to fix this but was going - // crosseyed trying to understand FBX so I gave up for the time being -- Andrew. } // whether we're skinned depends on how many clusters are attached @@ -1437,20 +1468,26 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) const FBXCluster& fbxCluster = extracted.mesh.clusters.at(i); int jointIndex = fbxCluster.jointIndex; FBXJoint& joint = geometry.joints[jointIndex]; - glm::vec3 boneEnd = extractTranslation(inverseModelTransform * joint.bindTransform); + glm::mat4 transformJointToMesh = inverseModelTransform * joint.bindTransform; + glm::quat rotateMeshToJoint = glm::inverse(extractRotation(transformJointToMesh)); + glm::vec3 boneEnd = extractTranslation(transformJointToMesh); + glm::vec3 boneBegin = boneEnd; glm::vec3 boneDirection; float boneLength; if (joint.parentIndex != -1) { - boneDirection = boneEnd - extractTranslation(inverseModelTransform * - geometry.joints[joint.parentIndex].bindTransform); + boneBegin = extractTranslation(inverseModelTransform * geometry.joints[joint.parentIndex].bindTransform); + boneDirection = boneEnd - boneBegin; boneLength = glm::length(boneDirection); if (boneLength > EPSILON) { boneDirection /= boneLength; } } + float radiusScale = extractUniformScale(joint.transform * fbxCluster.inverseBindMatrix); + JointShapeInfo& jointShapeInfo = jointShapeInfos[jointIndex]; + jointShapeInfo.boneBegin = rotateMeshToJoint * (radiusScale * (boneBegin - boneEnd)); + bool jointIsStatic = joint.freeLineage.isEmpty(); glm::vec3 jointTranslation = extractTranslation(geometry.offset * joint.bindTransform); - float radiusScale = extractUniformScale(joint.transform * fbxCluster.inverseBindMatrix); float totalWeight = 0.0f; for (int j = 0; j < cluster.indices.size(); j++) { int oldIndex = cluster.indices.at(j); @@ -1464,13 +1501,17 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) const glm::vec3& vertex = extracted.mesh.vertices.at(it.value()); float proj = glm::dot(boneDirection, vertex - boneEnd); if (proj < 0.0f && proj > -boneLength) { - joint.boneRadius = glm::max(joint.boneRadius, radiusScale * glm::distance( - vertex, boneEnd + boneDirection * proj)); + joint.boneRadius = glm::max(joint.boneRadius, + radiusScale * glm::distance(vertex, boneEnd + boneDirection * proj)); + ++jointShapeInfo.numProjectedVertices; } + glm::vec3 vertexInJointFrame = rotateMeshToJoint * (radiusScale * (vertex - boneEnd)); + jointShapeInfo.extents.addPoint(vertexInJointFrame); + jointShapeInfo.averageVertex += vertexInJointFrame; + ++jointShapeInfo.numVertices; if (jointIsStatic) { // expand the extents of static (nonmovable) joints - geometry.staticExtents.minimum = glm::min(geometry.staticExtents.minimum, vertex + jointTranslation); - geometry.staticExtents.maximum = glm::max(geometry.staticExtents.maximum, vertex + jointTranslation); + geometry.staticExtents.addPoint(vertex + jointTranslation); } } @@ -1493,24 +1534,47 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) } else { int jointIndex = maxJointIndex; FBXJoint& joint = geometry.joints[jointIndex]; - glm::vec3 boneEnd = extractTranslation(inverseModelTransform * joint.bindTransform); + JointShapeInfo& jointShapeInfo = jointShapeInfos[jointIndex]; + + glm::mat4 transformJointToMesh = inverseModelTransform * joint.bindTransform; + glm::quat rotateMeshToJoint = glm::inverse(extractRotation(transformJointToMesh)); + glm::vec3 boneEnd = extractTranslation(transformJointToMesh); + glm::vec3 boneBegin = boneEnd; + glm::vec3 boneDirection; float boneLength; if (joint.parentIndex != -1) { - boneDirection = boneEnd - extractTranslation(inverseModelTransform * - geometry.joints[joint.parentIndex].bindTransform); + boneBegin = extractTranslation(inverseModelTransform * geometry.joints[joint.parentIndex].bindTransform); + boneDirection = boneEnd - boneBegin; boneLength = glm::length(boneDirection); if (boneLength > EPSILON) { boneDirection /= boneLength; } } float radiusScale = extractUniformScale(joint.transform * firstFBXCluster.inverseBindMatrix); + jointShapeInfo.boneBegin = rotateMeshToJoint * (radiusScale * (boneBegin - boneEnd)); + + glm::vec3 averageVertex(0.f); foreach (const glm::vec3& vertex, extracted.mesh.vertices) { float proj = glm::dot(boneDirection, vertex - boneEnd); if (proj < 0.0f && proj > -boneLength) { - joint.boneRadius = glm::max(joint.boneRadius, radiusScale * glm::distance( - vertex, boneEnd + boneDirection * proj)); + joint.boneRadius = glm::max(joint.boneRadius, radiusScale * glm::distance(vertex, boneEnd + boneDirection * proj)); + ++jointShapeInfo.numProjectedVertices; } + glm::vec3 vertexInJointFrame = rotateMeshToJoint * (radiusScale * (vertex - boneEnd)); + jointShapeInfo.extents.addPoint(vertexInJointFrame); + jointShapeInfo.averageVertex += vertexInJointFrame; + averageVertex += vertex; + } + int numVertices = extracted.mesh.vertices.size(); + jointShapeInfo.numVertices = numVertices; + if (numVertices > 0) { + averageVertex /= float(jointShapeInfo.numVertices); + float averageRadius = 0.f; + foreach (const glm::vec3& vertex, extracted.mesh.vertices) { + averageRadius += glm::distance(vertex, averageVertex); + } + jointShapeInfo.averageRadius = averageRadius * radiusScale; } } extracted.mesh.isEye = (maxJointIndex == geometry.leftEyeJointIndex || maxJointIndex == geometry.rightEyeJointIndex); @@ -1560,6 +1624,39 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) geometry.meshes.append(extracted.mesh); } + // now that all joints have been scanned, compute a collision shape for each joint + glm::vec3 defaultCapsuleAxis(0.f, 1.f, 0.f); + for (int i = 0; i < geometry.joints.size(); ++i) { + FBXJoint& joint = geometry.joints[i]; + JointShapeInfo& jointShapeInfo = jointShapeInfos[i]; + + // we use a capsule if the joint ANY mesh vertices successfully projected onto the bone + // AND its boneRadius is not too close to zero + bool collideLikeCapsule = jointShapeInfo.numProjectedVertices > 0 + && glm::length(jointShapeInfo.boneBegin) > EPSILON; + + if (collideLikeCapsule) { + joint.shapeRotation = rotationBetween(defaultCapsuleAxis, jointShapeInfo.boneBegin); + joint.shapePosition = 0.5f * jointShapeInfo.boneBegin; + joint.shapeType = Shape::CAPSULE_SHAPE; + } else { + // collide the joint like a sphere + if (jointShapeInfo.numVertices > 0) { + jointShapeInfo.averageVertex /= float(jointShapeInfo.numVertices); + joint.shapePosition = jointShapeInfo.averageVertex; + } else { + joint.shapePosition = glm::vec3(0.f); + joint.shapeType = Shape::SPHERE_SHAPE; + } + if (jointShapeInfo.numProjectedVertices == 0 + && jointShapeInfo.numVertices > 0) { + // the bone projection algorithm was not able to compute the joint radius + // so we use an alternative measure + jointShapeInfo.averageRadius /= float(jointShapeInfo.numVertices); + joint.boneRadius = jointShapeInfo.averageRadius; + } + } + } geometry.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); // process attachments diff --git a/interface/src/renderer/FBXReader.h b/interface/src/renderer/FBXReader.h index 9d1b81cc0e..71518e53c8 100644 --- a/interface/src/renderer/FBXReader.h +++ b/interface/src/renderer/FBXReader.h @@ -25,6 +25,23 @@ typedef QList FBXNodeList; /// The names of the blendshapes expected by Faceshift, terminated with an empty string. extern const char* FACESHIFT_BLENDSHAPES[]; +class Extents { +public: + /// set minimum and maximum to FLT_MAX and -FLT_MAX respectively + void reset(); + + /// \param point new point to compare against existing limits + /// compare point to current limits and expand them if necessary to contain point + void addPoint(const glm::vec3& point); + + /// \param point + /// \return true if point is within current limits + bool containsPoint(const glm::vec3& point) const; + + glm::vec3 minimum; + glm::vec3 maximum; +}; + /// A node within an FBX document. class FBXNode { public: @@ -64,8 +81,13 @@ public: glm::quat inverseDefaultRotation; glm::quat inverseBindRotation; glm::mat4 bindTransform; + QString name; // temp field for debugging + glm::vec3 shapePosition; // in joint frame + glm::quat shapeRotation; // in joint frame + int shapeType; }; + /// A single binding to a joint in an FBX document. class FBXCluster { public: @@ -125,13 +147,6 @@ public: glm::vec3 scale; }; -class Extents { -public: - - glm::vec3 minimum; - glm::vec3 maximum; -}; - /// A set of meshes extracted from an FBX document. class FBXGeometry { public: diff --git a/interface/src/renderer/GeometryCache.cpp b/interface/src/renderer/GeometryCache.cpp index 5b13e2da42..88d2a8962d 100644 --- a/interface/src/renderer/GeometryCache.cpp +++ b/interface/src/renderer/GeometryCache.cpp @@ -343,7 +343,14 @@ QSharedPointer NetworkGeometry::getLODOrFallback(float distance // if we previously selected a different distance, make sure we've moved far enough to justify switching const float HYSTERESIS_PROPORTION = 0.1f; if (glm::abs(distance - qMax(hysteresis, lodDistance)) / fabsf(hysteresis - lodDistance) < HYSTERESIS_PROPORTION) { - return getLODOrFallback(hysteresis, hysteresis); + lod = _lodParent; + lodDistance = 0.0f; + it = _lods.upperBound(hysteresis); + if (it != _lods.constBegin()) { + it = it - 1; + lod = it.value(); + lodDistance = it.key(); + } } } if (lod->isLoaded()) { diff --git a/interface/src/renderer/Model.cpp b/interface/src/renderer/Model.cpp index 1aab0a4dab..e1fede84d1 100644 --- a/interface/src/renderer/Model.cpp +++ b/interface/src/renderer/Model.cpp @@ -13,10 +13,15 @@ #include "Application.h" #include "Model.h" +#include +#include +#include + using namespace std; Model::Model(QObject* parent) : QObject(parent), + _shapesAreDirty(true), _lodDistance(0.0f), _pupilDilation(0.0f) { // we may have been created in the network thread, but we live in the main thread @@ -100,6 +105,51 @@ void Model::reset() { } } +void Model::clearShapes() { + for (int i = 0; i < _shapes.size(); ++i) { + delete _shapes[i]; + } + _shapes.clear(); +} + +void Model::createCollisionShapes() { + clearShapes(); + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + float uniformScale = extractUniformScale(_scale); + for (int i = 0; i < _jointStates.size(); i++) { + const FBXJoint& joint = geometry.joints[i]; + glm::vec3 meshCenter = _jointStates[i].combinedRotation * joint.shapePosition; + glm::vec3 position = _rotation * (extractTranslation(_jointStates[i].transform) + uniformScale * meshCenter) + _translation; + + float radius = uniformScale * joint.boneRadius; + if (joint.shapeType == Shape::CAPSULE_SHAPE) { + float halfHeight = 0.5f * uniformScale * joint.distanceToParent; + CapsuleShape* shape = new CapsuleShape(radius, halfHeight); + shape->setPosition(position); + _shapes.push_back(shape); + } else { + SphereShape* shape = new SphereShape(radius, position); + _shapes.push_back(shape); + } + } +} + +void Model::updateShapePositions() { + if (_shapesAreDirty && _shapes.size() == _jointStates.size()) { + float uniformScale = extractUniformScale(_scale); + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + for (int i = 0; i < _jointStates.size(); i++) { + const FBXJoint& joint = geometry.joints[i]; + // shape position and rotation need to be in world-frame + glm::vec3 jointToShapeOffset = uniformScale * (_jointStates[i].combinedRotation * joint.shapePosition); + glm::vec3 worldPosition = extractTranslation(_jointStates[i].transform) + jointToShapeOffset + _translation; + _shapes[i]->setPosition(worldPosition); + _shapes[i]->setRotation(_jointStates[i].combinedRotation * joint.shapeRotation); + } + _shapesAreDirty = false; + } +} + void Model::simulate(float deltaTime, bool delayLoad) { // update our LOD QVector newJointStates = updateGeometry(delayLoad); @@ -128,6 +178,7 @@ void Model::simulate(float deltaTime, bool delayLoad) { _attachments.append(model); } _resetStates = true; + createCollisionShapes(); } // update the world space transforms for all joints @@ -416,6 +467,7 @@ void Model::setURL(const QUrl& url, const QUrl& fallback, bool retainCurrent, bo // if so instructed, keep the current geometry until the new one is loaded _nextBaseGeometry = _nextGeometry = Application::getInstance()->getGeometryCache()->getGeometry(url, fallback, delayLoad); + _nextLODHysteresis = NetworkGeometry::NO_HYSTERESIS; if (!retainCurrent || !isActive() || _nextGeometry->isLoaded()) { applyNextGeometry(); } @@ -454,20 +506,28 @@ bool Model::findRayIntersection(const glm::vec3& origin, const glm::vec3& direct return false; } -bool Model::findSphereCollisions(const glm::vec3& penetratorCenter, float penetratorRadius, - CollisionList& collisions, float boneScale, int skipIndex) const { +bool Model::findCollisions(const QVector shapes, CollisionList& collisions) { bool collided = false; - const glm::vec3 relativeCenter = penetratorCenter - _translation; + for (int i = 0; i < shapes.size(); ++i) { + const Shape* theirShape = shapes[i]; + for (int j = 0; j < _shapes.size(); ++j) { + const Shape* ourShape = _shapes[j]; + if (ShapeCollider::shapeShape(theirShape, ourShape, collisions)) { + collided = true; + } + } + } + return collided; +} + +bool Model::findSphereCollisions(const glm::vec3& sphereCenter, float sphereRadius, + CollisionList& collisions, int skipIndex) { + bool collided = false; + updateShapePositions(); + SphereShape sphere(sphereRadius, sphereCenter); const FBXGeometry& geometry = _geometry->getFBXGeometry(); - glm::vec3 totalPenetration; - float radiusScale = extractUniformScale(_scale) * boneScale; - for (int i = 0; i < _jointStates.size(); i++) { + for (int i = 0; i < _shapes.size(); i++) { const FBXJoint& joint = geometry.joints[i]; - glm::vec3 end = extractTranslation(_jointStates[i].transform); - float endRadius = joint.boneRadius * radiusScale; - glm::vec3 start = end; - float startRadius = joint.boneRadius * radiusScale; - glm::vec3 bonePenetration; if (joint.parentIndex != -1) { if (skipIndex != -1) { int ancestorIndex = joint.parentIndex; @@ -479,24 +539,13 @@ bool Model::findSphereCollisions(const glm::vec3& penetratorCenter, float penetr } while (ancestorIndex != -1); } - start = extractTranslation(_jointStates[joint.parentIndex].transform); - startRadius = geometry.joints[joint.parentIndex].boneRadius * radiusScale; } - if (findSphereCapsuleConePenetration(relativeCenter, penetratorRadius, start, end, - startRadius, endRadius, bonePenetration)) { - totalPenetration = addPenetrations(totalPenetration, bonePenetration); - CollisionInfo* collision = collisions.getNewCollision(); - if (collision) { - collision->_type = MODEL_COLLISION; - collision->_data = (void*)(this); - collision->_flags = i; - collision->_contactPoint = penetratorCenter + penetratorRadius * glm::normalize(totalPenetration); - collision->_penetration = totalPenetration; - collided = true; - } else { - // collisions are full, so we might as well break - break; - } + if (ShapeCollider::shapeShape(&sphere, _shapes[i], collisions)) { + CollisionInfo* collision = collisions.getLastCollision(); + collision->_type = MODEL_COLLISION; + collision->_data = (void*)(this); + collision->_flags = i; + collided = true; } outerContinue: ; } @@ -504,6 +553,7 @@ bool Model::findSphereCollisions(const glm::vec3& penetratorCenter, float penetr } void Model::updateJointState(int index) { + _shapesAreDirty = true; JointState& state = _jointStates[index]; const FBXGeometry& geometry = _geometry->getFBXGeometry(); const FBXJoint& joint = geometry.joints.at(index); @@ -702,34 +752,51 @@ void Model::applyRotationDelta(int jointIndex, const glm::quat& delta, bool cons void Model::renderCollisionProxies(float alpha) { glPushMatrix(); Application::getInstance()->loadTranslatedViewMatrix(_translation); - - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - float uniformScale = extractUniformScale(_scale); - for (int i = 0; i < _jointStates.size(); i++) { + updateShapePositions(); + const int BALL_SUBDIVISIONS = 10; + for (int i = 0; i < _shapes.size(); i++) { glPushMatrix(); + + Shape* shape = _shapes[i]; - glm::vec3 position = extractTranslation(_jointStates[i].transform); - glTranslatef(position.x, position.y, position.z); - - glm::quat rotation; - getJointRotation(i, rotation); - glm::vec3 axis = glm::axis(rotation); - glRotatef(glm::angle(rotation), axis.x, axis.y, axis.z); - - glColor4f(0.75f, 0.75f, 0.75f, alpha); - float scaledRadius = geometry.joints[i].boneRadius * uniformScale; - const int BALL_SUBDIVISIONS = 10; - glutSolidSphere(scaledRadius, BALL_SUBDIVISIONS, BALL_SUBDIVISIONS); - - glPopMatrix(); - - int parentIndex = geometry.joints[i].parentIndex; - if (parentIndex != -1) { - Avatar::renderJointConnectingCone(extractTranslation(_jointStates[parentIndex].transform), position, - geometry.joints[parentIndex].boneRadius * uniformScale, scaledRadius); + if (shape->getType() == Shape::SPHERE_SHAPE) { + // shapes are stored in world-frame, so we have to transform into model frame + glm::vec3 position = shape->getPosition() - _translation; + glTranslatef(position.x, position.y, position.z); + const glm::quat& rotation = shape->getRotation(); + glm::vec3 axis = glm::axis(rotation); + glRotatef(glm::angle(rotation), axis.x, axis.y, axis.z); + + // draw a grey sphere at shape position + glColor4f(0.75f, 0.75f, 0.75f, alpha); + glutSolidSphere(shape->getBoundingRadius(), BALL_SUBDIVISIONS, BALL_SUBDIVISIONS); + } else if (shape->getType() == Shape::CAPSULE_SHAPE) { + CapsuleShape* capsule = static_cast(shape); + + // draw a blue sphere at the capsule endpoint + glm::vec3 endPoint; + capsule->getEndPoint(endPoint); + endPoint = endPoint - _translation; + glTranslatef(endPoint.x, endPoint.y, endPoint.z); + glColor4f(0.6f, 0.6f, 0.8f, alpha); + glutSolidSphere(capsule->getRadius(), BALL_SUBDIVISIONS, BALL_SUBDIVISIONS); + + // draw a yellow sphere at the capsule startpoint + glm::vec3 startPoint; + capsule->getStartPoint(startPoint); + startPoint = startPoint - _translation; + glm::vec3 axis = endPoint - startPoint; + glTranslatef(-axis.x, -axis.y, -axis.z); + glColor4f(0.8f, 0.8f, 0.6f, alpha); + glutSolidSphere(capsule->getRadius(), BALL_SUBDIVISIONS, BALL_SUBDIVISIONS); + + // draw a green cylinder between the two points + glm::vec3 origin(0.f); + glColor4f(0.6f, 0.8f, 0.6f, alpha); + Avatar::renderJointConnectingCone( origin, axis, capsule->getRadius(), capsule->getRadius()); } + glPopMatrix(); } - glPopMatrix(); } @@ -784,7 +851,7 @@ void Model::applyCollision(CollisionInfo& collision) { QVector Model::updateGeometry(bool delayLoad) { QVector newJointStates; if (_nextGeometry) { - _nextGeometry = _nextGeometry->getLODOrFallback(_lodDistance, _lodHysteresis, delayLoad); + _nextGeometry = _nextGeometry->getLODOrFallback(_lodDistance, _nextLODHysteresis, delayLoad); if (!delayLoad) { _nextGeometry->setLoadPriority(this, -_lodDistance); _nextGeometry->ensureLoading(); @@ -827,7 +894,7 @@ void Model::applyNextGeometry() { // delete our local geometry and custom textures deleteGeometry(); _dilatedTextures.clear(); - _lodHysteresis = NetworkGeometry::NO_HYSTERESIS; + _lodHysteresis = _nextLODHysteresis; // we retain a reference to the base geometry so that its reference count doesn't fall to zero _baseGeometry = _nextBaseGeometry; @@ -847,6 +914,7 @@ void Model::deleteGeometry() { _blendedVertexBufferIDs.clear(); _jointStates.clear(); _meshStates.clear(); + clearShapes(); if (_geometry) { _geometry->clearLoadPriority(this); diff --git a/interface/src/renderer/Model.h b/interface/src/renderer/Model.h index 524bd9dc79..55f8b27765 100644 --- a/interface/src/renderer/Model.h +++ b/interface/src/renderer/Model.h @@ -17,6 +17,8 @@ #include "ProgramObject.h" #include "TextureCache.h" +class Shape; + /// A generic 3D model displaying geometry loaded from a URL. class Model : public QObject { Q_OBJECT @@ -52,6 +54,9 @@ public: void init(); void reset(); + void clearShapes(); + void createCollisionShapes(); + void updateShapePositions(); void simulate(float deltaTime, bool delayLoad = false); bool render(float alpha); @@ -164,9 +169,14 @@ public: glm::vec4 computeAverageColor() const; bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance) const; + + /// \param shapes list of pointers shapes to test against Model + /// \param collisions list to store collision results + /// \return true if at least one shape collided agains Model + bool findCollisions(const QVector shapes, CollisionList& collisions); bool findSphereCollisions(const glm::vec3& penetratorCenter, float penetratorRadius, - CollisionList& collisions, float boneScale = 1.0f, int skipIndex = -1) const; + CollisionList& collisions, int skipIndex = -1); void renderCollisionProxies(float alpha); @@ -189,13 +199,15 @@ protected: class JointState { public: - glm::vec3 translation; - glm::quat rotation; - glm::mat4 transform; - glm::quat combinedRotation; + glm::vec3 translation; // translation relative to parent + glm::quat rotation; // rotation relative to parent + glm::mat4 transform; // rotation to world frame + translation in model frame + glm::quat combinedRotation; // rotation from joint local to world frame }; + bool _shapesAreDirty; QVector _jointStates; + QVector _shapes; class MeshState { public: @@ -247,6 +259,7 @@ private: QSharedPointer _nextGeometry; float _lodDistance; float _lodHysteresis; + float _nextLODHysteresis; float _pupilDilation; std::vector _blendshapeCoefficients; diff --git a/interface/src/renderer/TextureCache.cpp b/interface/src/renderer/TextureCache.cpp index d5d3b42155..1c58f93510 100644 --- a/interface/src/renderer/TextureCache.cpp +++ b/interface/src/renderer/TextureCache.cpp @@ -113,7 +113,8 @@ GLuint TextureCache::getFileTextureID(const QString& filename) { glBindTexture(GL_TEXTURE_2D, id); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width(), image.height(), 1, GL_BGRA, GL_UNSIGNED_BYTE, image.constBits()); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glGenerateMipmap(GL_TEXTURE_2D); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glBindTexture(GL_TEXTURE_2D, 0); _fileTextureIDs.insert(filename, id); @@ -335,7 +336,8 @@ void NetworkTexture::setImage(const QImage& image, const glm::vec4& averageColor glBindTexture(GL_TEXTURE_2D, getID()); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width(), image.height(), 1, GL_BGRA, GL_UNSIGNED_BYTE, image.constBits()); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glGenerateMipmap(GL_TEXTURE_2D); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glBindTexture(GL_TEXTURE_2D, 0); } @@ -388,7 +390,8 @@ QSharedPointer DilatableNetworkTexture::getDilatedTexture(float dilatio glBindTexture(GL_TEXTURE_2D, texture->getID()); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, dilatedImage.width(), dilatedImage.height(), 1, GL_BGRA, GL_UNSIGNED_BYTE, dilatedImage.constBits()); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glGenerateMipmap(GL_TEXTURE_2D); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glBindTexture(GL_TEXTURE_2D, 0); } diff --git a/libraries/avatars/src/HandData.h b/libraries/avatars/src/HandData.h index 5f7a49e0a2..cdab9f71e9 100755 --- a/libraries/avatars/src/HandData.h +++ b/libraries/avatars/src/HandData.h @@ -89,7 +89,7 @@ public: friend class AvatarData; protected: AvatarData* _owningAvatarData; - std::vector _palms; + std::vector _palms; glm::quat getBaseOrientation() const; glm::vec3 getBasePosition() const; diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index 4515deb6b5..3e7e5dd3c1 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -17,9 +17,7 @@ #include -#include -#include -#include +#include #include "CoverageMap.h" #include diff --git a/libraries/particles/src/Particle.cpp b/libraries/particles/src/Particle.cpp index d9f0beb81a..1a0ca7680b 100644 --- a/libraries/particles/src/Particle.cpp +++ b/libraries/particles/src/Particle.cpp @@ -884,25 +884,25 @@ void Particle::executeUpdateScripts() { } } -void Particle::collisionWithParticle(Particle* other) { +void Particle::collisionWithParticle(Particle* other, const glm::vec3& penetration) { // Only run this particle script if there's a script attached directly to the particle. if (!_script.isEmpty()) { ScriptEngine engine(_script); ParticleScriptObject particleScriptable(this); startParticleScriptContext(engine, particleScriptable); ParticleScriptObject otherParticleScriptable(other); - particleScriptable.emitCollisionWithParticle(&otherParticleScriptable); + particleScriptable.emitCollisionWithParticle(&otherParticleScriptable, penetration); endParticleScriptContext(engine, particleScriptable); } } -void Particle::collisionWithVoxel(VoxelDetail* voxelDetails) { +void Particle::collisionWithVoxel(VoxelDetail* voxelDetails, const glm::vec3& penetration) { // Only run this particle script if there's a script attached directly to the particle. if (!_script.isEmpty()) { ScriptEngine engine(_script); ParticleScriptObject particleScriptable(this); startParticleScriptContext(engine, particleScriptable); - particleScriptable.emitCollisionWithVoxel(*voxelDetails); + particleScriptable.emitCollisionWithVoxel(*voxelDetails, penetration); endParticleScriptContext(engine, particleScriptable); } } diff --git a/libraries/particles/src/Particle.h b/libraries/particles/src/Particle.h index 84efdd2916..dbe98c5bf6 100644 --- a/libraries/particles/src/Particle.h +++ b/libraries/particles/src/Particle.h @@ -295,8 +295,8 @@ public: void applyHardCollision(const CollisionInfo& collisionInfo); void update(const quint64& now); - void collisionWithParticle(Particle* other); - void collisionWithVoxel(VoxelDetail* voxel); + void collisionWithParticle(Particle* other, const glm::vec3& penetration); + void collisionWithVoxel(VoxelDetail* voxel, const glm::vec3& penetration); void debugDump() const; @@ -371,8 +371,10 @@ public: //~ParticleScriptObject() { qDebug() << "~ParticleScriptObject() this=" << this; } void emitUpdate() { emit update(); } - void emitCollisionWithParticle(QObject* other) { emit collisionWithParticle(other); } - void emitCollisionWithVoxel(const VoxelDetail& voxel) { emit collisionWithVoxel(voxel); } + void emitCollisionWithParticle(QObject* other, const glm::vec3& penetration) + { emit collisionWithParticle(other, penetration); } + void emitCollisionWithVoxel(const VoxelDetail& voxel, const glm::vec3& penetration) + { emit collisionWithVoxel(voxel, penetration); } public slots: unsigned int getID() const { return _particle->getID(); } @@ -417,8 +419,8 @@ public slots: signals: void update(); - void collisionWithVoxel(const VoxelDetail& voxel); - void collisionWithParticle(QObject* other); + void collisionWithVoxel(const VoxelDetail& voxel, const glm::vec3& penetration); + void collisionWithParticle(QObject* other, const glm::vec3& penetration); private: Particle* _particle; diff --git a/libraries/particles/src/ParticleCollisionSystem.cpp b/libraries/particles/src/ParticleCollisionSystem.cpp index b36b6a3a04..f7bf73a637 100644 --- a/libraries/particles/src/ParticleCollisionSystem.cpp +++ b/libraries/particles/src/ParticleCollisionSystem.cpp @@ -71,15 +71,18 @@ void ParticleCollisionSystem::checkParticle(Particle* particle) { updateCollisionWithAvatars(particle); } -void ParticleCollisionSystem::emitGlobalParticleCollisionWithVoxel(Particle* particle, VoxelDetail* voxelDetails) { +void ParticleCollisionSystem::emitGlobalParticleCollisionWithVoxel(Particle* particle, + VoxelDetail* voxelDetails, const glm::vec3& penetration) { ParticleID particleID = particle->getParticleID(); - emit particleCollisionWithVoxel(particleID, *voxelDetails); + emit particleCollisionWithVoxel(particleID, *voxelDetails, penetration); } -void ParticleCollisionSystem::emitGlobalParticleCollisionWithParticle(Particle* particleA, Particle* particleB) { +void ParticleCollisionSystem::emitGlobalParticleCollisionWithParticle(Particle* particleA, + Particle* particleB, const glm::vec3& penetration) { + ParticleID idA = particleA->getParticleID(); ParticleID idB = particleB->getParticleID(); - emit particleCollisionWithParticle(idA, idB); + emit particleCollisionWithParticle(idA, idB, penetration); } void ParticleCollisionSystem::updateCollisionWithVoxels(Particle* particle) { @@ -95,10 +98,10 @@ void ParticleCollisionSystem::updateCollisionWithVoxels(Particle* particle) { if (_voxels->findSpherePenetration(center, radius, collisionInfo._penetration, (void**)&voxelDetails)) { // let the particles run their collision scripts if they have them - particle->collisionWithVoxel(voxelDetails); + particle->collisionWithVoxel(voxelDetails, collisionInfo._penetration); // let the global script run their collision scripts for particles if they have them - emitGlobalParticleCollisionWithVoxel(particle, voxelDetails); + emitGlobalParticleCollisionWithVoxel(particle, voxelDetails, collisionInfo._penetration); updateCollisionSound(particle, collisionInfo._penetration, COLLISION_FREQUENCY); collisionInfo._penetration /= (float)(TREE_SCALE); @@ -125,9 +128,9 @@ void ParticleCollisionSystem::updateCollisionWithParticles(Particle* particleA) // we don't want to count this as a collision. glm::vec3 relativeVelocity = particleA->getVelocity() - particleB->getVelocity(); if (glm::dot(relativeVelocity, penetration) > 0.0f) { - particleA->collisionWithParticle(particleB); - particleB->collisionWithParticle(particleA); - emitGlobalParticleCollisionWithParticle(particleA, particleB); + particleA->collisionWithParticle(particleB, penetration); + particleB->collisionWithParticle(particleA, penetration * -1.0f); // the penetration is reversed + emitGlobalParticleCollisionWithParticle(particleA, particleB, penetration); glm::vec3 axis = glm::normalize(penetration); glm::vec3 axialVelocity = glm::dot(relativeVelocity, axis) * axis; diff --git a/libraries/particles/src/ParticleCollisionSystem.h b/libraries/particles/src/ParticleCollisionSystem.h index 3bff843743..1b30fd31ac 100644 --- a/libraries/particles/src/ParticleCollisionSystem.h +++ b/libraries/particles/src/ParticleCollisionSystem.h @@ -53,13 +53,13 @@ public: void updateCollisionSound(Particle* particle, const glm::vec3 &penetration, float frequency); signals: - void particleCollisionWithVoxel(const ParticleID& particleID, const VoxelDetail& voxel); - void particleCollisionWithParticle(const ParticleID& idA, const ParticleID& idB); + void particleCollisionWithVoxel(const ParticleID& particleID, const VoxelDetail& voxel, const glm::vec3& penetration); + void particleCollisionWithParticle(const ParticleID& idA, const ParticleID& idB, const glm::vec3& penetration); private: static bool updateOperation(OctreeElement* element, void* extraData); - void emitGlobalParticleCollisionWithVoxel(Particle* particle, VoxelDetail* voxelDetails); - void emitGlobalParticleCollisionWithParticle(Particle* particleA, Particle* particleB); + void emitGlobalParticleCollisionWithVoxel(Particle* particle, VoxelDetail* voxelDetails, const glm::vec3& penetration); + void emitGlobalParticleCollisionWithParticle(Particle* particleA, Particle* particleB, const glm::vec3& penetration); ParticleEditPacketSender* _packetSender; ParticleTree* _particles; diff --git a/libraries/particles/src/ParticlesScriptingInterface.h b/libraries/particles/src/ParticlesScriptingInterface.h index ee21424f11..af5f76a6af 100644 --- a/libraries/particles/src/ParticlesScriptingInterface.h +++ b/libraries/particles/src/ParticlesScriptingInterface.h @@ -29,12 +29,13 @@ public: private slots: /// inbound slots for external collision systems - void forwardParticleCollisionWithVoxel(const ParticleID& particleID, const VoxelDetail& voxel) { - emit particleCollisionWithVoxel(particleID, voxel); + void forwardParticleCollisionWithVoxel(const ParticleID& particleID, + const VoxelDetail& voxel, const glm::vec3& penetration) { + emit particleCollisionWithVoxel(particleID, voxel, penetration); } - void forwardParticleCollisionWithParticle(const ParticleID& idA, const ParticleID& idB) { - emit particleCollisionWithParticle(idA, idB); + void forwardParticleCollisionWithParticle(const ParticleID& idA, const ParticleID& idB, const glm::vec3& penetration) { + emit particleCollisionWithParticle(idA, idB, penetration); } public slots: @@ -65,8 +66,8 @@ public slots: QVector findParticles(const glm::vec3& center, float radius) const; signals: - void particleCollisionWithVoxel(const ParticleID& particleID, const VoxelDetail& voxel); - void particleCollisionWithParticle(const ParticleID& idA, const ParticleID& idB); + void particleCollisionWithVoxel(const ParticleID& particleID, const VoxelDetail& voxel, const glm::vec3& penetration); + void particleCollisionWithParticle(const ParticleID& idA, const ParticleID& idB, const glm::vec3& penetration); private: void queueParticleMessage(PacketType packetType, ParticleID particleID, const ParticleProperties& properties); diff --git a/libraries/script-engine/src/Quat.cpp b/libraries/script-engine/src/Quat.cpp index 7271f06af6..78ea73987b 100644 --- a/libraries/script-engine/src/Quat.cpp +++ b/libraries/script-engine/src/Quat.cpp @@ -11,6 +11,8 @@ #include +#include + #include #include #include "Quat.h" @@ -56,3 +58,7 @@ glm::quat Quat::mix(const glm::quat& q1, const glm::quat& q2, float alpha) { return safeMix(q1, q2, alpha); } +void Quat::print(const QString& lable, const glm::quat& q) { + qDebug() << qPrintable(lable) << q.x << "," << q.y << "," << q.z << "," << q.w; +} + diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index ac12aaa351..f006374347 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -13,7 +13,9 @@ #define __hifi__Quat__ #include -#include + +#include +#include /// Scriptable interface a Quaternion helper class object. Used exclusively in the JavaScript API class Quat : public QObject { @@ -30,6 +32,7 @@ public slots: glm::vec3 safeEulerAngles(const glm::quat& orientation); glm::quat angleAxis(float angle, const glm::vec3& v); glm::quat mix(const glm::quat& q1, const glm::quat& q2, float alpha); + void print(const QString& lable, const glm::quat& q); }; #endif /* defined(__hifi__Quat__) */ diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 718e5331dd..59ce841969 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -220,6 +220,8 @@ void ScriptEngine::run() { int thisFrame = 0; NodeList* nodeList = NodeList::getInstance(); + + qint64 lastUpdate = usecTimestampNow(); while (!_isFinished) { int usecToSleep = usecTimestamp(&startTime) + (thisFrame++ * VISUAL_DATA_CALLBACK_USECS) - usecTimestampNow(); @@ -264,7 +266,10 @@ void ScriptEngine::run() { nodeList->broadcastToNodes(avatarPacket, NodeSet() << NodeType::AvatarMixer); } - emit willSendVisualDataCallback(); + qint64 now = usecTimestampNow(); + float deltaTime = (float)(now - lastUpdate)/(float)USECS_PER_SECOND; + emit update(deltaTime); + lastUpdate = now; if (_engine.hasUncaughtException()) { int line = _engine.uncaughtExceptionLineNumber(); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 00906de7b3..6b9ec9c9eb 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -71,8 +71,7 @@ public slots: void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast(timer)); } signals: - void willSendAudioDataCallback(); - void willSendVisualDataCallback(); + void update(float deltaTime); void scriptEnding(); void finished(const QString& fileNameString); void cleanupMenuItem(const QString& menuItemString); diff --git a/libraries/script-engine/src/Vec3.cpp b/libraries/script-engine/src/Vec3.cpp index 2c5bf172fd..dc5dcd9773 100644 --- a/libraries/script-engine/src/Vec3.cpp +++ b/libraries/script-engine/src/Vec3.cpp @@ -9,10 +9,12 @@ // // +#include + #include "Vec3.h" -glm::vec3 Vec3::multiply(const glm::vec3& v1, const glm::vec3& v2) { - return v1 * v2; +glm::vec3 Vec3::cross(const glm::vec3& v1, const glm::vec3& v2) { + return glm::cross(v1,v2); } glm::vec3 Vec3::multiply(const glm::vec3& v1, float f) { @@ -29,6 +31,15 @@ glm::vec3 Vec3::sum(const glm::vec3& v1, const glm::vec3& v2) { glm::vec3 Vec3::subtract(const glm::vec3& v1, const glm::vec3& v2) { return v1 - v2; } + float Vec3::length(const glm::vec3& v) { return glm::length(v); } + +glm::vec3 Vec3::normalize(const glm::vec3& v) { + return glm::normalize(v); +} + +void Vec3::print(const QString& lable, const glm::vec3& v) { + qDebug() << qPrintable(lable) << v.x << "," << v.y << "," << v.z; +} diff --git a/libraries/script-engine/src/Vec3.h b/libraries/script-engine/src/Vec3.h index 20ad3f7eaa..cbec55b992 100644 --- a/libraries/script-engine/src/Vec3.h +++ b/libraries/script-engine/src/Vec3.h @@ -14,19 +14,23 @@ #include #include -#include + +#include +#include /// Scriptable interface a Vec3ernion helper class object. Used exclusively in the JavaScript API class Vec3 : public QObject { Q_OBJECT public slots: - glm::vec3 multiply(const glm::vec3& v1, const glm::vec3& v2); + glm::vec3 cross(const glm::vec3& v1, const glm::vec3& v2); glm::vec3 multiply(const glm::vec3& v1, float f); glm::vec3 multiplyQbyV(const glm::quat& q, const glm::vec3& v); glm::vec3 sum(const glm::vec3& v1, const glm::vec3& v2); glm::vec3 subtract(const glm::vec3& v1, const glm::vec3& v2); float length(const glm::vec3& v); + glm::vec3 normalize(const glm::vec3& v); + void print(const QString& lable, const glm::vec3& v); }; diff --git a/libraries/shared/src/CapsuleShape.cpp b/libraries/shared/src/CapsuleShape.cpp new file mode 100644 index 0000000000..bae5f201ca --- /dev/null +++ b/libraries/shared/src/CapsuleShape.cpp @@ -0,0 +1,78 @@ +// +// CapsuleShape.cpp +// hifi +// +// Created by Andrew Meadows on 2014.02.20 +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#include +#include + +#include "CapsuleShape.h" +#include "SharedUtil.h" + + +// default axis of CapsuleShape is Y-axis +const glm::vec3 localAxis(0.f, 1.f, 0.f); + +CapsuleShape::CapsuleShape() : Shape(Shape::CAPSULE_SHAPE) {} + +CapsuleShape::CapsuleShape(float radius, float halfHeight) : Shape(Shape::CAPSULE_SHAPE), + _radius(radius), _halfHeight(halfHeight) { + updateBoundingRadius(); +} + +CapsuleShape::CapsuleShape(float radius, float halfHeight, const glm::vec3& position, const glm::quat& rotation) : + Shape(Shape::CAPSULE_SHAPE, position, rotation), _radius(radius), _halfHeight(halfHeight) { + updateBoundingRadius(); +} + +CapsuleShape::CapsuleShape(float radius, const glm::vec3& startPoint, const glm::vec3& endPoint) : + Shape(Shape::CAPSULE_SHAPE), _radius(radius), _halfHeight(0.f) { + glm::vec3 axis = endPoint - startPoint; + float height = glm::length(axis); + if (height > EPSILON) { + _halfHeight = 0.5f * height; + axis /= height; + glm::vec3 yAxis(0.f, 1.f, 0.f); + float angle = glm::angle(axis, yAxis); + if (angle > EPSILON) { + axis = glm::normalize(glm::cross(yAxis, axis)); + _rotation = glm::angleAxis(angle, axis); + } + } + updateBoundingRadius(); +} + +/// \param[out] startPoint is the center of start cap +void CapsuleShape::getStartPoint(glm::vec3& startPoint) const { + startPoint = getPosition() - _rotation * glm::vec3(0.f, _halfHeight, 0.f); +} + +/// \param[out] endPoint is the center of the end cap +void CapsuleShape::getEndPoint(glm::vec3& endPoint) const { + endPoint = getPosition() + _rotation * glm::vec3(0.f, _halfHeight, 0.f); +} + +void CapsuleShape::computeNormalizedAxis(glm::vec3& axis) const { + // default axis of a capsule is along the yAxis + axis = _rotation * glm::vec3(0.f, 1.f, 0.f); +} + +void CapsuleShape::setRadius(float radius) { + _radius = radius; + updateBoundingRadius(); +} + +void CapsuleShape::setHalfHeight(float halfHeight) { + _halfHeight = halfHeight; + updateBoundingRadius(); +} + +void CapsuleShape::setRadiusAndHalfHeight(float radius, float halfHeight) { + _radius = radius; + _halfHeight = halfHeight; + updateBoundingRadius(); +} + diff --git a/libraries/shared/src/CapsuleShape.h b/libraries/shared/src/CapsuleShape.h new file mode 100644 index 0000000000..6d7e0a50be --- /dev/null +++ b/libraries/shared/src/CapsuleShape.h @@ -0,0 +1,46 @@ +// +// CapsuleShape.h +// hifi +// +// Created by Andrew Meadows on 2014.02.20 +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __hifi__CapsuleShape__ +#define __hifi__CapsuleShape__ + +#include "Shape.h" + +// adebug bookmark TODO: convert to new world-frame approach +// default axis of CapsuleShape is Y-axis + +class CapsuleShape : public Shape { +public: + CapsuleShape(); + CapsuleShape(float radius, float halfHeight); + CapsuleShape(float radius, float halfHeight, const glm::vec3& position, const glm::quat& rotation); + CapsuleShape(float radius, const glm::vec3& startPoint, const glm::vec3& endPoint); + + float getRadius() const { return _radius; } + float getHalfHeight() const { return _halfHeight; } + + /// \param[out] startPoint is the center of start cap + void getStartPoint(glm::vec3& startPoint) const; + + /// \param[out] endPoint is the center of the end cap + void getEndPoint(glm::vec3& endPoint) const; + + void computeNormalizedAxis(glm::vec3& axis) const; + + void setRadius(float radius); + void setHalfHeight(float height); + void setRadiusAndHalfHeight(float radius, float height); + +protected: + void updateBoundingRadius() { _boundingRadius = _radius + _halfHeight; } + + float _radius; + float _halfHeight; +}; + +#endif /* defined(__hifi__CapsuleShape__) */ diff --git a/libraries/shared/src/CollisionInfo.cpp b/libraries/shared/src/CollisionInfo.cpp index 5d74d591c6..f6c0d057a1 100644 --- a/libraries/shared/src/CollisionInfo.cpp +++ b/libraries/shared/src/CollisionInfo.cpp @@ -8,6 +8,7 @@ #include "CollisionInfo.h" + CollisionList::CollisionList(int maxSize) : _maxSize(maxSize), _size(0) { @@ -16,13 +17,17 @@ CollisionList::CollisionList(int maxSize) : CollisionInfo* CollisionList::getNewCollision() { // return pointer to existing CollisionInfo, or NULL of list is full - return (_size < _maxSize) ? &(_collisions[++_size]) : NULL; + return (_size < _maxSize) ? &(_collisions[_size++]) : NULL; } CollisionInfo* CollisionList::getCollision(int index) { return (index > -1 && index < _size) ? &(_collisions[index]) : NULL; } +CollisionInfo* CollisionList::getLastCollision() { + return (_size > 0) ? &(_collisions[_size - 1]) : NULL; +} + void CollisionList::clear() { for (int i = 0; i < _size; ++i) { // we only clear the important stuff diff --git a/libraries/shared/src/CollisionInfo.h b/libraries/shared/src/CollisionInfo.h index acd127435c..868d259ce3 100644 --- a/libraries/shared/src/CollisionInfo.h +++ b/libraries/shared/src/CollisionInfo.h @@ -10,6 +10,7 @@ #define __hifi__CollisionInfo__ #include +#include #include @@ -79,6 +80,12 @@ public: /// \return pointer to collision by index. NULL if index out of bounds. CollisionInfo* getCollision(int index); + /// \return pointer to last collision on the list. NULL if list is empty + CollisionInfo* getLastCollision(); + + /// \return true if list is full + bool isFull() const { return _size == _maxSize; } + /// \return number of valid collisions int size() const { return _size; } @@ -86,8 +93,8 @@ public: void clear(); private: - int _maxSize; - int _size; + int _maxSize; // the container cannot get larger than this + int _size; // the current number of valid collisions in the list QVector _collisions; }; diff --git a/libraries/shared/src/ListShape.cpp b/libraries/shared/src/ListShape.cpp new file mode 100644 index 0000000000..593304c75a --- /dev/null +++ b/libraries/shared/src/ListShape.cpp @@ -0,0 +1,88 @@ +// +// ListShape.cpp +// +// ListShape: A collection of shapes, each with a local transform. +// +// Created by Andrew Meadows on 2014.02.20 +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#include "ListShape.h" + +// ListShapeEntry + +void ListShapeEntry::updateTransform(const glm::vec3& rootPosition, const glm::quat& rootRotation) { + _shape->setPosition(rootPosition + rootRotation * _localPosition); + _shape->setRotation(_localRotation * rootRotation); +} + +// ListShape + +ListShape::~ListShape() { + clear(); +} + +void ListShape::setPosition(const glm::vec3& position) { + _subShapeTransformsAreDirty = true; + Shape::setPosition(position); +} + +void ListShape::setRotation(const glm::quat& rotation) { + _subShapeTransformsAreDirty = true; + Shape::setRotation(rotation); +} + +const Shape* ListShape::getSubShape(int index) const { + if (index < 0 || index > _subShapeEntries.size()) { + return NULL; + } + return _subShapeEntries[index]._shape; +} + +void ListShape::updateSubTransforms() { + if (_subShapeTransformsAreDirty) { + for (int i = 0; i < _subShapeEntries.size(); ++i) { + _subShapeEntries[i].updateTransform(_position, _rotation); + } + _subShapeTransformsAreDirty = false; + } +} + +void ListShape::addShape(Shape* shape, const glm::vec3& localPosition, const glm::quat& localRotation) { + if (shape) { + ListShapeEntry entry; + entry._shape = shape; + entry._localPosition = localPosition; + entry._localRotation = localRotation; + _subShapeEntries.push_back(entry); + } +} + +void ListShape::setShapes(QVector& shapes) { + clear(); + _subShapeEntries.swap(shapes); + // TODO: audit our new list of entries and delete any that have null pointers + computeBoundingRadius(); +} + +void ListShape::clear() { + // the ListShape owns its subShapes, so they need to be deleted + for (int i = 0; i < _subShapeEntries.size(); ++i) { + delete _subShapeEntries[i]._shape; + } + _subShapeEntries.clear(); + setBoundingRadius(0.f); +} + +void ListShape::computeBoundingRadius() { + float maxRadius = 0.f; + for (int i = 0; i < _subShapeEntries.size(); ++i) { + ListShapeEntry& entry = _subShapeEntries[i]; + float radius = glm::length(entry._localPosition) + entry._shape->getBoundingRadius(); + if (radius > maxRadius) { + maxRadius = radius; + } + } + setBoundingRadius(maxRadius); +} + diff --git a/libraries/shared/src/ListShape.h b/libraries/shared/src/ListShape.h new file mode 100644 index 0000000000..d6005ddfb9 --- /dev/null +++ b/libraries/shared/src/ListShape.h @@ -0,0 +1,65 @@ +// +// ListShape.h +// +// ListShape: A collection of shapes, each with a local transform. +// +// Created by Andrew Meadows on 2014.02.20 +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __hifi__ListShape__ +#define __hifi__ListShape__ + +#include + +#include +#include +#include + +#include "Shape.h" + + +class ListShapeEntry { +public: + void updateTransform(const glm::vec3& position, const glm::quat& rotation); + + Shape* _shape; + glm::vec3 _localPosition; + glm::quat _localRotation; +}; + +class ListShape : public Shape { +public: + + ListShape() : Shape(LIST_SHAPE), _subShapeEntries(), _subShapeTransformsAreDirty(false) {} + + ListShape(const glm::vec3& position, const glm::quat& rotation) : + Shape(LIST_SHAPE, position, rotation), _subShapeEntries(), _subShapeTransformsAreDirty(false) {} + + ~ListShape(); + + void setPosition(const glm::vec3& position); + void setRotation(const glm::quat& rotation); + + const Shape* getSubShape(int index) const; + + void updateSubTransforms(); + + int size() const { return _subShapeEntries.size(); } + + void addShape(Shape* shape, const glm::vec3& localPosition, const glm::quat& localRotation); + + void setShapes(QVector& shapes); + +protected: + void clear(); + void computeBoundingRadius(); + + QVector _subShapeEntries; + bool _subShapeTransformsAreDirty; + +private: + ListShape(const ListShape& otherList); // don't implement this +}; + +#endif // __hifi__ListShape__ diff --git a/libraries/shared/src/Shape.h b/libraries/shared/src/Shape.h new file mode 100644 index 0000000000..924d13000e --- /dev/null +++ b/libraries/shared/src/Shape.h @@ -0,0 +1,54 @@ +// +// Shape.h +// +// Created by Andrew Meadows on 2014.02.20 +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __hifi__Shape__ +#define __hifi__Shape__ + +#include +#include + + +class Shape { +public: + enum Type{ + UNKNOWN_SHAPE = 0, + SPHERE_SHAPE, + CAPSULE_SHAPE, + BOX_SHAPE, + LIST_SHAPE + }; + + Shape() : _type(UNKNOWN_SHAPE), _boundingRadius(0.f), _position(0.f), _rotation() { } + virtual ~Shape() {} + + int getType() const { return _type; } + float getBoundingRadius() const { return _boundingRadius; } + const glm::vec3& getPosition() const { return _position; } + const glm::quat& getRotation() const { return _rotation; } + + virtual void setPosition(const glm::vec3& position) { _position = position; } + virtual void setRotation(const glm::quat& rotation) { _rotation = rotation; } + +protected: + // these ctors are protected (used by derived classes only) + Shape(Type type) : _type(type), _boundingRadius(0.f), _position(0.f), _rotation() {} + + Shape(Type type, const glm::vec3& position) + : _type(type), _boundingRadius(0.f), _position(position), _rotation() {} + + Shape(Type type, const glm::vec3& position, const glm::quat& rotation) + : _type(type), _boundingRadius(0.f), _position(position), _rotation(rotation) {} + + void setBoundingRadius(float radius) { _boundingRadius = radius; } + + int _type; + float _boundingRadius; + glm::vec3 _position; + glm::quat _rotation; +}; + +#endif /* defined(__hifi__Shape__) */ diff --git a/libraries/shared/src/ShapeCollider.cpp b/libraries/shared/src/ShapeCollider.cpp new file mode 100644 index 0000000000..7f36d948e8 --- /dev/null +++ b/libraries/shared/src/ShapeCollider.cpp @@ -0,0 +1,408 @@ +// +// ShapeCollider.cpp +// hifi +// +// Created by Andrew Meadows on 2014.02.20 +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#include + +#include + +#include "ShapeCollider.h" + +// NOTE: +// +// * Large ListShape's are inefficient keep the lists short. +// * Collisions between lists of lists work in theory but are not recommended. + +namespace ShapeCollider { + +bool shapeShape(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { + // ATM we only have two shape types so we just check every case. + // TODO: make a fast lookup for correct method + int typeA = shapeA->getType(); + int typeB = shapeB->getType(); + if (typeA == Shape::SPHERE_SHAPE) { + const SphereShape* sphereA = static_cast(shapeA); + if (typeB == Shape::SPHERE_SHAPE) { + return sphereSphere(sphereA, static_cast(shapeB), collisions); + } else if (typeB == Shape::CAPSULE_SHAPE) { + return sphereCapsule(sphereA, static_cast(shapeB), collisions); + } + } else if (typeA == Shape::CAPSULE_SHAPE) { + const CapsuleShape* capsuleA = static_cast(shapeA); + if (typeB == Shape::SPHERE_SHAPE) { + return capsuleSphere(capsuleA, static_cast(shapeB), collisions); + } else if (typeB == Shape::CAPSULE_SHAPE) { + return capsuleCapsule(capsuleA, static_cast(shapeB), collisions); + } + } else if (typeA == Shape::LIST_SHAPE) { + const ListShape* listA = static_cast(shapeA); + if (typeB == Shape::SPHERE_SHAPE) { + return listSphere(listA, static_cast(shapeB), collisions); + } else if (typeB == Shape::CAPSULE_SHAPE) { + return listCapsule(listA, static_cast(shapeB), collisions); + } + } + return false; +} + +bool sphereSphere(const SphereShape* sphereA, const SphereShape* sphereB, CollisionList& collisions) { + glm::vec3 BA = sphereB->getPosition() - sphereA->getPosition(); + float distanceSquared = glm::dot(BA, BA); + float totalRadius = sphereA->getRadius() + sphereB->getRadius(); + if (distanceSquared < totalRadius * totalRadius) { + // normalize BA + float distance = sqrtf(distanceSquared); + if (distance < EPSILON) { + // the spheres are on top of each other, so we pick an arbitrary penetration direction + BA = glm::vec3(0.f, 1.f, 0.f); + distance = totalRadius; + } else { + BA /= distance; + } + // penetration points from A into B + CollisionInfo* collision = collisions.getNewCollision(); + if (collision) { + collision->_penetration = BA * (totalRadius - distance); + // contactPoint is on surface of A + collision->_contactPoint = sphereA->getPosition() + sphereA->getRadius() * BA; + return true; + } + } + return false; +} + +bool sphereCapsule(const SphereShape* sphereA, const CapsuleShape* capsuleB, CollisionList& collisions) { + // find sphereA's closest approach to axis of capsuleB + glm::vec3 BA = capsuleB->getPosition() - sphereA->getPosition(); + glm::vec3 capsuleAxis; + capsuleB->computeNormalizedAxis(capsuleAxis); + float axialDistance = - glm::dot(BA, capsuleAxis); + float absAxialDistance = fabs(axialDistance); + float totalRadius = sphereA->getRadius() + capsuleB->getRadius(); + if (absAxialDistance < totalRadius + capsuleB->getHalfHeight()) { + glm::vec3 radialAxis = BA + axialDistance * capsuleAxis; // points from A to axis of B + float radialDistance2 = glm::length2(radialAxis); + if (radialDistance2 > totalRadius * totalRadius) { + // sphere is too far from capsule axis + return false; + } + if (absAxialDistance > capsuleB->getHalfHeight()) { + // sphere hits capsule on a cap --> recompute radialAxis to point from spherA to cap center + float sign = (axialDistance > 0.f) ? 1.f : -1.f; + radialAxis = BA + (sign * capsuleB->getHalfHeight()) * capsuleAxis; + radialDistance2 = glm::length2(radialAxis); + } + if (radialDistance2 > EPSILON * EPSILON) { + CollisionInfo* collision = collisions.getNewCollision(); + if (!collision) { + // collisions list is full + return false; + } + // normalize the radialAxis + float radialDistance = sqrtf(radialDistance2); + radialAxis /= radialDistance; + // penetration points from A into B + collision->_penetration = (totalRadius - radialDistance) * radialAxis; // points from A into B + // contactPoint is on surface of sphereA + collision->_contactPoint = sphereA->getPosition() + sphereA->getRadius() * radialAxis; + } else { + // A is on B's axis, so the penetration is undefined... + if (absAxialDistance > capsuleB->getHalfHeight()) { + // ...for the cylinder case (for now we pretend the collision doesn't exist) + return false; + } + CollisionInfo* collision = collisions.getNewCollision(); + if (!collision) { + // collisions list is full + return false; + } + // ... but still defined for the cap case + if (axialDistance < 0.f) { + // we're hitting the start cap, so we negate the capsuleAxis + capsuleAxis *= -1; + } + // penetration points from A into B + float sign = (axialDistance > 0.f) ? -1.f : 1.f; + collision->_penetration = (sign * (totalRadius + capsuleB->getHalfHeight() - absAxialDistance)) * capsuleAxis; + // contactPoint is on surface of sphereA + collision->_contactPoint = sphereA->getPosition() + (sign * sphereA->getRadius()) * capsuleAxis; + } + return true; + } + return false; +} + +bool capsuleSphere(const CapsuleShape* capsuleA, const SphereShape* sphereB, CollisionList& collisions) { + // find sphereB's closest approach to axis of capsuleA + glm::vec3 AB = capsuleA->getPosition() - sphereB->getPosition(); + glm::vec3 capsuleAxis; + capsuleA->computeNormalizedAxis(capsuleAxis); + float axialDistance = - glm::dot(AB, capsuleAxis); + float absAxialDistance = fabs(axialDistance); + float totalRadius = sphereB->getRadius() + capsuleA->getRadius(); + if (absAxialDistance < totalRadius + capsuleA->getHalfHeight()) { + glm::vec3 radialAxis = AB + axialDistance * capsuleAxis; // from sphereB to axis of capsuleA + float radialDistance2 = glm::length2(radialAxis); + if (radialDistance2 > totalRadius * totalRadius) { + // sphere is too far from capsule axis + return false; + } + + // closestApproach = point on capsuleA's axis that is closest to sphereB's center + glm::vec3 closestApproach = capsuleA->getPosition() + axialDistance * capsuleAxis; + + if (absAxialDistance > capsuleA->getHalfHeight()) { + // sphere hits capsule on a cap + // --> recompute radialAxis and closestApproach + float sign = (axialDistance > 0.f) ? 1.f : -1.f; + closestApproach = capsuleA->getPosition() + (sign * capsuleA->getHalfHeight()) * capsuleAxis; + radialAxis = closestApproach - sphereB->getPosition(); + radialDistance2 = glm::length2(radialAxis); + } + if (radialDistance2 > EPSILON * EPSILON) { + CollisionInfo* collision = collisions.getNewCollision(); + if (!collision) { + // collisions list is full + return false; + } + // normalize the radialAxis + float radialDistance = sqrtf(radialDistance2); + radialAxis /= radialDistance; + // penetration points from A into B + collision->_penetration = (radialDistance - totalRadius) * radialAxis; // points from A into B + // contactPoint is on surface of capsuleA + collision->_contactPoint = closestApproach - capsuleA->getRadius() * radialAxis; + } else { + // A is on B's axis, so the penetration is undefined... + if (absAxialDistance > capsuleA->getHalfHeight()) { + // ...for the cylinder case (for now we pretend the collision doesn't exist) + return false; + } else { + CollisionInfo* collision = collisions.getNewCollision(); + if (!collision) { + // collisions list is full + return false; + } + // ... but still defined for the cap case + if (axialDistance < 0.f) { + // we're hitting the start cap, so we negate the capsuleAxis + capsuleAxis *= -1; + } + float sign = (axialDistance > 0.f) ? 1.f : -1.f; + collision->_penetration = (sign * (totalRadius + capsuleA->getHalfHeight() - absAxialDistance)) * capsuleAxis; + // contactPoint is on surface of sphereA + collision->_contactPoint = closestApproach + (sign * capsuleA->getRadius()) * capsuleAxis; + } + } + return true; + } + return false; +} + +bool capsuleCapsule(const CapsuleShape* capsuleA, const CapsuleShape* capsuleB, CollisionList& collisions) { + glm::vec3 axisA; + capsuleA->computeNormalizedAxis(axisA); + glm::vec3 axisB; + capsuleB->computeNormalizedAxis(axisB); + glm::vec3 centerA = capsuleA->getPosition(); + glm::vec3 centerB = capsuleB->getPosition(); + + // NOTE: The formula for closest approach between two lines is: + // d = [(B - A) . (a - (a.b)b)] / (1 - (a.b)^2) + + float aDotB = glm::dot(axisA, axisB); + float denominator = 1.f - aDotB * aDotB; + float totalRadius = capsuleA->getRadius() + capsuleB->getRadius(); + if (denominator > EPSILON) { + // distances to points of closest approach + float distanceA = glm::dot((centerB - centerA), (axisA - (aDotB) * axisB)) / denominator; + float distanceB = glm::dot((centerA - centerB), (axisB - (aDotB) * axisA)) / denominator; + + // clamp the distances to the ends of the capsule line segments + float absDistanceA = fabs(distanceA); + if (absDistanceA > capsuleA->getHalfHeight() + capsuleA->getRadius()) { + float signA = distanceA < 0.f ? -1.f : 1.f; + distanceA = signA * capsuleA->getHalfHeight(); + } + float absDistanceB = fabs(distanceB); + if (absDistanceB > capsuleB->getHalfHeight() + capsuleB->getRadius()) { + float signB = distanceB < 0.f ? -1.f : 1.f; + distanceB = signB * capsuleB->getHalfHeight(); + } + + // collide like spheres at closest approaches (do most of the math relative to B) + glm::vec3 BA = (centerB + distanceB * axisB) - (centerA + distanceA * axisA); + float distanceSquared = glm::dot(BA, BA); + if (distanceSquared < totalRadius * totalRadius) { + CollisionInfo* collision = collisions.getNewCollision(); + if (!collision) { + // collisions list is full + return false; + } + // normalize BA + float distance = sqrtf(distanceSquared); + if (distance < EPSILON) { + // the contact spheres are on top of each other, so we need to pick a penetration direction... + // try vector between the capsule centers... + BA = centerB - centerA; + distanceSquared = glm::length2(BA); + if (distanceSquared > EPSILON * EPSILON) { + distance = sqrtf(distanceSquared); + BA /= distance; + } else + { + // the capsule centers are on top of each other! + // give up on a valid penetration direction and just use the yAxis + BA = glm::vec3(0.f, 1.f, 0.f); + distance = glm::max(capsuleB->getRadius(), capsuleA->getRadius()); + } + } else { + BA /= distance; + } + // penetration points from A into B + collision->_penetration = BA * (totalRadius - distance); + // contactPoint is on surface of A + collision->_contactPoint = centerA + distanceA * axisA + capsuleA->getRadius() * BA; + return true; + } + } else { + // capsules are approximiately parallel but might still collide + glm::vec3 BA = centerB - centerA; + float axialDistance = glm::dot(BA, axisB); + if (axialDistance > totalRadius + capsuleA->getHalfHeight() + capsuleB->getHalfHeight()) { + return false; + } + BA = BA - axialDistance * axisB; // BA now points from centerA to axisB (perp to axis) + float distanceSquared = glm::length2(BA); + if (distanceSquared < totalRadius * totalRadius) { + CollisionInfo* collision = collisions.getNewCollision(); + if (!collision) { + // collisions list is full + return false; + } + // We have all the info we need to compute the penetration vector... + // normalize BA + float distance = sqrtf(distanceSquared); + if (distance < EPSILON) { + // the spheres are on top of each other, so we pick an arbitrary penetration direction + BA = glm::vec3(0.f, 1.f, 0.f); + } else { + BA /= distance; + } + // penetration points from A into B + collision->_penetration = BA * (totalRadius - distance); + + // However we need some more world-frame info to compute the contactPoint, + // which is on the surface of capsuleA... + // + // Find the overlapping secion of the capsules --> they collide as if there were + // two spheres at the midpoint of this overlapping section. + // So we project all endpoints to axisB, find the interior pair, + // and put A's proxy sphere on axisA at the midpoint of this section. + + // sort the projections as much as possible during calculation + float points[5]; + points[0] = -capsuleB->getHalfHeight(); + points[1] = axialDistance - capsuleA->getHalfHeight(); + points[2] = axialDistance + capsuleA->getHalfHeight(); + points[3] = capsuleB->getHalfHeight(); + + // Since there are only three comparisons to do we unroll the sort algorithm... + // and use a fifth slot as temp during swap. + if (points[4] > points[2]) { + points[4] = points[1]; + points[1] = points[2]; + points[2] = points[4]; + } + if (points[2] > points[3]) { + points[4] = points[2]; + points[2] = points[3]; + points[3] = points[4]; + } + if (points[0] > points[1]) { + points[4] = points[0]; + points[0] = points[1]; + points[1] = points[4]; + } + + // average the internal pair, and then do the math from centerB + collision->_contactPoint = centerB + (0.5f * (points[1] + points[2])) * axisB + + (capsuleA->getRadius() - distance) * BA; + return true; + } + } + return false; +} + +bool sphereList(const SphereShape* sphereA, const ListShape* listB, CollisionList& collisions) { + bool touching = false; + for (int i = 0; i < listB->size() && !collisions.isFull(); ++i) { + const Shape* subShape = listB->getSubShape(i); + int subType = subShape->getType(); + if (subType == Shape::SPHERE_SHAPE) { + touching = sphereSphere(sphereA, static_cast(subShape), collisions) || touching; + } else if (subType == Shape::CAPSULE_SHAPE) { + touching = sphereCapsule(sphereA, static_cast(subShape), collisions) || touching; + } + } + return touching; +} + +bool capsuleList(const CapsuleShape* capsuleA, const ListShape* listB, CollisionList& collisions) { + bool touching = false; + for (int i = 0; i < listB->size() && !collisions.isFull(); ++i) { + const Shape* subShape = listB->getSubShape(i); + int subType = subShape->getType(); + if (subType == Shape::SPHERE_SHAPE) { + touching = capsuleSphere(capsuleA, static_cast(subShape), collisions) || touching; + } else if (subType == Shape::CAPSULE_SHAPE) { + touching = capsuleCapsule(capsuleA, static_cast(subShape), collisions) || touching; + } + } + return touching; +} + +bool listSphere(const ListShape* listA, const SphereShape* sphereB, CollisionList& collisions) { + bool touching = false; + for (int i = 0; i < listA->size() && !collisions.isFull(); ++i) { + const Shape* subShape = listA->getSubShape(i); + int subType = subShape->getType(); + if (subType == Shape::SPHERE_SHAPE) { + touching = sphereSphere(static_cast(subShape), sphereB, collisions) || touching; + } else if (subType == Shape::CAPSULE_SHAPE) { + touching = capsuleSphere(static_cast(subShape), sphereB, collisions) || touching; + } + } + return touching; +} + +bool listCapsule(const ListShape* listA, const CapsuleShape* capsuleB, CollisionList& collisions) { + bool touching = false; + for (int i = 0; i < listA->size() && !collisions.isFull(); ++i) { + const Shape* subShape = listA->getSubShape(i); + int subType = subShape->getType(); + if (subType == Shape::SPHERE_SHAPE) { + touching = sphereCapsule(static_cast(subShape), capsuleB, collisions) || touching; + } else if (subType == Shape::CAPSULE_SHAPE) { + touching = capsuleCapsule(static_cast(subShape), capsuleB, collisions) || touching; + } + } + return touching; +} + +bool listList(const ListShape* listA, const ListShape* listB, CollisionList& collisions) { + bool touching = false; + for (int i = 0; i < listA->size() && !collisions.isFull(); ++i) { + const Shape* subShape = listA->getSubShape(i); + for (int j = 0; j < listB->size() && !collisions.isFull(); ++j) { + touching = shapeShape(subShape, listB->getSubShape(j), collisions) || touching; + } + } + return touching; +} + +} // namespace ShapeCollider diff --git a/libraries/shared/src/ShapeCollider.h b/libraries/shared/src/ShapeCollider.h new file mode 100644 index 0000000000..e3e044c8fe --- /dev/null +++ b/libraries/shared/src/ShapeCollider.h @@ -0,0 +1,82 @@ +// +// ShapeCollider.h +// hifi +// +// Created by Andrew Meadows on 2014.02.20 +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __hifi__ShapeCollider__ +#define __hifi__ShapeCollider__ + +#include "CapsuleShape.h" +#include "CollisionInfo.h" +#include "ListShape.h" +#include "SharedUtil.h" +#include "SphereShape.h" + +namespace ShapeCollider { + + /// \param shapeA pointer to first shape + /// \param shapeB pointer to second shape + /// \param[out] collisions where to append collision details + /// \return true if shapes collide + bool shapeShape(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions); + + /// \param sphereA pointer to first shape + /// \param sphereB pointer to second shape + /// \param[out] collisions where to append collision details + /// \return true if shapes collide + bool sphereSphere(const SphereShape* sphereA, const SphereShape* sphereB, CollisionList& collisions); + + /// \param sphereA pointer to first shape + /// \param capsuleB pointer to second shape + /// \param[out] collisions where to append collision details + /// \return true if shapes collide + bool sphereCapsule(const SphereShape* sphereA, const CapsuleShape* capsuleB, CollisionList& collisions); + + /// \param capsuleA pointer to first shape + /// \param sphereB pointer to second shape + /// \param[out] collisions where to append collision details + /// \return true if shapes collide + bool capsuleSphere(const CapsuleShape* capsuleA, const SphereShape* sphereB, CollisionList& collisions); + + /// \param capsuleA pointer to first shape + /// \param capsuleB pointer to second shape + /// \param[out] collisions where to append collision details + /// \return true if shapes collide + bool capsuleCapsule(const CapsuleShape* capsuleA, const CapsuleShape* capsuleB, CollisionList& collisions); + + /// \param sphereA pointer to first shape + /// \param listB pointer to second shape + /// \param[out] collisions where to append collision details + /// \return true if shapes collide + bool sphereList(const SphereShape* sphereA, const ListShape* listB, CollisionList& collisions); + + /// \param capuleA pointer to first shape + /// \param listB pointer to second shape + /// \param[out] collisions where to append collision details + /// \return true if shapes collide + bool capsuleList(const CapsuleShape* capsuleA, const ListShape* listB, CollisionList& collisions); + + /// \param listA pointer to first shape + /// \param sphereB pointer to second shape + /// \param[out] collisions where to append collision details + /// \return true if shapes collide + bool listSphere(const ListShape* listA, const SphereShape* sphereB, CollisionList& collisions); + + /// \param listA pointer to first shape + /// \param capsuleB pointer to second shape + /// \param[out] collisions where to append collision details + /// \return true if shapes collide + bool listCapsule(const ListShape* listA, const CapsuleShape* capsuleB, CollisionList& collisions); + + /// \param listA pointer to first shape + /// \param capsuleB pointer to second shape + /// \param[out] collisions where to append collision details + /// \return true if shapes collide + bool listList(const ListShape* listA, const ListShape* listB, CollisionList& collisions); + +} // namespace ShapeCollider + +#endif // __hifi__ShapeCollider__ diff --git a/libraries/shared/src/SphereShape.h b/libraries/shared/src/SphereShape.h new file mode 100644 index 0000000000..d720dd2289 --- /dev/null +++ b/libraries/shared/src/SphereShape.h @@ -0,0 +1,31 @@ +// +// SphereShape.h +// hifi +// +// Created by Andrew Meadows on 2014.02.20 +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __hifi__SphereShape__ +#define __hifi__SphereShape__ + +#include "Shape.h" + +class SphereShape : public Shape { +public: + SphereShape() : Shape(Shape::SPHERE_SHAPE) {} + + SphereShape(float radius) : Shape(Shape::SPHERE_SHAPE) { + _boundingRadius = radius; + } + + SphereShape(float radius, const glm::vec3& position) : Shape(Shape::SPHERE_SHAPE, position) { + _boundingRadius = radius; + } + + float getRadius() const { return _boundingRadius; } + + void setRadius(float radius) { _boundingRadius = radius; } +}; + +#endif /* defined(__hifi__SphereShape__) */ diff --git a/tests/physics/CMakeLists.txt b/tests/physics/CMakeLists.txt new file mode 100644 index 0000000000..482e6e0ec3 --- /dev/null +++ b/tests/physics/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 2.8) + +set(TARGET_NAME physics-tests) + +set(ROOT_DIR ../..) +set(MACRO_DIR ${ROOT_DIR}/cmake/macros) + +# setup for find modules +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") + +#find_package(Qt5Network REQUIRED) +#find_package(Qt5Script REQUIRED) +#find_package(Qt5Widgets REQUIRED) + +include(${MACRO_DIR}/SetupHifiProject.cmake) +setup_hifi_project(${TARGET_NAME} TRUE) + +include(${MACRO_DIR}/AutoMTC.cmake) +auto_mtc(${TARGET_NAME} ${ROOT_DIR}) + +#qt5_use_modules(${TARGET_NAME} Network Script Widgets) + +#include glm +include(${MACRO_DIR}/IncludeGLM.cmake) +include_glm(${TARGET_NAME} ${ROOT_DIR}) + +# link in the shared libraries +include(${MACRO_DIR}/LinkHifiLibrary.cmake) +link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) + +IF (WIN32) + #target_link_libraries(${TARGET_NAME} Winmm Ws2_32) +ENDIF(WIN32) + diff --git a/tests/physics/src/CollisionInfoTests.cpp b/tests/physics/src/CollisionInfoTests.cpp new file mode 100644 index 0000000000..813100944b --- /dev/null +++ b/tests/physics/src/CollisionInfoTests.cpp @@ -0,0 +1,104 @@ +// +// CollisionInfoTests.cpp +// physics-tests +// +// Created by Andrew Meadows on 2014.02.21 +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#include + +#include +#include + +#include +#include + +#include "CollisionInfoTests.h" +#include "PhysicsTestUtil.h" + + +/* +void CollisionInfoTests::rotateThenTranslate() { + CollisionInfo collision; + collision._penetration = xAxis; + collision._contactPoint = yAxis; + collision._addedVelocity = xAxis + yAxis + zAxis; + + glm::quat rotation = glm::angleAxis(rightAngle, zAxis); + float distance = 3.f; + glm::vec3 translation = distance * yAxis; + + collision.rotateThenTranslate(rotation, translation); + + float error = glm::distance(collision._penetration, yAxis); + if (error > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: collision._penetration = " << collision._penetration + << " but we expected " << yAxis + << std::endl; + } + + glm::vec3 expectedContactPoint = -xAxis + translation; + error = glm::distance(collision._contactPoint, expectedContactPoint); + if (error > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: collision._contactPoint = " << collision._contactPoint + << " but we expected " << expectedContactPoint + << std::endl; + } + + glm::vec3 expectedAddedVelocity = yAxis - xAxis + zAxis; + error = glm::distance(collision._addedVelocity, expectedAddedVelocity); + if (error > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: collision._addedVelocity = " << collision._contactPoint + << " but we expected " << expectedAddedVelocity + << std::endl; + } +} + +void CollisionInfoTests::translateThenRotate() { + CollisionInfo collision; + collision._penetration = xAxis; + collision._contactPoint = yAxis; + collision._addedVelocity = xAxis + yAxis + zAxis; + + glm::quat rotation = glm::angleAxis( -rightAngle, zAxis); + float distance = 3.f; + glm::vec3 translation = distance * yAxis; + + collision.translateThenRotate(translation, rotation); + + float error = glm::distance(collision._penetration, -yAxis); + if (error > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: collision._penetration = " << collision._penetration + << " but we expected " << -yAxis + << std::endl; + } + + glm::vec3 expectedContactPoint = (1.f + distance) * xAxis; + error = glm::distance(collision._contactPoint, expectedContactPoint); + if (error > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: collision._contactPoint = " << collision._contactPoint + << " but we expected " << expectedContactPoint + << std::endl; + } + + glm::vec3 expectedAddedVelocity = - yAxis + xAxis + zAxis; + error = glm::distance(collision._addedVelocity, expectedAddedVelocity); + if (error > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: collision._addedVelocity = " << collision._contactPoint + << " but we expected " << expectedAddedVelocity + << std::endl; + } +} +*/ + +void CollisionInfoTests::runAllTests() { +// CollisionInfoTests::rotateThenTranslate(); +// CollisionInfoTests::translateThenRotate(); +} diff --git a/tests/physics/src/CollisionInfoTests.h b/tests/physics/src/CollisionInfoTests.h new file mode 100644 index 0000000000..51579e8f11 --- /dev/null +++ b/tests/physics/src/CollisionInfoTests.h @@ -0,0 +1,21 @@ +// +// CollisionInfoTests.h +// physics-tests +// +// Created by Andrew Meadows on 2014.02.21 +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __tests__CollisionInfoTests__ +#define __tests__CollisionInfoTests__ + +namespace CollisionInfoTests { + +// void rotateThenTranslate(); +// void translateThenRotate(); + + void runAllTests(); +} + +#endif // __tests__CollisionInfoTests__ + diff --git a/tests/physics/src/PhysicsTestUtil.cpp b/tests/physics/src/PhysicsTestUtil.cpp new file mode 100644 index 0000000000..fb940d2043 --- /dev/null +++ b/tests/physics/src/PhysicsTestUtil.cpp @@ -0,0 +1,37 @@ +// +// PhysicsTestUtil.cpp +// physics-tests +// +// Created by Andrew Meadows on 2014.02.21 +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#include + +#include "PhysicsTestUtil.h" + +std::ostream& operator<<(std::ostream& s, const glm::vec3& v) { + s << "<" << v.x << "," << v.y << "," << v.z << ">"; + return s; +} + +std::ostream& operator<<(std::ostream& s, const glm::quat& q) { + s << "<" << q.x << "," << q.y << "," << q.z << "," << q.w << ">"; + return s; +} + +std::ostream& operator<<(std::ostream& s, const glm::mat4& m) { + s << "["; + for (int j = 0; j < 4; ++j) { + s << " " << m[0][j] << " " << m[1][j] << " " << m[2][j] << " " << m[3][j] << ";"; + } + s << " ]"; + return s; +} + +std::ostream& operator<<(std::ostream& s, const CollisionInfo& c) { + s << "[penetration=" << c._penetration + << ", contactPoint=" << c._contactPoint + << ", addedVelocity=" << c._addedVelocity; + return s; +} diff --git a/tests/physics/src/PhysicsTestUtil.h b/tests/physics/src/PhysicsTestUtil.h new file mode 100644 index 0000000000..dcbeaed346 --- /dev/null +++ b/tests/physics/src/PhysicsTestUtil.h @@ -0,0 +1,28 @@ +// +// PhysicsTestUtil.h +// physics-tests +// +// Created by Andrew Meadows on 2014.02.21 +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __tests__PhysicsTestUtil__ +#define __tests__PhysicsTestUtil__ + +#include +#include + +#include + +const glm::vec3 xAxis(1.f, 0.f, 0.f); +const glm::vec3 yAxis(0.f, 1.f, 0.f); +const glm::vec3 zAxis(0.f, 0.f, 1.f); + +const float rightAngle = 90.f; // degrees + +std::ostream& operator<<(std::ostream& s, const glm::vec3& v); +std::ostream& operator<<(std::ostream& s, const glm::quat& q); +std::ostream& operator<<(std::ostream& s, const glm::mat4& m); +std::ostream& operator<<(std::ostream& s, const CollisionInfo& c); + +#endif // __tests__PhysicsTestUtil__ diff --git a/tests/physics/src/ShapeColliderTests.cpp b/tests/physics/src/ShapeColliderTests.cpp new file mode 100644 index 0000000000..314498b1d6 --- /dev/null +++ b/tests/physics/src/ShapeColliderTests.cpp @@ -0,0 +1,706 @@ +// +// ShapeColliderTests.cpp +// physics-tests +// +// Created by Andrew Meadows on 2014.02.21 +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +//#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "PhysicsTestUtil.h" +#include "ShapeColliderTests.h" + +const glm::vec3 origin(0.f); + +void ShapeColliderTests::sphereMissesSphere() { + // non-overlapping spheres of unequal size + float radiusA = 7.f; + float radiusB = 3.f; + float alpha = 1.2f; + float beta = 1.3f; + glm::vec3 offsetDirection = glm::normalize(glm::vec3(1.f, 2.f, 3.f)); + float offsetDistance = alpha * radiusA + beta * radiusB; + + SphereShape sphereA(radiusA, origin); + SphereShape sphereB(radiusB, offsetDistance * offsetDirection); + CollisionList collisions(16); + + // collide A to B... + { + bool touching = ShapeCollider::shapeShape(&sphereA, &sphereB, collisions); + if (touching) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphereA and sphereB should NOT touch" << std::endl; + } + } + + // collide B to A... + { + bool touching = ShapeCollider::shapeShape(&sphereB, &sphereA, collisions); + if (touching) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphereA and sphereB should NOT touch" << std::endl; + } + } + + // also test shapeShape + { + bool touching = ShapeCollider::shapeShape(&sphereB, &sphereA, collisions); + if (touching) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphereA and sphereB should NOT touch" << std::endl; + } + } + + if (collisions.size() > 0) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: expected empty collision list but size is " << collisions.size() + << std::endl; + } +} + +void ShapeColliderTests::sphereTouchesSphere() { + // overlapping spheres of unequal size + float radiusA = 7.f; + float radiusB = 3.f; + float alpha = 0.2f; + float beta = 0.3f; + glm::vec3 offsetDirection = glm::normalize(glm::vec3(1.f, 2.f, 3.f)); + float offsetDistance = alpha * radiusA + beta * radiusB; + float expectedPenetrationDistance = (1.f - alpha) * radiusA + (1.f - beta) * radiusB; + glm::vec3 expectedPenetration = expectedPenetrationDistance * offsetDirection; + + SphereShape sphereA(radiusA, origin); + SphereShape sphereB(radiusB, offsetDistance * offsetDirection); + CollisionList collisions(16); + int numCollisions = 0; + + // collide A to B... + { + bool touching = ShapeCollider::shapeShape(&sphereA, &sphereB, collisions); + if (!touching) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphereA and sphereB should touch" << std::endl; + } else { + ++numCollisions; + } + + // verify state of collisions + if (numCollisions != collisions.size()) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: expected collisions size of " << numCollisions << " but actual size is " << collisions.size() + << std::endl; + } + CollisionInfo* collision = collisions.getCollision(numCollisions - 1); + if (!collision) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: null collision" << std::endl; + } + + // penetration points from sphereA into sphereB + float inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration + << std::endl; + } + + // contactPoint is on surface of sphereA + glm::vec3 AtoB = sphereB.getPosition() - sphereA.getPosition(); + glm::vec3 expectedContactPoint = sphereA.getPosition() + radiusA * glm::normalize(AtoB); + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint + << std::endl; + } + } + + // collide B to A... + { + bool touching = ShapeCollider::shapeShape(&sphereB, &sphereA, collisions); + if (!touching) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphereA and sphereB should touch" << std::endl; + } else { + ++numCollisions; + } + + // penetration points from sphereA into sphereB + CollisionInfo* collision = collisions.getCollision(numCollisions - 1); + float inaccuracy = glm::length(collision->_penetration + expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration + << std::endl; + } + + // contactPoint is on surface of sphereA + glm::vec3 BtoA = sphereA.getPosition() - sphereB.getPosition(); + glm::vec3 expectedContactPoint = sphereB.getPosition() + radiusB * glm::normalize(BtoA); + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint + << std::endl; + } + } +} + +void ShapeColliderTests::sphereMissesCapsule() { + // non-overlapping sphere and capsule + float radiusA = 1.5f; + float radiusB = 2.3f; + float totalRadius = radiusA + radiusB; + float halfHeightB = 1.7f; + float axialOffset = totalRadius + 1.1f * halfHeightB; + float radialOffset = 1.2f * radiusA + 1.3f * radiusB; + + SphereShape sphereA(radiusA); + CapsuleShape capsuleB(radiusB, halfHeightB); + + // give the capsule some arbirary transform + float angle = 37.8; + glm::vec3 axis = glm::normalize( glm::vec3(-7.f, 2.8f, 9.3f) ); + glm::quat rotation = glm::angleAxis(angle, axis); + glm::vec3 translation(15.1f, -27.1f, -38.6f); + capsuleB.setRotation(rotation); + capsuleB.setPosition(translation); + + CollisionList collisions(16); + + // walk sphereA along the local yAxis next to, but not touching, capsuleB + glm::vec3 localStartPosition(radialOffset, axialOffset, 0.f); + int numberOfSteps = 10; + float delta = 1.3f * (totalRadius + halfHeightB) / (numberOfSteps - 1); + for (int i = 0; i < numberOfSteps; ++i) { + // translate sphereA into world-frame + glm::vec3 localPosition = localStartPosition + (float(i) * delta) * yAxis; + sphereA.setPosition(rotation * localPosition + translation); + + // sphereA agains capsuleB + if (ShapeCollider::shapeShape(&sphereA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphere and capsule should NOT touch" + << std::endl; + } + + // capsuleB against sphereA + if (ShapeCollider::shapeShape(&capsuleB, &sphereA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphere and capsule should NOT touch" + << std::endl; + } + } + + if (collisions.size() > 0) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: expected empty collision list but size is " << collisions.size() + << std::endl; + } +} + +void ShapeColliderTests::sphereTouchesCapsule() { + // overlapping sphere and capsule + float radiusA = 2.f; + float radiusB = 1.f; + float totalRadius = radiusA + radiusB; + float halfHeightB = 2.f; + float alpha = 0.5f; + float beta = 0.5f; + float radialOffset = alpha * radiusA + beta * radiusB; + + SphereShape sphereA(radiusA); + CapsuleShape capsuleB(radiusB, halfHeightB); + + CollisionList collisions(16); + int numCollisions = 0; + + { // sphereA collides with capsuleB's cylindrical wall + sphereA.setPosition(radialOffset * xAxis); + + if (!ShapeCollider::shapeShape(&sphereA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphere and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + + // penetration points from sphereA into capsuleB + CollisionInfo* collision = collisions.getCollision(numCollisions - 1); + glm::vec3 expectedPenetration = (radialOffset - totalRadius) * xAxis; + float inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration + << std::endl; + } + + // contactPoint is on surface of sphereA + glm::vec3 expectedContactPoint = sphereA.getPosition() - radiusA * xAxis; + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint + << std::endl; + } + + // capsuleB collides with sphereA + if (!ShapeCollider::shapeShape(&capsuleB, &sphereA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and sphere should touch" + << std::endl; + } else { + ++numCollisions; + } + + // penetration points from sphereA into capsuleB + collision = collisions.getCollision(numCollisions - 1); + expectedPenetration = - (radialOffset - totalRadius) * xAxis; + inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration + << std::endl; + } + + // contactPoint is on surface of capsuleB + glm::vec3 BtoA = sphereA.getPosition() - capsuleB.getPosition(); + glm::vec3 closestApproach = capsuleB.getPosition() + glm::dot(BtoA, yAxis) * yAxis; + expectedContactPoint = closestApproach + radiusB * glm::normalize(BtoA - closestApproach); + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint + << std::endl; + } + } + { // sphereA hits end cap at axis + glm::vec3 axialOffset = (halfHeightB + alpha * radiusA + beta * radiusB) * yAxis; + sphereA.setPosition(axialOffset * yAxis); + + if (!ShapeCollider::shapeShape(&sphereA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphere and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + + // penetration points from sphereA into capsuleB + CollisionInfo* collision = collisions.getCollision(numCollisions - 1); + glm::vec3 expectedPenetration = - ((1.f - alpha) * radiusA + (1.f - beta) * radiusB) * yAxis; + float inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration + << std::endl; + } + + // contactPoint is on surface of sphereA + glm::vec3 expectedContactPoint = sphereA.getPosition() - radiusA * yAxis; + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint + << std::endl; + } + + // capsuleB collides with sphereA + if (!ShapeCollider::shapeShape(&capsuleB, &sphereA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and sphere should touch" + << std::endl; + } else { + ++numCollisions; + } + + // penetration points from sphereA into capsuleB + collision = collisions.getCollision(numCollisions - 1); + expectedPenetration = ((1.f - alpha) * radiusA + (1.f - beta) * radiusB) * yAxis; + inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration + << std::endl; + } + + // contactPoint is on surface of capsuleB + glm::vec3 endPoint; + capsuleB.getEndPoint(endPoint); + expectedContactPoint = endPoint + radiusB * yAxis; + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint + << std::endl; + } + } + { // sphereA hits start cap at axis + glm::vec3 axialOffset = - (halfHeightB + alpha * radiusA + beta * radiusB) * yAxis; + sphereA.setPosition(axialOffset * yAxis); + + if (!ShapeCollider::shapeShape(&sphereA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: sphere and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + + // penetration points from sphereA into capsuleB + CollisionInfo* collision = collisions.getCollision(numCollisions - 1); + glm::vec3 expectedPenetration = ((1.f - alpha) * radiusA + (1.f - beta) * radiusB) * yAxis; + float inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration + << std::endl; + } + + // contactPoint is on surface of sphereA + glm::vec3 expectedContactPoint = sphereA.getPosition() + radiusA * yAxis; + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint + << std::endl; + } + + // capsuleB collides with sphereA + if (!ShapeCollider::shapeShape(&capsuleB, &sphereA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and sphere should touch" + << std::endl; + } else { + ++numCollisions; + } + + // penetration points from sphereA into capsuleB + collision = collisions.getCollision(numCollisions - 1); + expectedPenetration = - ((1.f - alpha) * radiusA + (1.f - beta) * radiusB) * yAxis; + inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration + << std::endl; + } + + // contactPoint is on surface of capsuleB + glm::vec3 startPoint; + capsuleB.getStartPoint(startPoint); + expectedContactPoint = startPoint - radiusB * yAxis; + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint + << std::endl; + } + } + if (collisions.size() != numCollisions) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: expected " << numCollisions << " collisions but actual number is " << collisions.size() + << std::endl; + } +} + +void ShapeColliderTests::capsuleMissesCapsule() { + // non-overlapping capsules + float radiusA = 2.f; + float halfHeightA = 3.f; + float radiusB = 3.f; + float halfHeightB = 4.f; + + float totalRadius = radiusA + radiusB; + float totalHalfLength = totalRadius + halfHeightA + halfHeightB; + + CapsuleShape capsuleA(radiusA, halfHeightA); + CapsuleShape capsuleB(radiusA, halfHeightA); + + CollisionList collisions(16); + + // side by side + capsuleB.setPosition((1.01f * totalRadius) * xAxis); + if (ShapeCollider::shapeShape(&capsuleA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should NOT touch" + << std::endl; + } + if (ShapeCollider::shapeShape(&capsuleB, &capsuleA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should NOT touch" + << std::endl; + } + + // end to end + capsuleB.setPosition((1.01f * totalHalfLength) * xAxis); + if (ShapeCollider::shapeShape(&capsuleA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should NOT touch" + << std::endl; + } + if (ShapeCollider::shapeShape(&capsuleB, &capsuleA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should NOT touch" + << std::endl; + } + + // rotate B and move it to the side + glm::quat rotation = glm::angleAxis(rightAngle, zAxis); + capsuleB.setRotation(rotation); + capsuleB.setPosition((1.01f * (totalRadius + capsuleB.getHalfHeight())) * xAxis); + if (ShapeCollider::shapeShape(&capsuleA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should NOT touch" + << std::endl; + } + if (ShapeCollider::shapeShape(&capsuleB, &capsuleA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should NOT touch" + << std::endl; + } + + if (collisions.size() > 0) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: expected empty collision list but size is " << collisions.size() + << std::endl; + } +} + +void ShapeColliderTests::capsuleTouchesCapsule() { + // overlapping capsules + float radiusA = 2.f; + float halfHeightA = 3.f; + float radiusB = 3.f; + float halfHeightB = 4.f; + + float totalRadius = radiusA + radiusB; + float totalHalfLength = totalRadius + halfHeightA + halfHeightB; + + CapsuleShape capsuleA(radiusA, halfHeightA); + CapsuleShape capsuleB(radiusB, halfHeightB); + + CollisionList collisions(16); + int numCollisions = 0; + + { // side by side + capsuleB.setPosition((0.99f * totalRadius) * xAxis); + if (!ShapeCollider::shapeShape(&capsuleA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + if (!ShapeCollider::shapeShape(&capsuleB, &capsuleA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + } + + { // end to end + capsuleB.setPosition((0.99f * totalHalfLength) * yAxis); + + if (!ShapeCollider::shapeShape(&capsuleA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + if (!ShapeCollider::shapeShape(&capsuleB, &capsuleA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + } + + { // rotate B and move it to the side + glm::quat rotation = glm::angleAxis(rightAngle, zAxis); + capsuleB.setRotation(rotation); + capsuleB.setPosition((0.99f * (totalRadius + capsuleB.getHalfHeight())) * xAxis); + + if (!ShapeCollider::shapeShape(&capsuleA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + if (!ShapeCollider::shapeShape(&capsuleB, &capsuleA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + } + + { // again, but this time check collision details + float overlap = 0.1f; + glm::quat rotation = glm::angleAxis(rightAngle, zAxis); + capsuleB.setRotation(rotation); + glm::vec3 positionB = ((totalRadius + capsuleB.getHalfHeight()) - overlap) * xAxis; + capsuleB.setPosition(positionB); + + // capsuleA vs capsuleB + if (!ShapeCollider::shapeShape(&capsuleA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + + CollisionInfo* collision = collisions.getCollision(numCollisions - 1); + glm::vec3 expectedPenetration = overlap * xAxis; + float inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration + << std::endl; + } + + glm::vec3 expectedContactPoint = capsuleA.getPosition() + radiusA * xAxis; + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint + << std::endl; + } + + // capsuleB vs capsuleA + if (!ShapeCollider::shapeShape(&capsuleB, &capsuleA, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + + collision = collisions.getCollision(numCollisions - 1); + expectedPenetration = - overlap * xAxis; + inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration + << std::endl; + } + + expectedContactPoint = capsuleB.getPosition() - (radiusB + halfHeightB) * xAxis; + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint + << std::endl; + } + } + + { // collide cylinder wall against cylinder wall + float overlap = 0.137f; + float shift = 0.317f * halfHeightA; + glm::quat rotation = glm::angleAxis(rightAngle, zAxis); + capsuleB.setRotation(rotation); + glm::vec3 positionB = (totalRadius - overlap) * zAxis + shift * yAxis; + capsuleB.setPosition(positionB); + + // capsuleA vs capsuleB + if (!ShapeCollider::shapeShape(&capsuleA, &capsuleB, collisions)) + { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: capsule and capsule should touch" + << std::endl; + } else { + ++numCollisions; + } + + CollisionInfo* collision = collisions.getCollision(numCollisions - 1); + glm::vec3 expectedPenetration = overlap * zAxis; + float inaccuracy = glm::length(collision->_penetration - expectedPenetration); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad penetration: expected = " << expectedPenetration + << " actual = " << collision->_penetration + << std::endl; + } + + glm::vec3 expectedContactPoint = capsuleA.getPosition() + radiusA * zAxis + shift * yAxis; + inaccuracy = glm::length(collision->_contactPoint - expectedContactPoint); + if (fabs(inaccuracy) > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ + << " ERROR: bad contactPoint: expected = " << expectedContactPoint + << " actual = " << collision->_contactPoint + << std::endl; + } + } +} + + +void ShapeColliderTests::runAllTests() { + sphereMissesSphere(); + sphereTouchesSphere(); + + sphereMissesCapsule(); + sphereTouchesCapsule(); + + capsuleMissesCapsule(); + capsuleTouchesCapsule(); +} diff --git a/tests/physics/src/ShapeColliderTests.h b/tests/physics/src/ShapeColliderTests.h new file mode 100644 index 0000000000..ecd4a7f045 --- /dev/null +++ b/tests/physics/src/ShapeColliderTests.h @@ -0,0 +1,26 @@ +// +// ShapeColliderTests.h +// physics-tests +// +// Created by Andrew Meadows on 2014.02.21 +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __tests__ShapeColliderTests__ +#define __tests__ShapeColliderTests__ + +namespace ShapeColliderTests { + + void sphereMissesSphere(); + void sphereTouchesSphere(); + + void sphereMissesCapsule(); + void sphereTouchesCapsule(); + + void capsuleMissesCapsule(); + void capsuleTouchesCapsule(); + + void runAllTests(); +} + +#endif // __tests__ShapeColliderTests__ diff --git a/tests/physics/src/main.cpp b/tests/physics/src/main.cpp new file mode 100644 index 0000000000..b0a7adde4e --- /dev/null +++ b/tests/physics/src/main.cpp @@ -0,0 +1,11 @@ +// +// main.cpp +// physics-tests +// + +#include "ShapeColliderTests.h" + +int main(int argc, char** argv) { + ShapeColliderTests::runAllTests(); + return 0; +}