Merge branch 'master' of https://github.com/highfidelity/hifi into metavoxels

Conflicts:
	interface/src/renderer/FBXReader.cpp
This commit is contained in:
Andrzej Kapolka 2014-03-05 18:28:25 -08:00
commit 0448596e58
84 changed files with 2905 additions and 281 deletions

View file

@ -36,11 +36,8 @@ public:
public slots:
void run();
void readPendingDatagrams();
signals:
void willSendAudioDataCallback();
void willSendVisualDataCallback();
private:
ScriptEngine _scriptEngine;
VoxelEditPacketSender _voxelEditSender;

View file

@ -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);

View file

@ -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() {});

View file

@ -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"));

View file

@ -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);

View file

@ -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);
Script.update.connect(maybePlaySound);

View file

@ -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");

View file

@ -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);

View file

@ -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);

View file

@ -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);
Script.update.connect(checkSticks);

View file

@ -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);

View file

@ -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();

View file

@ -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);

464
examples/flockingBirds.js Normal file
View file

@ -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);

View file

@ -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);
Script.update.connect(makeFountain);

View file

@ -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");

View file

@ -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...");

View file

@ -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);

View file

@ -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);

View file

@ -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) {

View file

@ -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);

View file

@ -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);

View file

@ -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);
Script.update.connect(moveVoxel);

View file

@ -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() {

View file

@ -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.

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);
Script.update.connect(maybePlaySound);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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) {

View file

@ -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);

View file

@ -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);
Script.update.connect(moveBird);

View file

@ -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);

View file

@ -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

View file

@ -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<const Shape*>& 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) {

View file

@ -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<const Shape*>& 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

View file

@ -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<Avatar*>(_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<Avatar*>(fadingAvatar.data());
avatar->render();
avatar->render(forShadowMap);
}
}

View file

@ -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);

View file

@ -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<const Shape*> 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);
}
}

View file

@ -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();

View file

@ -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);

View file

@ -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;

View file

@ -73,6 +73,17 @@ bool SkeletonModel::render(float alpha) {
return true;
}
void SkeletonModel::getHandShapes(int jointIndex, QVector<const Shape*>& 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;

View file

@ -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<const Shape*>& shapes) const;
protected:

View file

@ -6,6 +6,7 @@
// Copyright (c) 2013 High Fidelity, Inc. All rights reserved.
//
#include <iostream>
#include <QBuffer>
#include <QDataStream>
#include <QIODevice>
@ -21,6 +22,7 @@
#include <OctalCode.h>
#include <GeometryUtil.h>
#include <Shape.h>
#include <VoxelTree.h>
#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<FBXGeometry>();
template<class T> 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<QString, ExtractedMesh> meshes;
QVector<ExtractedBlendshape> 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<JointShapeInfo> 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

View file

@ -25,6 +25,23 @@ typedef QList<FBXNode> 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:

View file

@ -343,7 +343,14 @@ QSharedPointer<NetworkGeometry> 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()) {

View file

@ -13,10 +13,15 @@
#include "Application.h"
#include "Model.h"
#include <SphereShape.h>
#include <CapsuleShape.h>
#include <ShapeCollider.h>
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<JointState> 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<const Shape*> 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<CapsuleShape*>(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::JointState> Model::updateGeometry(bool delayLoad) {
QVector<JointState> 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);

View file

@ -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<const Shape*> 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<JointState> _jointStates;
QVector<Shape*> _shapes;
class MeshState {
public:
@ -247,6 +259,7 @@ private:
QSharedPointer<NetworkGeometry> _nextGeometry;
float _lodDistance;
float _lodHysteresis;
float _nextLODHysteresis;
float _pupilDilation;
std::vector<float> _blendshapeCoefficients;

View file

@ -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<Texture> 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);
}

View file

@ -89,7 +89,7 @@ public:
friend class AvatarData;
protected:
AvatarData* _owningAvatarData;
std::vector<PalmData> _palms;
std::vector<PalmData> _palms;
glm::quat getBaseOrientation() const;
glm::vec3 getBasePosition() const;

View file

@ -17,9 +17,7 @@
#include <glm/gtc/noise.hpp>
#include <QtCore/QDebug>
#include <QImage>
#include <QRgb>
#include <QDebug>
#include "CoverageMap.h"
#include <GeometryUtil.h>

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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<ParticleID> 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);

View file

@ -11,6 +11,8 @@
#include <glm/gtx/vector_angle.hpp>
#include <QDebug>
#include <OctreeConstants.h>
#include <SharedUtil.h>
#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;
}

View file

@ -13,7 +13,9 @@
#define __hifi__Quat__
#include <glm/gtc/quaternion.hpp>
#include <QtCore/QObject>
#include <QObject>
#include <QString>
/// 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__) */

View file

@ -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();

View file

@ -71,8 +71,7 @@ public slots:
void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); }
signals:
void willSendAudioDataCallback();
void willSendVisualDataCallback();
void update(float deltaTime);
void scriptEnding();
void finished(const QString& fileNameString);
void cleanupMenuItem(const QString& menuItemString);

View file

@ -9,10 +9,12 @@
//
//
#include <QDebug>
#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;
}

View file

@ -14,19 +14,23 @@
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <QtCore/QObject>
#include <QObject>
#include <QString>
/// 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);
};

View file

@ -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 <iostream>
#include <glm/gtx/vector_angle.hpp>
#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();
}

View file

@ -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__) */

View file

@ -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

View file

@ -10,6 +10,7 @@
#define __hifi__CollisionInfo__
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <QVector>
@ -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<CollisionInfo> _collisions;
};

View file

@ -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<ListShapeEntry>& 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);
}

View file

@ -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 <QVector>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/norm.hpp>
#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<ListShapeEntry>& shapes);
protected:
void clear();
void computeBoundingRadius();
QVector<ListShapeEntry> _subShapeEntries;
bool _subShapeTransformsAreDirty;
private:
ListShape(const ListShape& otherList); // don't implement this
};
#endif // __hifi__ListShape__

View file

@ -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 <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
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__) */

View file

@ -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 <iostream>
#include <glm/gtx/norm.hpp>
#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<const SphereShape*>(shapeA);
if (typeB == Shape::SPHERE_SHAPE) {
return sphereSphere(sphereA, static_cast<const SphereShape*>(shapeB), collisions);
} else if (typeB == Shape::CAPSULE_SHAPE) {
return sphereCapsule(sphereA, static_cast<const CapsuleShape*>(shapeB), collisions);
}
} else if (typeA == Shape::CAPSULE_SHAPE) {
const CapsuleShape* capsuleA = static_cast<const CapsuleShape*>(shapeA);
if (typeB == Shape::SPHERE_SHAPE) {
return capsuleSphere(capsuleA, static_cast<const SphereShape*>(shapeB), collisions);
} else if (typeB == Shape::CAPSULE_SHAPE) {
return capsuleCapsule(capsuleA, static_cast<const CapsuleShape*>(shapeB), collisions);
}
} else if (typeA == Shape::LIST_SHAPE) {
const ListShape* listA = static_cast<const ListShape*>(shapeA);
if (typeB == Shape::SPHERE_SHAPE) {
return listSphere(listA, static_cast<const SphereShape*>(shapeB), collisions);
} else if (typeB == Shape::CAPSULE_SHAPE) {
return listCapsule(listA, static_cast<const CapsuleShape*>(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<const SphereShape*>(subShape), collisions) || touching;
} else if (subType == Shape::CAPSULE_SHAPE) {
touching = sphereCapsule(sphereA, static_cast<const CapsuleShape*>(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<const SphereShape*>(subShape), collisions) || touching;
} else if (subType == Shape::CAPSULE_SHAPE) {
touching = capsuleCapsule(capsuleA, static_cast<const CapsuleShape*>(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<const SphereShape*>(subShape), sphereB, collisions) || touching;
} else if (subType == Shape::CAPSULE_SHAPE) {
touching = capsuleSphere(static_cast<const CapsuleShape*>(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<const SphereShape*>(subShape), capsuleB, collisions) || touching;
} else if (subType == Shape::CAPSULE_SHAPE) {
touching = capsuleCapsule(static_cast<const CapsuleShape*>(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

View file

@ -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__

View file

@ -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__) */

View file

@ -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)

View file

@ -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 <iostream>
#include <glm/glm.hpp>
#include <glm/gtx/quaternion.hpp>
#include <CollisionInfo.h>
#include <SharedUtil.h>
#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();
}

View file

@ -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__

View file

@ -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 <glm/gtc/type_ptr.hpp>
#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;
}

View file

@ -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 <glm/glm.hpp>
#include <glm/gtx/quaternion.hpp>
#include <CollisionInfo.h>
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__

View file

@ -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 <stdio.h>
#include <iostream>
#include <glm/glm.hpp>
#include <glm/gtx/quaternion.hpp>
#include <CollisionInfo.h>
#include <ShapeCollider.h>
#include <SharedUtil.h>
#include <SphereShape.h>
#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();
}

View file

@ -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__

View file

@ -0,0 +1,11 @@
//
// main.cpp
// physics-tests
//
#include "ShapeColliderTests.h"
int main(int argc, char** argv) {
ShapeColliderTests::runAllTests();
return 0;
}