diff --git a/CMakeLists.txt b/CMakeLists.txt index 347341efa0..e57e33e3b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,7 +40,8 @@ if (WIN32) endif () message (WINDOW_SDK_PATH= ${WINDOW_SDK_PATH}) set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${WINDOW_SDK_PATH}) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") + # /wd4351 disables warning C4351: new behavior: elements of array will be default initialized + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP /wd4351") # /LARGEADDRESSAWARE enables 32-bit apps to use more than 2GB of memory. # Caveats: http://stackoverflow.com/questions/2288728/drawbacks-of-using-largeaddressaware-for-32-bit-windows-executables # TODO: Remove when building 64-bit. diff --git a/examples/blockWorld.js b/examples/blockWorld.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/controllers/hydra/gun.js b/examples/controllers/hydra/gun.js index 7dd2b5974f..146f9daca3 100644 --- a/examples/controllers/hydra/gun.js +++ b/examples/controllers/hydra/gun.js @@ -153,6 +153,26 @@ if (showScore) { var BULLET_VELOCITY = 10.0; +function entityCollisionWithEntity(entity1, entity2, collision) { + if (entity2 === targetID) { + score++; + if (showScore) { + Overlays.editOverlay(text, { text: "Score: " + score } ); + } + + // We will delete the bullet and target in 1/2 sec, but for now we can see them bounce! + Script.setTimeout(deleteBulletAndTarget, 500); + + // Turn the target and the bullet white + Entities.editEntity(entity1, { color: { red: 255, green: 255, blue: 255 }}); + Entities.editEntity(entity2, { color: { red: 255, green: 255, blue: 255 }}); + + // play the sound near the camera so the shooter can hear it + audioOptions.position = Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation())); + Audio.playSound(targetHitSound, audioOptions); + } +} + function shootBullet(position, velocity, grenade) { var BULLET_SIZE = 0.10; var BULLET_LIFETIME = 10.0; @@ -178,6 +198,7 @@ function shootBullet(position, velocity, grenade) { ignoreCollisions: false, collisionsWillMove: true }); + Script.addEventHandler(bulletID, "collisionWithEntity", entityCollisionWithEntity); // Play firing sounds audioOptions.position = position; @@ -310,27 +331,6 @@ function makePlatform(gravity, scale, size) { } -function entityCollisionWithEntity(entity1, entity2, collision) { - if (((entity1.id == bulletID.id) || (entity1.id == targetID.id)) && - ((entity2.id == bulletID.id) || (entity2.id == targetID.id))) { - score++; - if (showScore) { - Overlays.editOverlay(text, { text: "Score: " + score } ); - } - - // We will delete the bullet and target in 1/2 sec, but for now we can see them bounce! - Script.setTimeout(deleteBulletAndTarget, 500); - - // Turn the target and the bullet white - Entities.editEntity(entity1, { color: { red: 255, green: 255, blue: 255 }}); - Entities.editEntity(entity2, { color: { red: 255, green: 255, blue: 255 }}); - - // play the sound near the camera so the shooter can hear it - audioOptions.position = Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation())); - Audio.playSound(targetHitSound, audioOptions); - } -} - function keyPressEvent(event) { // if our tools are off, then don't do anything if (event.text == "t") { @@ -505,7 +505,6 @@ function scriptEnding() { clearPose(); } -Entities.entityCollisionWithEntity.connect(entityCollisionWithEntity); Script.scriptEnding.connect(scriptEnding); Script.update.connect(update); Controller.mouseReleaseEvent.connect(mouseReleaseEvent); diff --git a/examples/example/entityCollisionExample.js b/examples/example/entityCollisionExample.js new file mode 100644 index 0000000000..de50d52753 --- /dev/null +++ b/examples/example/entityCollisionExample.js @@ -0,0 +1,53 @@ +// +// entityCollisionExample.js +// examples +// +// Created by Howard Stearns on 5/25/15. +// Copyright 2015 High Fidelity, Inc. +// +// This is an example script that demonstrates use of the per-entity event handlers. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +function someCollisionFunction(entityA, entityB, collision) { + print("collision: " + JSON.stringify({a: entityA, b: entityB, c: collision})); +} + +var position = Vec3.sum(MyAvatar.position, Quat.getFront(MyAvatar.orientation)); +var properties = { + type: "Box", + position: position, + collisionsWillMove: true, + color: { red: 200, green: 0, blue: 0 } +}; +var collider = Entities.addEntity(properties); +var armed = false; +function togglePrinting() { + print('togglePrinting from ' + armed + ' on ' + collider); + if (armed) { + Script.removeEventHandler(collider, "collisionWithEntity", someCollisionFunction); + } else { + Script.addEventHandler(collider, "collisionWithEntity", someCollisionFunction); + } + armed = !armed; + print("Red box " + (armed ? "will" : "will not") + " print on collision."); +} +togglePrinting(); + +properties.position.y += 0.2; +properties.color.blue += 200; +// A handy target for the collider to hit. +var target = Entities.addEntity(properties); + +properties.position.y += 0.2; +properties.color.green += 200; +var button = Entities.addEntity(properties); +Script.addEventHandler(button, "clickReleaseOnEntity", togglePrinting); + +Script.scriptEnding.connect(function () { + Entities.deleteEntity(collider); + Entities.deleteEntity(target); + Entities.deleteEntity(button); +}); diff --git a/examples/example/games/billiards.js b/examples/example/games/billiards.js index 5e08322c77..25ff5e7eae 100644 --- a/examples/example/games/billiards.js +++ b/examples/example/games/billiards.js @@ -33,8 +33,8 @@ var cuePosition; var startStroke = 0; // Sounds to use -hitSounds = []; -hitSounds.push(SoundCache.getSound(HIFI_PUBLIC_BUCKET + "Collisions-ballhitsandcatches/billiards/collision1.wav")); +var hitSound = HIFI_PUBLIC_BUCKET + "sounds/Collisions-ballhitsandcatches/billiards/collision1.wav"; +SoundCache.getSound(hitSound); HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; var screenSize = Controller.getViewportDimensions(); @@ -127,6 +127,7 @@ function makeBalls(pos) { ignoreCollisions: false, damping: 0.50, shapeType: "sphere", + collisionSoundURL: hitSound, collisionsWillMove: true })); ballPosition.z += (BALL_SIZE + BALL_GAP) * SCALE; ballNumber++; @@ -225,26 +226,11 @@ function update(deltaTime) { } } -function entityCollisionWithEntity(entity1, entity2, collision) { - /* - NOT WORKING YET - if ((entity1.id == cueBall.id) || (entity2.id == cueBall.id)) { - print("Cue ball collision!"); - //audioOptions.position = Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation())); - //Audio.playSound(hitSounds[0], { position: Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation())) }); - } - - else if (isObjectBall(entity1.id) || isObjectBall(entity2.id)) { - print("Object ball collision"); - } */ -} - tableCenter = Vec3.sum(MyAvatar.position, Vec3.multiply(4.0, Quat.getFront(Camera.getOrientation()))); makeTable(tableCenter); makeBalls(tableCenter); -Entities.entityCollisionWithEntity.connect(entityCollisionWithEntity); Script.scriptEnding.connect(cleanup); Controller.keyPressEvent.connect(keyPressEvent); Controller.keyReleaseEvent.connect(keyReleaseEvent); diff --git a/examples/example/games/spaceInvadersExample.js b/examples/example/games/spaceInvadersExample.js index 819cf6b774..08ad56c04d 100644 --- a/examples/example/games/spaceInvadersExample.js +++ b/examples/example/games/spaceInvadersExample.js @@ -21,6 +21,16 @@ var gameOver = false; var invaderStepsPerCycle = 120; // the number of update steps it takes then invaders to move one column to the right var invaderStepOfCycle = 0; // current iteration in the cycle var invaderMoveDirection = 1; // 1 for moving to right, -1 for moving to left +var LEFT = ","; +var RIGHT = "."; +var FIRE = "SPACE"; +var QUIT = "q"; + +print("Use:"); +print(LEFT + " to move left"); +print(RIGHT + " to move right"); +print(FIRE + " to fire"); +print(QUIT + " to quit"); // game length... var itemLifetimes = 60; // 1 minute @@ -65,8 +75,8 @@ var myShipProperties; // create the rows of space invaders var invaders = new Array(); -var numberOfRows = 5; -var invadersPerRow = 8; +var numberOfRows = 3 // FIXME 5; +var invadersPerRow = 3 // FIXME 8; var emptyColumns = 2; // number of invader width columns not filled with invaders var invadersBottomCorner = { x: gameAt.x, y: middleY , z: gameAt.z }; var rowHeight = ((gameAt.y + gameSize.y) - invadersBottomCorner.y) / numberOfRows; @@ -80,7 +90,6 @@ var stepsToGround = (middleY - gameAt.y) / yPerStep; var maxInvaderRowOffset=stepsToGround; // missile related items -var missileFired = false; var myMissile; // sounds @@ -174,6 +183,7 @@ function initializeInvaders() { invaderPosition = getInvaderPosition(row, column); invaders[row][column] = Entities.addEntity({ type: "Model", + shapeType: "box", position: invaderPosition, velocity: { x: 0, y: 0, z: 0 }, gravity: { x: 0, y: 0, z: 0 }, @@ -181,6 +191,7 @@ function initializeInvaders() { dimensions: { x: invaderSize * 2, y: invaderSize * 2, z: invaderSize * 2 }, color: { red: 255, green: 0, blue: 0 }, modelURL: invaderModels[row].modelURL, + collisionsWillMove: true, lifetime: itemLifetimes }); } @@ -264,17 +275,17 @@ Script.update.connect(update); function cleanupGame() { print("cleaning up game..."); Entities.deleteEntity(myShip); - print("cleanupGame() ... Entities.deleteEntity(myShip)... myShip.id="+myShip.id); + print("cleanupGame() ... Entities.deleteEntity(myShip)... myShip="+myShip); for (var row = 0; row < numberOfRows; row++) { for (var column = 0; column < invadersPerRow; column++) { Entities.deleteEntity(invaders[row][column]); - print("cleanupGame() ... Entities.deleteEntity(invaders[row][column])... invaders[row][column].id=" - +invaders[row][column].id); + print("cleanupGame() ... Entities.deleteEntity(invaders[row][column])... invaders[row][column]=" + +invaders[row][column]); } } // clean up our missile - if (missileFired) { + if (myMissile) { Entities.deleteEntity(myMissile); } @@ -293,15 +304,23 @@ function moveShipTo(position) { Entities.editEntity(myShip, { position: position }); } +function entityCollisionWithEntity(entityA, entityB, collision) { + print("entityCollisionWithEntity() a="+entityA + " b=" + entityB); + Vec3.print('entityCollisionWithEntity() penetration=', collision.penetration); + Vec3.print('entityCollisionWithEntity() contactPoint=', collision.contactPoint); + + deleteIfInvader(entityB); +} + function fireMissile() { // we only allow one missile at a time... var canFire = false; // If we've fired a missile, then check to see if it's still alive - if (missileFired) { + if (myMissile) { var missileProperties = Entities.getEntityProperties(myMissile); - if (!missileProperties) { + if (!missileProperties || (missileProperties.type === 'Unknown')) { print("canFire = true"); canFire = true; } @@ -322,11 +341,12 @@ function fireMissile() { velocity: { x: 0, y: 5, z: 0}, gravity: { x: 0, y: 0, z: 0 }, damping: 0, - dimensions: { x: missileSize * 2, y: missileSize * 2, z: missileSize * 2 }, + collisionsWillMove: true, + dimensions: { x: missileSize, y: missileSize, z: missileSize }, color: { red: 0, green: 0, blue: 255 }, lifetime: 5 }); - + Script.addEventHandler(myMissile, "collisionWithEntity", entityCollisionWithEntity); var options = {} if (soundInMyHead) { options.position = { x: MyAvatar.position.x + 0.0, @@ -335,30 +355,30 @@ function fireMissile() { } else { options.position = missilePosition; } - + Audio.playSound(shootSound, options); - missileFired = true; } } function keyPressEvent(key) { //print("keyPressEvent key.text="+key.text); - if (key.text == ",") { + + if (key.text == LEFT) { myShipProperties.position.x -= 0.1; if (myShipProperties.position.x < gameAt.x) { myShipProperties.position.x = gameAt.x; } moveShipTo(myShipProperties.position); - } else if (key.text == ".") { + } else if (key.text == RIGHT) { myShipProperties.position.x += 0.1; if (myShipProperties.position.x > gameAt.x + gameSize.x) { myShipProperties.position.x = gameAt.x + gameSize.x; } moveShipTo(myShipProperties.position); - } else if (key.text == "f") { + } else if (key.text == FIRE) { fireMissile(); - } else if (key.text == "q") { + } else if (key.text == QUIT) { endGame(); } } @@ -370,7 +390,7 @@ Controller.captureKeyEvents({text: " "}); function deleteIfInvader(possibleInvaderEntity) { for (var row = 0; row < numberOfRows; row++) { for (var column = 0; column < invadersPerRow; column++) { - if (invaders[row][column].id && invaders[row][column].id == possibleInvaderEntity.id) { + if (invaders[row][column] == possibleInvaderEntity) { Entities.deleteEntity(possibleInvaderEntity); Entities.deleteEntity(myMissile); @@ -390,20 +410,6 @@ function deleteIfInvader(possibleInvaderEntity) { } } -function entityCollisionWithEntity(entityA, entityB, collision) { - print("entityCollisionWithEntity() a.id="+entityA.id + " b.id=" + entityB.id); - Vec3.print('entityCollisionWithEntity() penetration=', collision.penetration); - Vec3.print('entityCollisionWithEntity() contactPoint=', collision.contactPoint); - if (missileFired) { - if (myMissile.id == entityA.id) { - deleteIfInvader(entityB); - } else if (myMissile.id == entityB.id) { - deleteIfInvader(entityA); - } - } -} -Entities.entityCollisionWithEntity.connect(entityCollisionWithEntity); - // initialize the game... initializeMyShip(); diff --git a/examples/example/globalCollisionsExample.js b/examples/example/globalCollisionsExample.js index 5813cb2472..624ad43219 100644 --- a/examples/example/globalCollisionsExample.js +++ b/examples/example/globalCollisionsExample.js @@ -12,17 +12,6 @@ // -print("hello..."); - - -function entityCollisionWithEntity(entityA, entityB, collision) { - print("entityCollisionWithParticle().."); - print(" entityA.getID()=" + entityA.id); - print(" entityB.getID()=" + entityB.id); - Vec3.print('penetration=', collision.penetration); - Vec3.print('contactPoint=', collision.contactPoint); -} - -Entities.entityCollisionWithEntity.connect(entityCollisionWithEntity); - -print("here... hello..."); +print("The global collision event is obsolete. Please instead use:"); +print(" the collisionSoundURL property on entities, or"); +print(" entityCollisionExample.js"); diff --git a/examples/pointer.js b/examples/pointer.js index cdfb93f2d3..eebe4ec5be 100644 --- a/examples/pointer.js +++ b/examples/pointer.js @@ -1,9 +1,88 @@ +// pointer.js +// examples +// +// Created by Eric Levin on May 26, 2015 +// Copyright 2015 High Fidelity, Inc. +// +// Provides a pointer with option to draw on surfaces +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + var lineEntityID = null; var lineIsRezzed = false; var altHeld = false; var lineCreated = false; +var position, positionOffset, prevPosition; +var nearLinePosition; +var strokes = []; +var STROKE_ADJUST = 0.005; +var DISTANCE_DRAW_THRESHOLD = .02; +var drawDistance = 0; -function nearLinePoint(targetPosition) { +var LINE_WIDTH = 20; + +var userCanPoint = false; +var userCanDraw = false; + +var BUTTON_SIZE = 32; +var PADDING = 3; + +var buttonOffColor = { + red: 250, + green: 10, + blue: 10 +}; +var buttonOnColor = { + red: 10, + green: 200, + blue: 100 +}; + +HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; +var screenSize = Controller.getViewportDimensions(); + +var drawButton = Overlays.addOverlay("image", { + x: screenSize.x / 2 - BUTTON_SIZE + PADDING * 2, + y: screenSize.y - (BUTTON_SIZE + PADDING), + width: BUTTON_SIZE, + height: BUTTON_SIZE, + imageURL: HIFI_PUBLIC_BUCKET + "images/pencil.png?v2", + color: buttonOffColor, + alpha: 1 +}); + +var pointerButton = Overlays.addOverlay("image", { + x: screenSize.x / 2 - BUTTON_SIZE * 2 + PADDING, + y: screenSize.y - (BUTTON_SIZE + PADDING), + width: BUTTON_SIZE, + height: BUTTON_SIZE, + imageURL: HIFI_PUBLIC_BUCKET + "images/laser.png", + color: buttonOffColor, + alpha: 1 +}) + + + +var center = Vec3.sum(MyAvatar.position, Vec3.multiply(2.0, Quat.getFront(Camera.getOrientation()))); +center.y += 0.5; +var whiteBoard = Entities.addEntity({ + type: "Box", + position: center, + dimensions: { + x: 1, + y: 1, + z: .001 + }, + color: { + red: 255, + green: 255, + blue: 255 + } +}); + +function calculateNearLinePosition(targetPosition) { var handPosition = MyAvatar.getRightPalmPosition(); var along = Vec3.subtract(targetPosition, handPosition); along = Vec3.normalize(along); @@ -22,30 +101,39 @@ function removeLine() { function createOrUpdateLine(event) { + if (!userCanPoint) { + return; + } var pickRay = Camera.computePickRay(event.x, event.y); var intersection = Entities.findRayIntersection(pickRay, true); // accurate picking var props = Entities.getEntityProperties(intersection.entityID); if (intersection.intersects) { - var dim = Vec3.subtract(intersection.intersection, nearLinePoint(intersection.intersection)); + startPosition = intersection.intersection; + var subtractVec = Vec3.multiply(Vec3.normalize(pickRay.direction), STROKE_ADJUST); + startPosition = Vec3.subtract(startPosition, subtractVec); + nearLinePosition = calculateNearLinePosition(intersection.intersection); + positionOffset = Vec3.subtract(startPosition, nearLinePosition); if (lineIsRezzed) { Entities.editEntity(lineEntityID, { - position: nearLinePoint(intersection.intersection), - dimensions: dim, - lifetime: 15 + props.lifespan // renew lifetime + position: nearLinePosition, + dimensions: positionOffset, }); + if (userCanDraw) { + draw(); + } } else { lineIsRezzed = true; + prevPosition = startPosition; lineEntityID = Entities.addEntity({ type: "Line", - position: nearLinePoint(intersection.intersection), - dimensions: dim, + position: nearLinePosition, + dimensions: positionOffset, color: { red: 255, green: 255, blue: 255 }, - lifetime: 15 // if someone crashes while pointing, don't leave the line there forever. }); } } else { @@ -53,8 +141,69 @@ function createOrUpdateLine(event) { } } +function draw() { + + //We only want to draw line if distance between starting and previous point is large enough + drawDistance = Vec3.distance(startPosition, prevPosition); + if (drawDistance < DISTANCE_DRAW_THRESHOLD) { + return; + } + + var offset = Vec3.subtract(startPosition, prevPosition); + strokes.push(Entities.addEntity({ + type: "Line", + position: prevPosition, + dimensions: offset, + color: { + red: 200, + green: 40, + blue: 200 + }, + lineWidth: LINE_WIDTH + })); + prevPosition = startPosition; +} function mousePressEvent(event) { + var clickedOverlay = Overlays.getOverlayAtPoint({ + x: event.x, + y: event.y + }); + if (clickedOverlay == drawButton) { + userCanDraw = !userCanDraw; + if (userCanDraw === true) { + Overlays.editOverlay(drawButton, { + color: buttonOnColor + }); + } else { + Overlays.editOverlay(drawButton, { + color: buttonOffColor + }); + } + } + + if (clickedOverlay == pointerButton) { + userCanPoint = !userCanPoint; + if (userCanPoint === true) { + Overlays.editOverlay(pointerButton, { + color: buttonOnColor + }); + if (userCanDraw === true) { + + Overlays.editOverlay(drawButton, { + color: buttonOnColor + }); + } + } else { + Overlays.editOverlay(pointerButton, { + color: buttonOffColor + }); + Overlays.editOverlay(drawButton, { + color: buttonOffColor + }); + } + } + if (!event.isLeftButton || altHeld) { return; } @@ -64,11 +213,13 @@ function mousePressEvent(event) { } + function mouseMoveEvent(event) { createOrUpdateLine(event); } + function mouseReleaseEvent(event) { if (!lineCreated) { return; @@ -91,7 +242,18 @@ function keyReleaseEvent(event) { } +function cleanup() { + Entities.deleteEntity(whiteBoard); + for (var i = 0; i < strokes.length; i++) { + Entities.deleteEntity(strokes[i]); + } + Overlays.deleteOverlay(drawButton); + Overlays.deleteOverlay(pointerButton); +} + + +Script.scriptEnding.connect(cleanup); Controller.mousePressEvent.connect(mousePressEvent); Controller.mouseReleaseEvent.connect(mouseReleaseEvent); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5bc1040a34..f564cf952a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2175,9 +2175,6 @@ void Application::init() { auto entityScriptingInterface = DependencyManager::get(); - connect(&_entitySimulation, &EntitySimulation::entityCollisionWithEntity, - entityScriptingInterface.data(), &EntityScriptingInterface::entityCollisionWithEntity); - // connect the _entityCollisionSystem to our EntityTreeRenderer since that's what handles running entity scripts connect(&_entitySimulation, &EntitySimulation::entityCollisionWithEntity, &_entities, &EntityTreeRenderer::entityCollisionWithEntity); diff --git a/interface/src/devices/DdeFaceTracker.cpp b/interface/src/devices/DdeFaceTracker.cpp index fba37736e3..27e15cea0e 100644 --- a/interface/src/devices/DdeFaceTracker.cpp +++ b/interface/src/devices/DdeFaceTracker.cpp @@ -141,13 +141,6 @@ static const float STARTING_DDE_MESSAGE_TIME = 0.033f; static const float DEFAULT_DDE_EYE_CLOSING_THRESHOLD = 0.8f; static const int CALIBRATION_SAMPLES = 150; -#ifdef WIN32 -// warning C4351: new behavior: elements of array 'DdeFaceTracker::_lastEyeBlinks' will be default initialized -// warning C4351: new behavior: elements of array 'DdeFaceTracker::_filteredEyeBlinks' will be default initialized -// warning C4351: new behavior: elements of array 'DdeFaceTracker::_lastEyeCoefficients' will be default initialized -#pragma warning(disable:4351) -#endif - DdeFaceTracker::DdeFaceTracker() : DdeFaceTracker(QHostAddress::Any, DDE_SERVER_PORT, DDE_CONTROL_PORT) { @@ -214,10 +207,6 @@ DdeFaceTracker::~DdeFaceTracker() { } } -#ifdef WIN32 -#pragma warning(default:4351) -#endif - void DdeFaceTracker::init() { FaceTracker::init(); setEnabled(Menu::getInstance()->isOptionChecked(MenuOption::UseCamera) && !_isMuted); diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 6ad2bed291..b4941b833b 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -318,15 +318,6 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef return 0; } - // if this bitstream indicates that this node is the simulation owner, ignore any physics-related updates. - glm::vec3 savePosition = getPosition(); - glm::quat saveRotation = getRotation(); - // glm::vec3 saveVelocity = _velocity; - // glm::vec3 saveAngularVelocity = _angularVelocity; - // glm::vec3 saveGravity = _gravity; - // glm::vec3 saveAcceleration = _acceleration; - - // Header bytes // object ID [16 bytes] // ByteCountCoded(type code) [~1 byte] @@ -337,299 +328,308 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef const int MINIMUM_HEADER_BYTES = 27; int bytesRead = 0; - if (bytesLeftToRead >= MINIMUM_HEADER_BYTES) { + if (bytesLeftToRead < MINIMUM_HEADER_BYTES) { + return 0; + } - int originalLength = bytesLeftToRead; - QByteArray originalDataBuffer((const char*)data, originalLength); + // if this bitstream indicates that this node is the simulation owner, ignore any physics-related updates. + glm::vec3 savePosition = getPosition(); + glm::quat saveRotation = getRotation(); + glm::vec3 saveVelocity = _velocity; + glm::vec3 saveAngularVelocity = _angularVelocity; - int clockSkew = args.sourceNode ? args.sourceNode->getClockSkewUsec() : 0; + int originalLength = bytesLeftToRead; + QByteArray originalDataBuffer((const char*)data, originalLength); - const unsigned char* dataAt = data; + int clockSkew = args.sourceNode ? args.sourceNode->getClockSkewUsec() : 0; - // id - QByteArray encodedID = originalDataBuffer.mid(bytesRead, NUM_BYTES_RFC4122_UUID); // maximum possible size - _id = QUuid::fromRfc4122(encodedID); - dataAt += encodedID.size(); - bytesRead += encodedID.size(); - - // type - QByteArray encodedType = originalDataBuffer.mid(bytesRead); // maximum possible size - ByteCountCoded typeCoder = encodedType; - encodedType = typeCoder; // determine true length - dataAt += encodedType.size(); - bytesRead += encodedType.size(); - quint32 type = typeCoder; - _type = (EntityTypes::EntityType)type; + const unsigned char* dataAt = data; - bool overwriteLocalData = true; // assume the new content overwrites our local data + // id + QByteArray encodedID = originalDataBuffer.mid(bytesRead, NUM_BYTES_RFC4122_UUID); // maximum possible size + _id = QUuid::fromRfc4122(encodedID); + dataAt += encodedID.size(); + bytesRead += encodedID.size(); + + // type + QByteArray encodedType = originalDataBuffer.mid(bytesRead); // maximum possible size + ByteCountCoded typeCoder = encodedType; + encodedType = typeCoder; // determine true length + dataAt += encodedType.size(); + bytesRead += encodedType.size(); + quint32 type = typeCoder; + _type = (EntityTypes::EntityType)type; - // _created - quint64 createdFromBuffer = 0; - memcpy(&createdFromBuffer, dataAt, sizeof(createdFromBuffer)); - dataAt += sizeof(createdFromBuffer); - bytesRead += sizeof(createdFromBuffer); + bool overwriteLocalData = true; // assume the new content overwrites our local data - quint64 now = usecTimestampNow(); - if (_created == UNKNOWN_CREATED_TIME) { - // we don't yet have a _created timestamp, so we accept this one - createdFromBuffer -= clockSkew; - if (createdFromBuffer > now || createdFromBuffer == UNKNOWN_CREATED_TIME) { - createdFromBuffer = now; - } - _created = createdFromBuffer; + // _created + quint64 createdFromBuffer = 0; + memcpy(&createdFromBuffer, dataAt, sizeof(createdFromBuffer)); + dataAt += sizeof(createdFromBuffer); + bytesRead += sizeof(createdFromBuffer); + + quint64 now = usecTimestampNow(); + if (_created == UNKNOWN_CREATED_TIME) { + // we don't yet have a _created timestamp, so we accept this one + createdFromBuffer -= clockSkew; + if (createdFromBuffer > now || createdFromBuffer == UNKNOWN_CREATED_TIME) { + createdFromBuffer = now; } + _created = createdFromBuffer; + } + #ifdef WANT_DEBUG + quint64 lastEdited = getLastEdited(); + float editedAgo = getEditedAgo(); + QString agoAsString = formatSecondsElapsed(editedAgo); + QString ageAsString = formatSecondsElapsed(getAge()); + qCDebug(entities) << "------------------------------------------"; + qCDebug(entities) << "Loading entity " << getEntityItemID() << " from buffer..."; + qCDebug(entities) << "------------------------------------------"; + debugDump(); + qCDebug(entities) << "------------------------------------------"; + qCDebug(entities) << " _created =" << _created; + qCDebug(entities) << " age=" << getAge() << "seconds - " << ageAsString; + qCDebug(entities) << " lastEdited =" << lastEdited; + qCDebug(entities) << " ago=" << editedAgo << "seconds - " << agoAsString; + #endif + + quint64 lastEditedFromBuffer = 0; + quint64 lastEditedFromBufferAdjusted = 0; + + // TODO: we could make this encoded as a delta from _created + // _lastEdited + memcpy(&lastEditedFromBuffer, dataAt, sizeof(lastEditedFromBuffer)); + dataAt += sizeof(lastEditedFromBuffer); + bytesRead += sizeof(lastEditedFromBuffer); + lastEditedFromBufferAdjusted = lastEditedFromBuffer - clockSkew; + if (lastEditedFromBufferAdjusted > now) { + lastEditedFromBufferAdjusted = now; + } + + bool fromSameServerEdit = (lastEditedFromBuffer == _lastEditedFromRemoteInRemoteTime); + + #ifdef WANT_DEBUG + qCDebug(entities) << "data from server **************** "; + qCDebug(entities) << " entityItemID:" << getEntityItemID(); + qCDebug(entities) << " now:" << now; + qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); + qCDebug(entities) << " lastEditedFromBuffer:" << debugTime(lastEditedFromBuffer, now); + qCDebug(entities) << " clockSkew:" << debugTimeOnly(clockSkew); + qCDebug(entities) << " lastEditedFromBufferAdjusted:" << debugTime(lastEditedFromBufferAdjusted, now); + qCDebug(entities) << " _lastEditedFromRemote:" << debugTime(_lastEditedFromRemote, now); + qCDebug(entities) << " _lastEditedFromRemoteInRemoteTime:" << debugTime(_lastEditedFromRemoteInRemoteTime, now); + qCDebug(entities) << " fromSameServerEdit:" << fromSameServerEdit; + #endif + + bool ignoreServerPacket = false; // assume we'll use this server packet + + // If this packet is from the same server edit as the last packet we accepted from the server + // we probably want to use it. + if (fromSameServerEdit) { + // If this is from the same sever packet, then check against any local changes since we got + // the most recent packet from this server time + if (_lastEdited > _lastEditedFromRemote) { + ignoreServerPacket = true; + } + } else { + // If this isn't from the same sever packet, then honor our skew adjusted times... + // If we've changed our local tree more recently than the new data from this packet + // then we will not be changing our values, instead we just read and skip the data + if (_lastEdited > lastEditedFromBufferAdjusted) { + ignoreServerPacket = true; + } + } + + if (ignoreServerPacket) { + overwriteLocalData = false; #ifdef WANT_DEBUG - quint64 lastEdited = getLastEdited(); - float editedAgo = getEditedAgo(); - QString agoAsString = formatSecondsElapsed(editedAgo); - QString ageAsString = formatSecondsElapsed(getAge()); - qCDebug(entities) << "------------------------------------------"; - qCDebug(entities) << "Loading entity " << getEntityItemID() << " from buffer..."; - qCDebug(entities) << "------------------------------------------"; + qCDebug(entities) << "IGNORING old data from server!!! ****************"; debugDump(); - qCDebug(entities) << "------------------------------------------"; - qCDebug(entities) << " _created =" << _created; - qCDebug(entities) << " age=" << getAge() << "seconds - " << ageAsString; - qCDebug(entities) << " lastEdited =" << lastEdited; - qCDebug(entities) << " ago=" << editedAgo << "seconds - " << agoAsString; #endif - - quint64 lastEditedFromBuffer = 0; - quint64 lastEditedFromBufferAdjusted = 0; - - // TODO: we could make this encoded as a delta from _created - // _lastEdited - memcpy(&lastEditedFromBuffer, dataAt, sizeof(lastEditedFromBuffer)); - dataAt += sizeof(lastEditedFromBuffer); - bytesRead += sizeof(lastEditedFromBuffer); - lastEditedFromBufferAdjusted = lastEditedFromBuffer - clockSkew; - if (lastEditedFromBufferAdjusted > now) { - lastEditedFromBufferAdjusted = now; - } - - bool fromSameServerEdit = (lastEditedFromBuffer == _lastEditedFromRemoteInRemoteTime); - + } else { #ifdef WANT_DEBUG - qCDebug(entities) << "data from server **************** "; - qCDebug(entities) << " entityItemID:" << getEntityItemID(); - qCDebug(entities) << " now:" << now; - qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); - qCDebug(entities) << " lastEditedFromBuffer:" << debugTime(lastEditedFromBuffer, now); - qCDebug(entities) << " clockSkew:" << debugTimeOnly(clockSkew); - qCDebug(entities) << " lastEditedFromBufferAdjusted:" << debugTime(lastEditedFromBufferAdjusted, now); - qCDebug(entities) << " _lastEditedFromRemote:" << debugTime(_lastEditedFromRemote, now); - qCDebug(entities) << " _lastEditedFromRemoteInRemoteTime:" << debugTime(_lastEditedFromRemoteInRemoteTime, now); - qCDebug(entities) << " fromSameServerEdit:" << fromSameServerEdit; + qCDebug(entities) << "USING NEW data from server!!! ****************"; + debugDump(); #endif - bool ignoreServerPacket = false; // assume we'll use this server packet - - // If this packet is from the same server edit as the last packet we accepted from the server - // we probably want to use it. - if (fromSameServerEdit) { - // If this is from the same sever packet, then check against any local changes since we got - // the most recent packet from this server time - if (_lastEdited > _lastEditedFromRemote) { - ignoreServerPacket = true; - } - } else { - // If this isn't from the same sever packet, then honor our skew adjusted times... - // If we've changed our local tree more recently than the new data from this packet - // then we will not be changing our values, instead we just read and skip the data - if (_lastEdited > lastEditedFromBufferAdjusted) { - ignoreServerPacket = true; - } - } + // don't allow _lastEdited to be in the future + _lastEdited = lastEditedFromBufferAdjusted; + _lastEditedFromRemote = now; + _lastEditedFromRemoteInRemoteTime = lastEditedFromBuffer; - if (ignoreServerPacket) { - overwriteLocalData = false; - #ifdef WANT_DEBUG - qCDebug(entities) << "IGNORING old data from server!!! ****************"; - debugDump(); - #endif - } else { + // TODO: only send this notification if something ACTUALLY changed (hint, we haven't yet parsed + // the properties out of the bitstream (see below)) + somethingChangedNotification(); // notify derived classes that something has changed + } - #ifdef WANT_DEBUG - qCDebug(entities) << "USING NEW data from server!!! ****************"; - debugDump(); - #endif - - // don't allow _lastEdited to be in the future - _lastEdited = lastEditedFromBufferAdjusted; - _lastEditedFromRemote = now; - _lastEditedFromRemoteInRemoteTime = lastEditedFromBuffer; - - // TODO: only send this notification if something ACTUALLY changed (hint, we haven't yet parsed - // the properties out of the bitstream (see below)) - somethingChangedNotification(); // notify derived classes that something has changed - } - - // last updated is stored as ByteCountCoded delta from lastEdited - QByteArray encodedUpdateDelta = originalDataBuffer.mid(bytesRead); // maximum possible size - ByteCountCoded updateDeltaCoder = encodedUpdateDelta; - quint64 updateDelta = updateDeltaCoder; + // last updated is stored as ByteCountCoded delta from lastEdited + QByteArray encodedUpdateDelta = originalDataBuffer.mid(bytesRead); // maximum possible size + ByteCountCoded updateDeltaCoder = encodedUpdateDelta; + quint64 updateDelta = updateDeltaCoder; + if (overwriteLocalData) { + _lastUpdated = lastEditedFromBufferAdjusted + updateDelta; // don't adjust for clock skew since we already did that + #ifdef WANT_DEBUG + qCDebug(entities) << " _lastUpdated:" << debugTime(_lastUpdated, now); + qCDebug(entities) << " _lastEdited:" << debugTime(_lastEdited, now); + qCDebug(entities) << " lastEditedFromBufferAdjusted:" << debugTime(lastEditedFromBufferAdjusted, now); + #endif + } + encodedUpdateDelta = updateDeltaCoder; // determine true length + dataAt += encodedUpdateDelta.size(); + bytesRead += encodedUpdateDelta.size(); + + // Newer bitstreams will have a last simulated and a last updated value + if (args.bitstreamVersion >= VERSION_ENTITIES_HAS_LAST_SIMULATED_TIME) { + // last simulated is stored as ByteCountCoded delta from lastEdited + QByteArray encodedSimulatedDelta = originalDataBuffer.mid(bytesRead); // maximum possible size + ByteCountCoded simulatedDeltaCoder = encodedSimulatedDelta; + quint64 simulatedDelta = simulatedDeltaCoder; if (overwriteLocalData) { - _lastUpdated = lastEditedFromBufferAdjusted + updateDelta; // don't adjust for clock skew since we already did that + _lastSimulated = lastEditedFromBufferAdjusted + simulatedDelta; // don't adjust for clock skew since we already did that #ifdef WANT_DEBUG - qCDebug(entities) << " _lastUpdated:" << debugTime(_lastUpdated, now); + qCDebug(entities) << " _lastSimulated:" << debugTime(_lastSimulated, now); qCDebug(entities) << " _lastEdited:" << debugTime(_lastEdited, now); qCDebug(entities) << " lastEditedFromBufferAdjusted:" << debugTime(lastEditedFromBufferAdjusted, now); #endif } - encodedUpdateDelta = updateDeltaCoder; // determine true length - dataAt += encodedUpdateDelta.size(); - bytesRead += encodedUpdateDelta.size(); - - // Newer bitstreams will have a last simulated and a last updated value - if (args.bitstreamVersion >= VERSION_ENTITIES_HAS_LAST_SIMULATED_TIME) { - // last simulated is stored as ByteCountCoded delta from lastEdited - QByteArray encodedSimulatedDelta = originalDataBuffer.mid(bytesRead); // maximum possible size - ByteCountCoded simulatedDeltaCoder = encodedSimulatedDelta; - quint64 simulatedDelta = simulatedDeltaCoder; + encodedSimulatedDelta = simulatedDeltaCoder; // determine true length + dataAt += encodedSimulatedDelta.size(); + bytesRead += encodedSimulatedDelta.size(); + } + + #ifdef WANT_DEBUG + if (overwriteLocalData) { + qCDebug(entities) << "EntityItem::readEntityDataFromBuffer()... changed entity:" << getEntityItemID(); + qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); + qCDebug(entities) << " getLastSimulated:" << debugTime(getLastSimulated(), now); + qCDebug(entities) << " getLastUpdated:" << debugTime(getLastUpdated(), now); + } + #endif + + + // Property Flags + QByteArray encodedPropertyFlags = originalDataBuffer.mid(bytesRead); // maximum possible size + EntityPropertyFlags propertyFlags = encodedPropertyFlags; + dataAt += propertyFlags.getEncodedLength(); + bytesRead += propertyFlags.getEncodedLength(); + bool useMeters = (args.bitstreamVersion >= VERSION_ENTITIES_USE_METERS_AND_RADIANS); + if (useMeters) { + READ_ENTITY_PROPERTY(PROP_POSITION, glm::vec3, updatePosition); + } else { + READ_ENTITY_PROPERTY(PROP_POSITION, glm::vec3, updatePositionInDomainUnits); + } + + // Old bitstreams had PROP_RADIUS, new bitstreams have PROP_DIMENSIONS + if (args.bitstreamVersion < VERSION_ENTITIES_SUPPORT_DIMENSIONS) { + if (propertyFlags.getHasProperty(PROP_RADIUS)) { + float fromBuffer; + memcpy(&fromBuffer, dataAt, sizeof(fromBuffer)); + dataAt += sizeof(fromBuffer); + bytesRead += sizeof(fromBuffer); if (overwriteLocalData) { - _lastSimulated = lastEditedFromBufferAdjusted + simulatedDelta; // don't adjust for clock skew since we already did that - #ifdef WANT_DEBUG - qCDebug(entities) << " _lastSimulated:" << debugTime(_lastSimulated, now); - qCDebug(entities) << " _lastEdited:" << debugTime(_lastEdited, now); - qCDebug(entities) << " lastEditedFromBufferAdjusted:" << debugTime(lastEditedFromBufferAdjusted, now); - #endif + setRadius(fromBuffer); } - encodedSimulatedDelta = simulatedDeltaCoder; // determine true length - dataAt += encodedSimulatedDelta.size(); - bytesRead += encodedSimulatedDelta.size(); } - - #ifdef WANT_DEBUG - if (overwriteLocalData) { - qCDebug(entities) << "EntityItem::readEntityDataFromBuffer()... changed entity:" << getEntityItemID(); - qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); - qCDebug(entities) << " getLastSimulated:" << debugTime(getLastSimulated(), now); - qCDebug(entities) << " getLastUpdated:" << debugTime(getLastUpdated(), now); - } - #endif - - - // Property Flags - QByteArray encodedPropertyFlags = originalDataBuffer.mid(bytesRead); // maximum possible size - EntityPropertyFlags propertyFlags = encodedPropertyFlags; - dataAt += propertyFlags.getEncodedLength(); - bytesRead += propertyFlags.getEncodedLength(); - bool useMeters = (args.bitstreamVersion >= VERSION_ENTITIES_USE_METERS_AND_RADIANS); + } else { if (useMeters) { - READ_ENTITY_PROPERTY(PROP_POSITION, glm::vec3, updatePosition); + READ_ENTITY_PROPERTY(PROP_DIMENSIONS, glm::vec3, updateDimensions); } else { - READ_ENTITY_PROPERTY(PROP_POSITION, glm::vec3, updatePositionInDomainUnits); - } - - // Old bitstreams had PROP_RADIUS, new bitstreams have PROP_DIMENSIONS - if (args.bitstreamVersion < VERSION_ENTITIES_SUPPORT_DIMENSIONS) { - if (propertyFlags.getHasProperty(PROP_RADIUS)) { - float fromBuffer; - memcpy(&fromBuffer, dataAt, sizeof(fromBuffer)); - dataAt += sizeof(fromBuffer); - bytesRead += sizeof(fromBuffer); - if (overwriteLocalData) { - setRadius(fromBuffer); - } - } - } else { - if (useMeters) { - READ_ENTITY_PROPERTY(PROP_DIMENSIONS, glm::vec3, updateDimensions); - } else { - READ_ENTITY_PROPERTY(PROP_DIMENSIONS, glm::vec3, updateDimensionsInDomainUnits); - } - } - - READ_ENTITY_PROPERTY(PROP_ROTATION, glm::quat, updateRotation); - READ_ENTITY_PROPERTY(PROP_DENSITY, float, updateDensity); - if (useMeters) { - READ_ENTITY_PROPERTY(PROP_VELOCITY, glm::vec3, updateVelocity); - READ_ENTITY_PROPERTY(PROP_GRAVITY, glm::vec3, updateGravity); - } else { - READ_ENTITY_PROPERTY(PROP_VELOCITY, glm::vec3, updateVelocityInDomainUnits); - READ_ENTITY_PROPERTY(PROP_GRAVITY, glm::vec3, updateGravityInDomainUnits); - } - if (args.bitstreamVersion >= VERSION_ENTITIES_HAVE_ACCELERATION) { - READ_ENTITY_PROPERTY(PROP_ACCELERATION, glm::vec3, setAcceleration); - } - - READ_ENTITY_PROPERTY(PROP_DAMPING, float, updateDamping); - READ_ENTITY_PROPERTY(PROP_RESTITUTION, float, updateRestitution); - READ_ENTITY_PROPERTY(PROP_FRICTION, float, updateFriction); - READ_ENTITY_PROPERTY(PROP_LIFETIME, float, updateLifetime); - READ_ENTITY_PROPERTY(PROP_SCRIPT, QString, setScript); - READ_ENTITY_PROPERTY(PROP_REGISTRATION_POINT, glm::vec3, setRegistrationPoint); - if (useMeters) { - READ_ENTITY_PROPERTY(PROP_ANGULAR_VELOCITY, glm::vec3, updateAngularVelocity); - } else { - READ_ENTITY_PROPERTY(PROP_ANGULAR_VELOCITY, glm::vec3, updateAngularVelocityInDegrees); - } - READ_ENTITY_PROPERTY(PROP_ANGULAR_DAMPING, float, updateAngularDamping); - READ_ENTITY_PROPERTY(PROP_VISIBLE, bool, setVisible); - READ_ENTITY_PROPERTY(PROP_IGNORE_FOR_COLLISIONS, bool, updateIgnoreForCollisions); - READ_ENTITY_PROPERTY(PROP_COLLISIONS_WILL_MOVE, bool, updateCollisionsWillMove); - READ_ENTITY_PROPERTY(PROP_LOCKED, bool, setLocked); - READ_ENTITY_PROPERTY(PROP_USER_DATA, QString, setUserData); - - if (args.bitstreamVersion >= VERSION_ENTITIES_HAVE_ACCELERATION) { - READ_ENTITY_PROPERTY(PROP_SIMULATOR_ID, QUuid, updateSimulatorID); - } - - if (args.bitstreamVersion >= VERSION_ENTITIES_HAS_MARKETPLACE_ID) { - READ_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, QString, setMarketplaceID); - } - - READ_ENTITY_PROPERTY(PROP_NAME, QString, setName); - READ_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, QString, setCollisionSoundURL); - bytesRead += readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args, propertyFlags, overwriteLocalData); - - //////////////////////////////////// - // WARNING: Do not add stream content here after the subclass. Always add it before the subclass - // - // NOTE: we had a bad version of the stream that we added stream data after the subclass. We can attempt to recover - // by doing this parsing here... but it's not likely going to fully recover the content. - // - // TODO: Remove this conde once we've sufficiently migrated content past this damaged version - if (args.bitstreamVersion == VERSION_ENTITIES_HAS_MARKETPLACE_ID_DAMAGED) { - READ_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, QString, setMarketplaceID); - } - - if (overwriteLocalData && (getDirtyFlags() & (EntityItem::DIRTY_TRANSFORM | EntityItem::DIRTY_VELOCITIES))) { - // NOTE: This code is attempting to "repair" the old data we just got from the server to make it more - // closely match where the entities should be if they'd stepped forward in time to "now". The server - // is sending us data with a known "last simulated" time. That time is likely in the past, and therefore - // this "new" data is actually slightly out of date. We calculate the time we need to skip forward and - // use our simulation helper routine to get a best estimate of where the entity should be. - const float MIN_TIME_SKIP = 0.0f; - const float MAX_TIME_SKIP = 1.0f; // in seconds - float skipTimeForward = glm::clamp((float)(now - _lastSimulated) / (float)(USECS_PER_SECOND), - MIN_TIME_SKIP, MAX_TIME_SKIP); - if (skipTimeForward > 0.0f) { - #ifdef WANT_DEBUG - qCDebug(entities) << "skipTimeForward:" << skipTimeForward; - #endif - - // we want to extrapolate the motion forward to compensate for packet travel time, but - // we don't want the side effect of flag setting. - simulateKinematicMotion(skipTimeForward, false); - } - _lastSimulated = now; + READ_ENTITY_PROPERTY(PROP_DIMENSIONS, glm::vec3, updateDimensionsInDomainUnits); } } + READ_ENTITY_PROPERTY(PROP_ROTATION, glm::quat, updateRotation); + READ_ENTITY_PROPERTY(PROP_DENSITY, float, updateDensity); + if (useMeters) { + READ_ENTITY_PROPERTY(PROP_VELOCITY, glm::vec3, updateVelocity); + READ_ENTITY_PROPERTY(PROP_GRAVITY, glm::vec3, updateGravity); + } else { + READ_ENTITY_PROPERTY(PROP_VELOCITY, glm::vec3, updateVelocityInDomainUnits); + READ_ENTITY_PROPERTY(PROP_GRAVITY, glm::vec3, updateGravityInDomainUnits); + } + if (args.bitstreamVersion >= VERSION_ENTITIES_HAVE_ACCELERATION) { + READ_ENTITY_PROPERTY(PROP_ACCELERATION, glm::vec3, setAcceleration); + } + + READ_ENTITY_PROPERTY(PROP_DAMPING, float, updateDamping); + READ_ENTITY_PROPERTY(PROP_RESTITUTION, float, updateRestitution); + READ_ENTITY_PROPERTY(PROP_FRICTION, float, updateFriction); + READ_ENTITY_PROPERTY(PROP_LIFETIME, float, updateLifetime); + READ_ENTITY_PROPERTY(PROP_SCRIPT, QString, setScript); + READ_ENTITY_PROPERTY(PROP_REGISTRATION_POINT, glm::vec3, setRegistrationPoint); + if (useMeters) { + READ_ENTITY_PROPERTY(PROP_ANGULAR_VELOCITY, glm::vec3, updateAngularVelocity); + } else { + READ_ENTITY_PROPERTY(PROP_ANGULAR_VELOCITY, glm::vec3, updateAngularVelocityInDegrees); + } + READ_ENTITY_PROPERTY(PROP_ANGULAR_DAMPING, float, updateAngularDamping); + READ_ENTITY_PROPERTY(PROP_VISIBLE, bool, setVisible); + READ_ENTITY_PROPERTY(PROP_IGNORE_FOR_COLLISIONS, bool, updateIgnoreForCollisions); + READ_ENTITY_PROPERTY(PROP_COLLISIONS_WILL_MOVE, bool, updateCollisionsWillMove); + READ_ENTITY_PROPERTY(PROP_LOCKED, bool, setLocked); + READ_ENTITY_PROPERTY(PROP_USER_DATA, QString, setUserData); + + if (args.bitstreamVersion >= VERSION_ENTITIES_HAVE_ACCELERATION) { + // we always accept the server's notion of simulatorID, so we fake overwriteLocalData as true + // before we try to READ_ENTITY_PROPERTY it + bool temp = overwriteLocalData; + overwriteLocalData = true; + READ_ENTITY_PROPERTY(PROP_SIMULATOR_ID, QUuid, updateSimulatorID); + overwriteLocalData = temp; + } + + if (args.bitstreamVersion >= VERSION_ENTITIES_HAS_MARKETPLACE_ID) { + READ_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, QString, setMarketplaceID); + } + + READ_ENTITY_PROPERTY(PROP_NAME, QString, setName); + READ_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, QString, setCollisionSoundURL); + bytesRead += readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args, propertyFlags, overwriteLocalData); + + //////////////////////////////////// + // WARNING: Do not add stream content here after the subclass. Always add it before the subclass + // + // NOTE: we had a bad version of the stream that we added stream data after the subclass. We can attempt to recover + // by doing this parsing here... but it's not likely going to fully recover the content. + // + // TODO: Remove this conde once we've sufficiently migrated content past this damaged version + if (args.bitstreamVersion == VERSION_ENTITIES_HAS_MARKETPLACE_ID_DAMAGED) { + READ_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, QString, setMarketplaceID); + } + + if (overwriteLocalData && (getDirtyFlags() & (EntityItem::DIRTY_TRANSFORM | EntityItem::DIRTY_VELOCITIES))) { + // NOTE: This code is attempting to "repair" the old data we just got from the server to make it more + // closely match where the entities should be if they'd stepped forward in time to "now". The server + // is sending us data with a known "last simulated" time. That time is likely in the past, and therefore + // this "new" data is actually slightly out of date. We calculate the time we need to skip forward and + // use our simulation helper routine to get a best estimate of where the entity should be. + const float MIN_TIME_SKIP = 0.0f; + const float MAX_TIME_SKIP = 1.0f; // in seconds + float skipTimeForward = glm::clamp((float)(now - _lastSimulated) / (float)(USECS_PER_SECOND), + MIN_TIME_SKIP, MAX_TIME_SKIP); + if (skipTimeForward > 0.0f) { + #ifdef WANT_DEBUG + qCDebug(entities) << "skipTimeForward:" << skipTimeForward; + #endif + + // we want to extrapolate the motion forward to compensate for packet travel time, but + // we don't want the side effect of flag setting. + simulateKinematicMotion(skipTimeForward, false); + } + _lastSimulated = now; + } auto nodeList = DependencyManager::get(); const QUuid& myNodeID = nodeList->getSessionUUID(); - if (_simulatorID == myNodeID && !_simulatorID.isNull()) { - // the packet that produced this bitstream originally came from physics simulations performed by - // this node, so our version has to be newer than what the packet contained. + if (overwriteLocalData && _simulatorID == myNodeID && !_simulatorID.isNull()) { + // we own the simulation, so we keep our transform+velocities and remove any related dirty flags + // rather than accept the values in the packet setPosition(savePosition); setRotation(saveRotation); - // _velocity = saveVelocity; - // _angularVelocity = saveAngularVelocity; - // _gravity = saveGravity; - // _acceleration = saveAcceleration; + _velocity = saveVelocity; + _angularVelocity = saveAngularVelocity; + _dirtyFlags &= ~(EntityItem::DIRTY_TRANSFORM | EntityItem::DIRTY_VELOCITIES); } return bytesRead; @@ -949,40 +949,25 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(name, setName); if (somethingChanged) { - somethingChangedNotification(); // notify derived classes that something has changed uint64_t now = usecTimestampNow(); #ifdef WANT_DEBUG int elapsed = now - getLastEdited(); qCDebug(entities) << "EntityItem::setProperties() AFTER update... edited AGO=" << elapsed << "now=" << now << " getLastEdited()=" << getLastEdited(); #endif - if (_created != UNKNOWN_CREATED_TIME) { - setLastEdited(now); + setLastEdited(now); + somethingChangedNotification(); // notify derived classes that something has changed + if (_created == UNKNOWN_CREATED_TIME) { + _created = now; } if (getDirtyFlags() & (EntityItem::DIRTY_TRANSFORM | EntityItem::DIRTY_VELOCITIES)) { - // TODO: Andrew & Brad to discuss. Is this correct? Maybe it is. Need to think through all cases. + // anything that sets the transform or velocity must update _lastSimulated which is used + // for kinematic extrapolation (e.g. we want to extrapolate forward from this moment + // when position and/or velocity was changed). _lastSimulated = now; } } - // timestamps - quint64 timestamp = properties.getCreated(); - if (_created == UNKNOWN_CREATED_TIME && timestamp != UNKNOWN_CREATED_TIME) { - quint64 now = usecTimestampNow(); - if (timestamp > now) { - timestamp = now; - } - _created = timestamp; - - timestamp = properties.getLastEdited(); - if (timestamp > now) { - timestamp = now; - } else if (timestamp < _created) { - timestamp = _created; - } - _lastEdited = timestamp; - } - return somethingChanged; } diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index e27c5ec02e..5c8a5b63a1 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -490,19 +490,21 @@ void ParticleEffectEntityItem::stepSimulation(float deltaTime) { } void ParticleEffectEntityItem::setMaxParticles(quint32 maxParticles) { - _maxParticles = maxParticles; + if (_maxParticles != maxParticles) { + _maxParticles = maxParticles; - // TODO: try to do something smart here and preserve the state of existing particles. + // TODO: try to do something smart here and preserve the state of existing particles. - // resize vectors - _particleLifetimes.resize(_maxParticles); - _particlePositions.resize(_maxParticles); - _particleVelocities.resize(_maxParticles); + // resize vectors + _particleLifetimes.resize(_maxParticles); + _particlePositions.resize(_maxParticles); + _particleVelocities.resize(_maxParticles); - // effectivly clear all particles and start emitting new ones from scratch. - _particleHeadIndex = 0; - _particleTailIndex = 0; - _timeUntilNextEmit = 0.0f; + // effectivly clear all particles and start emitting new ones from scratch. + _particleHeadIndex = 0; + _particleTailIndex = 0; + _timeUntilNextEmit = 0.0f; + } } // because particles are in a ring buffer, this isn't trivial diff --git a/libraries/networking/src/Assignment.cpp b/libraries/networking/src/Assignment.cpp index 944041730e..a4fa246c93 100644 --- a/libraries/networking/src/Assignment.cpp +++ b/libraries/networking/src/Assignment.cpp @@ -32,12 +32,6 @@ Assignment::Type Assignment::typeForNodeType(NodeType_t nodeType) { } } -#ifdef WIN32 -//warning C4351: new behavior: elements of array 'Assignment::_payload' will be default initialized -// We're disabling this warning because the new behavior which is to initialize the array with 0 is acceptable to us. -#pragma warning(disable:4351) -#endif - Assignment::Assignment() : _uuid(), _command(Assignment::RequestCommand), diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 6c4b3dad75..9a24aabb34 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -132,7 +132,6 @@ void EntityMotionState::getWorldTransform(btTransform& worldTrans) const { uint32_t thisStep = ObjectMotionState::getWorldSimulationStep(); float dt = (thisStep - _lastKinematicStep) * PHYSICS_ENGINE_FIXED_SUBSTEP; _entity->simulateKinematicMotion(dt); - _entity->setLastSimulated(usecTimestampNow()); // bypass const-ness so we can remember the step const_cast(this)->_lastKinematicStep = thisStep; @@ -401,13 +400,12 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, const Q properties.setAcceleration(_serverAcceleration); properties.setAngularVelocity(_serverAngularVelocity); - // we only update lastEdited when we're sending new physics data - quint64 lastSimulated = _entity->getLastSimulated(); - _entity->setLastEdited(lastSimulated); - properties.setLastEdited(lastSimulated); + // set the LastEdited of the properties but NOT the entity itself + quint64 now = usecTimestampNow(); + properties.setLastEdited(now); #ifdef WANT_DEBUG - quint64 now = usecTimestampNow(); + quint64 lastSimulated = _entity->getLastSimulated(); qCDebug(physics) << "EntityMotionState::sendUpdate()"; qCDebug(physics) << " EntityItemId:" << _entity->getEntityItemID() << "---------------------------------------------"; diff --git a/libraries/physics/src/MeshInfo.cpp b/libraries/physics/src/MeshInfo.cpp deleted file mode 100644 index 29ddc74a98..0000000000 --- a/libraries/physics/src/MeshInfo.cpp +++ /dev/null @@ -1,150 +0,0 @@ -// -// MeshInfo.cpp -// libraries/physics/src -// -// Created by Virendra Singh 2015.02.28 -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "MeshInfo.h" -#include -using namespace meshinfo; - -//class to compute volume, mass, center of mass, and inertia tensor of a mesh. -//origin is the default reference point for generating the tetrahedron from each triangle of the mesh. - -MeshInfo::MeshInfo(vector *vertices, vector *triangles) :\ - _vertices(vertices), - _centerOfMass(Vertex(0.0, 0.0, 0.0)), - _triangles(triangles) -{ - -} - -MeshInfo::~MeshInfo(){ - - _vertices = NULL; - _triangles = NULL; - -} - -inline float MeshInfo::getVolume(const Vertex p1, const Vertex p2, const Vertex p3, const Vertex p4) const{ - glm::mat4 tet = { glm::vec4(p1.x, p2.x, p3.x, p4.x), glm::vec4(p1.y, p2.y, p3.y, p4.y), glm::vec4(p1.z, p2.z, p3.z, p4.z), - glm::vec4(1.0f, 1.0f, 1.0f, 1.0f) }; - return glm::determinant(tet) / 6.0f; -} - -Vertex MeshInfo::getMeshCentroid() const{ - return _centerOfMass; -} - -vector MeshInfo::computeMassProperties(){ - vector volumeAndInertia = { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }; - Vertex origin(0.0, 0.0, 0.0); - glm::mat3 identity; - float meshVolume = 0.0f; - glm::mat3 globalInertiaTensors(0.0); - - for (unsigned int i = 0; i < _triangles->size(); i += 3){ - Vertex p1 = _vertices->at(_triangles->at(i)); - Vertex p2 = _vertices->at(_triangles->at(i + 1)); - Vertex p3 = _vertices->at(_triangles->at(i + 2)); - float volume = getVolume(p1, p2, p3, origin); - Vertex com = 0.25f * (p1 + p2 + p3); - meshVolume += volume; - _centerOfMass += com * volume; - - //centroid is used for calculating inertia tensor relative to center of mass. - // translate the tetrahedron to its center of mass using P = P - centroid - Vertex p0 = origin - com; - p1 = p1 - com; - p2 = p2 - com; - p3 = p3 - com; - - //Calculate inertia tensor based on Tonon's Formulae given in the paper mentioned below. - //http://docsdrive.com/pdfs/sciencepublications/jmssp/2005/8-11.pdf - //Explicit exact formulas for the 3-D tetrahedron inertia tensor in terms of its vertex coordinates - F.Tonon - - float i11 = (volume * 0.1f) * ( - p0.y*p0.y + p0.y*p1.y + p0.y*p2.y + p0.y*p3.y + - p1.y*p1.y + p1.y*p2.y + p1.y*p3.y + - p2.y*p2.y + p2.y*p3.y + - p3.y*p3.y + - p0.z*p0.z + p0.z*p1.z + p0.z*p2.z + p0.z*p3.z + - p1.z*p1.z + p1.z*p2.z + p1.z*p3.z + - p2.z*p2.z + p2.z*p3.z + - p3.z*p3.z); - - float i22 = (volume * 0.1f) * ( - p0.x*p0.x + p0.x*p1.x + p0.x*p2.x + p0.x*p3.x + - p1.x*p1.x + p1.x*p2.x + p1.x*p3.x + - p2.x*p2.x + p2.x*p3.x + - p3.x*p3.x + - p0.z*p0.z + p0.z*p1.z + p0.z*p2.z + p0.z*p3.z + - p1.z*p1.z + p1.z*p2.z + p1.z*p3.z + - p2.z*p2.z + p2.z*p3.z + - p3.z*p3.z); - - float i33 = (volume * 0.1f) * ( - p0.x*p0.x + p0.x*p1.x + p0.x*p2.x + p0.x*p3.x + - p1.x*p1.x + p1.x*p2.x + p1.x*p3.x + - p2.x*p2.x + p2.x*p3.x + - p3.x*p3.x + - p0.y*p0.y + p0.y*p1.y + p0.y*p2.y + p0.y*p3.y + - p1.y*p1.y + p1.y*p2.y + p1.y*p3.y + - p2.y*p2.y + p2.y*p3.y + - p3.y*p3.y); - - float i23 = -(volume * 0.05f) * (2.0f * (p0.y*p0.z + p1.y*p1.z + p2.y*p2.z + p3.y*p3.z) + - p0.y*p1.z + p0.y*p2.z + p0.y*p3.z + - p1.y*p0.z + p1.y*p2.z + p1.y*p3.z + - p2.y*p0.z + p2.y*p1.z + p2.y*p3.z + - p3.y*p0.z + p3.y*p1.z + p3.y*p2.z); - - float i21 = -(volume * 0.05f) * (2.0f * (p0.x*p0.z + p1.x*p1.z + p2.x*p2.z + p3.x*p3.z) + - p0.x*p1.z + p0.x*p2.z + p0.x*p3.z + - p1.x*p0.z + p1.x*p2.z + p1.x*p3.z + - p2.x*p0.z + p2.x*p1.z + p2.x*p3.z + - p3.x*p0.z + p3.x*p1.z + p3.x*p2.z); - - float i31 = -(volume * 0.05f) * (2.0f * (p0.x*p0.y + p1.x*p1.y + p2.x*p2.y + p3.x*p3.y) + - p0.x*p1.y + p0.x*p2.y + p0.x*p3.y + - p1.x*p0.y + p1.x*p2.y + p1.x*p3.y + - p2.x*p0.y + p2.x*p1.y + p2.x*p3.y + - p3.x*p0.y + p3.x*p1.y + p3.x*p2.y); - - //3x3 of local inertia tensors of each tetrahedron. Inertia tensors are the diagonal elements - // | I11 -I12 -I13 | - // I = | -I21 I22 -I23 | - // | -I31 -I32 I33 | - glm::mat3 localInertiaTensors = { Vertex(i11, i21, i31), Vertex(i21, i22, i23), - Vertex(i31, i23, i33) }; - - //Translate the inertia tensors from center of mass to origin - //Parallel axis theorem J = I * m[(R.R)*Identity - RxR] where x is outer cross product - globalInertiaTensors += localInertiaTensors + volume * ((glm::dot(com, com) * identity) - - glm::outerProduct(com, com)); - } - - //Translate accumulated center of mass from each tetrahedron to mesh's center of mass using parallel axis theorem - if (meshVolume == 0){ - return volumeAndInertia; - } - _centerOfMass = (_centerOfMass / meshVolume); - - //Translate the inertia tensors from origin to mesh's center of mass. - globalInertiaTensors = globalInertiaTensors - meshVolume * ((glm::dot(_centerOfMass, _centerOfMass) * - identity) - glm::outerProduct(_centerOfMass, _centerOfMass)); - - volumeAndInertia[0] = meshVolume; - volumeAndInertia[1] = globalInertiaTensors[0][0]; //i11 - volumeAndInertia[2] = globalInertiaTensors[1][1]; //i22 - volumeAndInertia[3] = globalInertiaTensors[2][2]; //i33 - volumeAndInertia[4] = -globalInertiaTensors[2][1]; //i23 or i32 - volumeAndInertia[5] = -globalInertiaTensors[1][0]; //i21 or i12 - volumeAndInertia[6] = -globalInertiaTensors[2][0]; //i13 or i31 - return volumeAndInertia; -} \ No newline at end of file diff --git a/libraries/physics/src/MeshInfo.h b/libraries/physics/src/MeshInfo.h deleted file mode 100644 index 67dbc8b0d6..0000000000 --- a/libraries/physics/src/MeshInfo.h +++ /dev/null @@ -1,33 +0,0 @@ -// -// MeshInfo.h -// libraries/physics/src -// -// Created by Virendra Singh 2015.02.28 -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#ifndef hifi_MeshInfo_h -#define hifi_MeshInfo_h -#include -#include -#include -using namespace std; -namespace meshinfo{ - typedef glm::vec3 Vertex; - class MeshInfo{ - private: - inline float getVolume(const Vertex p1, const Vertex p2, const Vertex p3, const Vertex p4) const; - vector computeVolumeAndInertia(const Vertex p1, const Vertex p2, const Vertex p3, const Vertex p4) const; - public: - vector *_vertices; - Vertex _centerOfMass; - vector *_triangles; - MeshInfo(vector *vertices, vector *triangles); - ~MeshInfo(); - Vertex getMeshCentroid() const; - vector computeMassProperties(); - }; -} -#endif // hifi_MeshInfo_h \ No newline at end of file diff --git a/libraries/physics/src/MeshMassProperties.cpp b/libraries/physics/src/MeshMassProperties.cpp new file mode 100644 index 0000000000..ee2945422d --- /dev/null +++ b/libraries/physics/src/MeshMassProperties.cpp @@ -0,0 +1,321 @@ +// +// MeshMassProperties.cpp +// libraries/physics/src +// +// Created by Andrew Meadows 2015.05.25 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include + +#include "MeshMassProperties.h" + +// this method is included for unit test verification +void computeBoxInertia(btScalar mass, const btVector3& diagonal, btMatrix3x3& inertia) { + // formula for box inertia tensor: + // + // | y^2 + z^2 0 0 | + // | | + // inertia = M/12 * | 0 z^2 + x^2 0 | + // | | + // | 0 0 x^2 + y^2 | + // + + mass = mass / btScalar(12.0f); + btScalar x = diagonal[0]; + x = mass * x * x; + btScalar y = diagonal[1]; + y = mass * y * y; + btScalar z = diagonal[2]; + z = mass * z * z; + inertia.setIdentity(); + inertia[0][0] = y + z; + inertia[1][1] = z + x; + inertia[2][2] = x + y; +} + +void computeTetrahedronInertia(btScalar mass, btVector3* points, btMatrix3x3& inertia) { + // Computes the inertia tensor of a tetrahedron about its center of mass. + // The tetrahedron is defined by array of four points in its center of mass frame. + // + // The analytic formulas were obtained from Tonon's paper: + // http://docsdrive.com/pdfs/sciencepublications/jmssp/2005/8-11.pdf + // http://thescipub.com/PDF/jmssp.2005.8.11.pdf + // + // The inertia tensor has the following form: + // + // | a f e | + // | | + // inertia = | f b d | + // | | + // | e d c | + + const btVector3& p0 = points[0]; + const btVector3& p1 = points[1]; + const btVector3& p2 = points[2]; + const btVector3& p3 = points[3]; + + for (uint32_t i = 0; i < 3; ++i ) { + uint32_t j = (i + 1) % 3; + uint32_t k = (j + 1) % 3; + + // compute diagonal + inertia[i][i] = mass * btScalar(0.1f) * + ( p0[j] * (p0[j] + p1[j] + p2[j] + p3[j]) + + p1[j] * (p1[j] + p2[j] + p3[j]) + + p2[j] * (p2[j] + p3[j]) + + p3[j] * p3[j] + + p0[k] * (p0[k] + p1[k] + p2[k] + p3[k]) + + p1[k] * (p1[k] + p2[k] + p3[k]) + + p2[k] * (p2[k] + p3[k]) + + p3[k] * p3[k] ); + + // compute off-diagonals + inertia[j][k] = inertia[k][j] = - mass * btScalar(0.05f) * + ( btScalar(2.0f) * ( p0[j] * p0[k] + p1[j] * p1[k] + p2[j] * p2[k] + p3[j] * p3[k] ) + + p0[j] * (p1[k] + p2[k] + p3[k]) + + p1[j] * (p0[k] + p2[k] + p3[k]) + + p2[j] * (p0[k] + p1[k] + p3[k]) + + p3[j] * (p0[k] + p1[k] + p2[k]) ); + } +} + +// helper function +void computePointInertia(const btVector3& point, btScalar mass, btMatrix3x3& inertia) { + btScalar distanceSquared = point.length2(); + if (distanceSquared > 0.0f) { + for (uint32_t i = 0; i < 3; ++i) { + btScalar pointi = point[i]; + inertia[i][i] = mass * (distanceSquared - (pointi * pointi)); + for (uint32_t j = i + 1; j < 3; ++j) { + btScalar offDiagonal = - mass * pointi * point[j]; + inertia[i][j] = offDiagonal; + inertia[j][i] = offDiagonal; + } + } + } +} + +// this method is included for unit test verification +void computeTetrahedronInertiaByBruteForce(btVector3* points, btMatrix3x3& inertia) { + // Computes the approximate inertia tensor of a tetrahedron (about frame's origin) + // by integration over the "point" masses. This is numerically expensive so it may + // take a while to complete. + + VectorOfIndices triangles = { + 0, 2, 1, + 0, 3, 2, + 0, 1, 3, + 1, 2, 3 }; + + for (int i = 0; i < 3; ++i) { + inertia[i].setZero(); + } + + // compute normals + btVector3 center = btScalar(0.25f) * (points[0] + points[1] + points[2] + points[3]); + btVector3 normals[4]; + btVector3 pointsOnPlane[4]; + for (int i = 0; i < 4; ++i) { + int t = 3 * i; + btVector3& p0 = points[triangles[t]]; + btVector3& p1 = points[triangles[t + 1]]; + btVector3& p2 = points[triangles[t + 2]]; + normals[i] = ((p1 - p0).cross(p2 - p1)).normalized(); + // make sure normal points away from center + if (normals[i].dot(p0 - center) < btScalar(0.0f)) { + normals[i] *= btScalar(-1.0f); + } + pointsOnPlane[i] = p0; + } + + // compute bounds of integration + btVector3 boxMax = points[0]; + btVector3 boxMin = points[0]; + for (int i = 1; i < 4; ++i) { + for(int j = 0; j < 3; ++j) { + if (points[i][j] > boxMax[j]) { + boxMax[j] = points[i][j]; + } + if (points[i][j] < boxMin[j]) { + boxMin[j] = points[i][j]; + } + } + } + + // compute step size + btVector3 diagonal = boxMax - boxMin; + btScalar maxDimension = diagonal[0]; + if (diagonal[1] > maxDimension) { + maxDimension = diagonal[1]; + } + if (diagonal[2] > maxDimension) { + maxDimension = diagonal[2]; + } + btScalar resolutionOfIntegration = btScalar(400.0f); + btScalar delta = maxDimension / resolutionOfIntegration; + btScalar deltaVolume = delta * delta * delta; + + // integrate over three dimensions + btMatrix3x3 deltaInertia; + btScalar XX = boxMax[0]; + btScalar YY = boxMax[1]; + btScalar ZZ = boxMax[2]; + btScalar x = boxMin[0]; + while(x < XX) { + btScalar y = boxMin[1]; + while (y < YY) { + btScalar z = boxMin[2]; + while (z < ZZ) { + btVector3 p(x, y, z); + // the point is inside the shape if it is behind all face planes + bool pointInside = true; + for (int i = 0; i < 4; ++i) { + if ((p - pointsOnPlane[i]).dot(normals[i]) > btScalar(0.0f)) { + pointInside = false; + break; + } + } + if (pointInside) { + // this point contributes to the total + computePointInertia(p, deltaVolume, deltaInertia); + inertia += deltaInertia; + } + z += delta; + } + y += delta; + } + x += delta; + } +} + +btScalar computeTetrahedronVolume(btVector3* points) { + // Assumes triangle {1, 2, 3} is wound according to the right-hand-rule. + // NOTE: volume may be negative, in which case the tetrahedron contributes negatively to totals + // volume = (face_area * face_normal).dot(face_to_far_point) / 3.0 + // (face_area * face_normal) = side0.cross(side1) / 2.0 + return ((points[2] - points[1]).cross(points[3] - points[2])).dot(points[3] - points[0]) / btScalar(6.0f); +} + +void applyParallelAxisTheorem(btMatrix3x3& inertia, const btVector3& shift, btScalar mass) { + // Parallel Axis Theorem says: + // + // Ishifted = Icm + M * [ (R*R)E - R(x)R ] + // + // where R*R = inside product + // R(x)R = outside product + // E = identity matrix + + btScalar distanceSquared = shift.length2(); + if (distanceSquared > btScalar(0.0f)) { + for (uint32_t i = 0; i < 3; ++i) { + btScalar shifti = shift[i]; + inertia[i][i] += mass * (distanceSquared - (shifti * shifti)); + for (uint32_t j = i + 1; j < 3; ++j) { + btScalar offDiagonal = mass * shifti * shift[j]; + inertia[i][j] -= offDiagonal; + inertia[j][i] -= offDiagonal; + } + } + } +} + +// helper function +void applyInverseParallelAxisTheorem(btMatrix3x3& inertia, const btVector3& shift, btScalar mass) { + // Parallel Axis Theorem says: + // + // Ishifted = Icm + M * [ (R*R)E - R(x)R ] + // + // So the inverse would be: + // + // Icm = Ishifted - M * [ (R*R)E - R(x)R ] + + btScalar distanceSquared = shift.length2(); + if (distanceSquared > btScalar(0.0f)) { + for (uint32_t i = 0; i < 3; ++i) { + btScalar shifti = shift[i]; + inertia[i][i] -= mass * (distanceSquared - (shifti * shifti)); + for (uint32_t j = i + 1; j < 3; ++j) { + btScalar offDiagonal = mass * shifti * shift[j]; + inertia[i][j] += offDiagonal; + inertia[j][i] += offDiagonal; + } + } + } +} + +MeshMassProperties::MeshMassProperties(const VectorOfPoints& points, const VectorOfIndices& triangleIndices) { + computeMassProperties(points, triangleIndices); +} + +void MeshMassProperties::computeMassProperties(const VectorOfPoints& points, const VectorOfIndices& triangleIndices) { + // We process the mesh one triangle at a time. Each triangle defines a tetrahedron + // relative to some local point p0 (which we chose to be the local origin for convenience). + // Each tetrahedron contributes to the three totals: volume, centerOfMass, and inertiaTensor. + // + // We assume the mesh triangles are wound using the right-hand-rule, such that the + // triangle's points circle counter-clockwise about its face normal. + // + + // initialize the totals + _volume = btScalar(0.0f); + btVector3 weightedCenter; + weightedCenter.setZero(); + for (uint32_t i = 0; i < 3; ++i) { + _inertia[i].setZero(); + } + + // create some variables to hold temporary results + uint32_t numPoints = points.size(); + const btVector3 p0(0.0f, 0.0f, 0.0f); + btMatrix3x3 tetraInertia; + btMatrix3x3 doubleDebugInertia; + btVector3 tetraPoints[4]; + btVector3 center; + + // loop over triangles + uint32_t numTriangles = triangleIndices.size() / 3; + for (uint32_t i = 0; i < numTriangles; ++i) { + uint32_t t = 3 * i; + assert(triangleIndices[t] < numPoints); + assert(triangleIndices[t + 1] < numPoints); + assert(triangleIndices[t + 2] < numPoints); + + // extract raw vertices + tetraPoints[0] = p0; + tetraPoints[1] = points[triangleIndices[t]]; + tetraPoints[2] = points[triangleIndices[t + 1]]; + tetraPoints[3] = points[triangleIndices[t + 2]]; + + // compute volume + btScalar volume = computeTetrahedronVolume(tetraPoints); + + // compute center + // NOTE: since tetraPoints[0] is the origin, we don't include it in the sum + center = btScalar(0.25f) * (tetraPoints[1] + tetraPoints[2] + tetraPoints[3]); + + // shift vertices so that center of mass is at origin + tetraPoints[0] -= center; + tetraPoints[1] -= center; + tetraPoints[2] -= center; + tetraPoints[3] -= center; + + // compute inertia tensor then shift it to origin-frame + computeTetrahedronInertia(volume, tetraPoints, tetraInertia); + applyParallelAxisTheorem(tetraInertia, center, volume); + + // tally results + weightedCenter += volume * center; + _volume += volume; + _inertia += tetraInertia; + } + + _centerOfMass = weightedCenter / _volume; + + applyInverseParallelAxisTheorem(_inertia, _centerOfMass, _volume); +} + diff --git a/libraries/physics/src/MeshMassProperties.h b/libraries/physics/src/MeshMassProperties.h new file mode 100644 index 0000000000..208955d3a6 --- /dev/null +++ b/libraries/physics/src/MeshMassProperties.h @@ -0,0 +1,61 @@ +// +// MeshMassProperties.h +// libraries/physics/src +// +// Created by Andrew Meadows 2015.05.25 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_MeshMassProperties_h +#define hifi_MeshMassProperties_h + +#include +#include + +#include + +typedef std::vector VectorOfPoints; +typedef std::vector VectorOfIndices; + +#define EXPOSE_HELPER_FUNCTIONS_FOR_UNIT_TEST +#ifdef EXPOSE_HELPER_FUNCTIONS_FOR_UNIT_TEST +void computeBoxInertia(btScalar mass, const btVector3& diagonal, btMatrix3x3& I); + +// mass = input mass of tetrahedron +// points = input array of four points with center of mass at origin +// I = output inertia of tetrahedron about its center of mass +void computeTetrahedronInertia(btScalar mass, btVector3* points, btMatrix3x3& I); +void computeTetrahedronInertiaByBruteForce(btVector3* points, btMatrix3x3& I); + +btScalar computeTetrahedronVolume(btVector3* points); + +void applyParallelAxisTheorem(btMatrix3x3& inertia, const btVector3& shift, btScalar mass); +#endif // EXPOSE_HELPER_FUNCTIONS_FOR_UNIT_TEST + +// Given a closed mesh with right-hand triangles a MeshMassProperties instance will compute +// its mass properties: +// +// volume +// center-of-mass +// normalized interia tensor about center of mass +// +class MeshMassProperties { +public: + + // the mass properties calculation is done in the constructor, so if the mesh is complex + // then the construction could be computationally expensiuve. + MeshMassProperties(const VectorOfPoints& points, const VectorOfIndices& triangleIndices); + + // compute the mass properties of a new mesh + void computeMassProperties(const VectorOfPoints& points, const VectorOfIndices& triangleIndices); + + // harveste the mass properties from these public data members + btScalar _volume = 1.0f; + btVector3 _centerOfMass = btVector3(0.0f, 0.0f, 0.0f); + btMatrix3x3 _inertia = btMatrix3x3(1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f); +}; + +#endif // _hifi_MeshMassProperties_h diff --git a/tests/physics/src/MeshInfoTests.cpp b/tests/physics/src/MeshInfoTests.cpp deleted file mode 100644 index 221ffa117c..0000000000 --- a/tests/physics/src/MeshInfoTests.cpp +++ /dev/null @@ -1,237 +0,0 @@ -// -// MeshInfoTests.cpp -// tests/physics/src -// -// Created by Virendra Singh on 2015.03.02 -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include - -#include "MeshInfoTests.h" -const float epsilon = 0.015f; -void MeshInfoTests::testWithTetrahedron(){ - glm::vec3 p0(8.33220, -11.86875, 0.93355); - glm::vec3 p1(0.75523, 5.00000, 16.37072); - glm::vec3 p2(52.61236, 5.00000, -5.38580); - glm::vec3 p3(2.00000, 5.00000, 3.00000); - glm::vec3 centroid(15.92492, 0.782813, 3.72962); - - //translate the tetrahedron so that its apex is on origin - glm::vec3 p11 = p1 - p0; - glm::vec3 p22 = p2 - p0; - glm::vec3 p33 = p3 - p0; - vector vertices = { p11, p22, p33 }; - vector triangles = { 0, 1, 2 }; - - float volume = 1873.233236f; - float inertia_a = 43520.33257f; - //actual should be 194711.28938f. But for some reason it becomes 194711.296875 during - //runtime due to how floating points are stored. - float inertia_b = 194711.289f; - float inertia_c = 191168.76173f; - float inertia_aa = 4417.66150f; - float inertia_bb = -46343.16662f; - float inertia_cc = 11996.20119f; - - meshinfo::MeshInfo meshinfo(&vertices,&triangles); - vector voumeAndInertia = meshinfo.computeMassProperties(); - glm::vec3 tetCenterOfMass = meshinfo.getMeshCentroid(); - - //get original center of mass - tetCenterOfMass = tetCenterOfMass + p0; - glm::vec3 diff = centroid - tetCenterOfMass; - std::cout << std::setprecision(12); - //test if centroid is correct - if (diff.x > epsilon || diff.y > epsilon || diff.z > epsilon){ - std::cout << __FILE__ << ":" << __LINE__ << " ERROR : Centroid is incorrect : Expected = " << centroid.x << " " << - centroid.y << " " << centroid.z << ", actual = " << tetCenterOfMass.x << " " << tetCenterOfMass.y << - " " << tetCenterOfMass.z << std::endl; - } - - //test if volume is correct - if (abs(volume - voumeAndInertia.at(0)) > epsilon){ - std::cout << __FILE__ << ":" << __LINE__ << " ERROR : Volume is incorrect : Expected = " << volume << " " << - ", actual = " << voumeAndInertia.at(0) << std::endl; - } - - //test if moment of inertia with respect to x axis is correct - if (abs(inertia_a - (voumeAndInertia.at(1))) > epsilon){ - std::cout << __FILE__ << ":" << __LINE__ << " ERROR : Moment of inertia with respect to x axis is incorrect : Expected = " << - inertia_a << " " << ", actual = " << voumeAndInertia.at(1) << std::endl; - } - - //test if moment of inertia with respect to y axis is correct - if (abs(inertia_b - voumeAndInertia.at(2)) > epsilon){ - std::cout << __FILE__ << ":" << __LINE__ << " ERROR : Moment of inertia with respect to y axis is incorrect : Expected = " << - inertia_b << " " << ", actual = " << (voumeAndInertia.at(2)) << std::endl; - } - - //test if moment of inertia with respect to z axis is correct - if (abs(inertia_c - (voumeAndInertia.at(3))) > epsilon){ - std::cout << __FILE__ << ":" << __LINE__ << " ERROR : Moment of inertia with respect to z axis is incorrect : Expected = " << - inertia_c << " " << ", actual = " << (voumeAndInertia.at(3)) << std::endl; - } - - //test if product of inertia with respect to x axis is correct - if (abs(inertia_aa - (voumeAndInertia.at(4))) > epsilon){ - std::cout << __FILE__ << ":" << __LINE__ << " ERROR : Product of inertia with respect to x axis is incorrect : Expected = " << - inertia_aa << " " << ", actual = " << (voumeAndInertia.at(4)) << std::endl; - } - - //test if product of inertia with respect to y axis is correct - if (abs(inertia_bb - (voumeAndInertia.at(5))) > epsilon){ - std::cout << __FILE__ << ":" << __LINE__ << " ERROR : Product of inertia with respect to y axis is incorrect : Expected = " << - inertia_bb << " " << ", actual = " << (voumeAndInertia.at(5)) << std::endl; - } - - //test if product of inertia with respect to z axis is correct - if (abs(inertia_cc - (voumeAndInertia.at(6))) > epsilon){ - std::cout << __FILE__ << ":" << __LINE__ << " ERROR : Product of inertia with respect to z axis is incorrect : Expected = " << - inertia_cc << " " << ", actual = " << (voumeAndInertia.at(6)) << std::endl; - } - -} - -void MeshInfoTests::testWithTetrahedronAsMesh(){ - glm::vec3 p0(8.33220, -11.86875, 0.93355); - glm::vec3 p1(0.75523, 5.00000, 16.37072); - glm::vec3 p2(52.61236, 5.00000, -5.38580); - glm::vec3 p3(2.00000, 5.00000, 3.00000); - glm::vec3 centroid(15.92492, 0.782813, 3.72962); - /* TODO: actually test inertia/volume calculations here - //float volume = 1873.233236f; - //runtime due to how floating points are stored. - float inertia_a = 43520.33257f; - float inertia_b = 194711.289f; - float inertia_c = 191168.76173f; - float inertia_aa = 4417.66150f; - float inertia_bb = -46343.16662f; - float inertia_cc = 11996.20119f; - */ - std::cout << std::setprecision(12); - vector vertices = { p0, p1, p2, p3 }; - vector triangles = { 0, 2, 1, 0, 3, 2, 0, 1, 3, 1, 2, 3 }; - meshinfo::MeshInfo massProp(&vertices, &triangles); - vector volumeAndInertia = massProp.computeMassProperties(); - std::cout << volumeAndInertia[0] << " " << volumeAndInertia[1] << " " << volumeAndInertia[2] - << " " << volumeAndInertia[3] - << " " << volumeAndInertia[4] - << " " << volumeAndInertia[5] << " " << volumeAndInertia[6] << std::endl; - - //translate the tetrahedron so that the model is placed at origin i.e. com is at origin - p0 -= centroid; - p1 -= centroid; - p2 -= centroid; - p3 -= centroid; -} - -void MeshInfoTests::testWithCube(){ - glm::vec3 p0(1.0, -1.0, -1.0); - glm::vec3 p1(1.0, -1.0, 1.0); - glm::vec3 p2(-1.0, -1.0, 1.0); - glm::vec3 p3(-1.0, -1.0, -1.0); - glm::vec3 p4(1.0, 1.0, -1.0); - glm::vec3 p5(1.0, 1.0, 1.0); - glm::vec3 p6(-1.0, 1.0, 1.0); - glm::vec3 p7(-1.0, 1.0, -1.0); - vector vertices; - vertices.push_back(p0); - vertices.push_back(p1); - vertices.push_back(p2); - vertices.push_back(p3); - vertices.push_back(p4); - vertices.push_back(p5); - vertices.push_back(p6); - vertices.push_back(p7); - std::cout << std::setprecision(10); - vector triangles = { 0, 1, 2, 0, 2, 3, 4, 7, 6, 4, 6, 5, 0, 4, 5, 0, 5, 1, 1, 5, 6, 1, 6, 2, 2, 6, - 7, 2, 7, 3, 4, 0, 3, 4, 3, 7 }; - glm::vec3 centerOfMass(0.0, 0.0, 0.0); - double volume = 8.0; - double side = 2.0; - double inertia = (volume * side * side) / 6.0; //inertia of a unit cube is (mass * side * side) /6 - - //test with origin as reference point - meshinfo::MeshInfo massProp(&vertices, &triangles); - vector volumeAndInertia = massProp.computeMassProperties(); - if (abs(centerOfMass.x - massProp.getMeshCentroid().x) > epsilon || abs(centerOfMass.y - massProp.getMeshCentroid().y) > epsilon || - abs(centerOfMass.z - massProp.getMeshCentroid().z) > epsilon){ - std::cout << __FILE__ << ":" << __LINE__ << " ERROR : Center of mass is incorrect : Expected = " << centerOfMass.x << " " << - centerOfMass.y << " " << centerOfMass.z << ", actual = " << massProp.getMeshCentroid().x << " " << - massProp.getMeshCentroid().y << " " << massProp.getMeshCentroid().z << std::endl; - } - - if (abs(volume - volumeAndInertia.at(0)) > epsilon){ - std::cout << __FILE__ << ":" << __LINE__ << " ERROR : Volume is incorrect : Expected = " << volume << - ", actual = " << volumeAndInertia.at(0) << std::endl; - } - - if (abs(inertia - (volumeAndInertia.at(1))) > epsilon || abs(inertia - (volumeAndInertia.at(2))) > epsilon || - abs(inertia - (volumeAndInertia.at(3))) > epsilon){ - std::cout << __FILE__ << ":" << __LINE__ << " ERROR : Moment of inertia is incorrect : Expected = " << inertia << " " << - inertia << " " << inertia << ", actual = " << (volumeAndInertia.at(1)) << " " << (volumeAndInertia.at(2)) << - " " << (volumeAndInertia.at(3)) << std::endl; - } -} - -void MeshInfoTests::testWithUnitCube() -{ - glm::vec3 p0(0, 0, 1); - glm::vec3 p1(1, 0, 1); - glm::vec3 p2(0, 1, 1); - glm::vec3 p3(1, 1, 1); - glm::vec3 p4(0, 0, 0); - glm::vec3 p5(1, 0, 0); - glm::vec3 p6(0, 1, 0); - glm::vec3 p7(1, 1, 0); - vector vertices; - vertices.push_back(p0); - vertices.push_back(p1); - vertices.push_back(p2); - vertices.push_back(p3); - vertices.push_back(p4); - vertices.push_back(p5); - vertices.push_back(p6); - vertices.push_back(p7); - vector triangles = { 0, 1, 2, 1, 3, 2, 2, 3, 7, 2, 7, 6, 1, 7, 3, 1, 5, 7, 6, 7, 4, 7, 5, 4, 0, 4, 1, - 1, 4, 5, 2, 6, 4, 0, 2, 4 }; - glm::vec3 centerOfMass(0.5, 0.5, 0.5); - double volume = 1.0; - double side = 1.0; - double inertia = (volume * side * side) / 6.0; //inertia of a unit cube is (mass * side * side) /6 - std::cout << std::setprecision(10); - - //test with origin as reference point - meshinfo::MeshInfo massProp(&vertices, &triangles); - vector volumeAndInertia = massProp.computeMassProperties(); - if (abs(centerOfMass.x - massProp.getMeshCentroid().x) > epsilon || abs(centerOfMass.y - massProp.getMeshCentroid().y) > - epsilon || abs(centerOfMass.z - massProp.getMeshCentroid().z) > epsilon){ - std::cout << __FILE__ << ":" << __LINE__ << " ERROR : Center of mass is incorrect : Expected = " << centerOfMass.x << - " " << centerOfMass.y << " " << centerOfMass.z << ", actual = " << massProp.getMeshCentroid().x << " " << - massProp.getMeshCentroid().y << " " << massProp.getMeshCentroid().z << std::endl; - } - - if (abs(volume - volumeAndInertia.at(0)) > epsilon){ - std::cout << __FILE__ << ":" << __LINE__ << " ERROR : Volume is incorrect : Expected = " << volume << - ", actual = " << volumeAndInertia.at(0) << std::endl; - } - - if (abs(inertia - (volumeAndInertia.at(1))) > epsilon || abs(inertia - (volumeAndInertia.at(2))) > epsilon || - abs(inertia - (volumeAndInertia.at(3))) > epsilon){ - std::cout << __FILE__ << ":" << __LINE__ << " ERROR : Moment of inertia is incorrect : Expected = " << inertia << " " << - inertia << " " << inertia << ", actual = " << (volumeAndInertia.at(1)) << " " << (volumeAndInertia.at(2)) << - " " << (volumeAndInertia.at(3)) << std::endl; - } -} -void MeshInfoTests::runAllTests(){ - testWithTetrahedron(); - testWithTetrahedronAsMesh(); - testWithUnitCube(); - testWithCube(); -} diff --git a/tests/physics/src/MeshInfoTests.h b/tests/physics/src/MeshInfoTests.h deleted file mode 100644 index 0ddd8d0944..0000000000 --- a/tests/physics/src/MeshInfoTests.h +++ /dev/null @@ -1,21 +0,0 @@ -// -// MeshInfoTests.h -// tests/physics/src -// -// Created by Virendra Singh on 2015.03.02 -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_MeshInfoTests_h -#define hifi_MeshInfoTests_h -namespace MeshInfoTests{ - void testWithTetrahedron(); - void testWithUnitCube(); - void testWithCube(); - void runAllTests(); - void testWithTetrahedronAsMesh(); -} -#endif // hifi_MeshInfoTests_h \ No newline at end of file diff --git a/tests/physics/src/MeshMassPropertiesTests.cpp b/tests/physics/src/MeshMassPropertiesTests.cpp new file mode 100644 index 0000000000..ebb762aa55 --- /dev/null +++ b/tests/physics/src/MeshMassPropertiesTests.cpp @@ -0,0 +1,459 @@ +// +// MeshMassProperties.cpp +// tests/physics/src +// +// Created by Virendra Singh on 2015.03.02 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include + +#include "MeshMassPropertiesTests.h" + +//#define VERBOSE_UNIT_TESTS + +const btScalar acceptableRelativeError(1.0e-5f); +const btScalar acceptableAbsoluteError(1.0e-4f); + +void printMatrix(const std::string& name, const btMatrix3x3& matrix) { + std::cout << name << " = [" << std::endl; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + std::cout << " " << matrix[i][j]; + } + std::cout << std::endl; + } + std::cout << "]" << std::endl; +} + +void pushTriangle(VectorOfIndices& indices, uint32_t a, uint32_t b, uint32_t c) { + indices.push_back(a); + indices.push_back(b); + indices.push_back(c); +} + +void MeshMassPropertiesTests::testParallelAxisTheorem() { +#ifdef EXPOSE_HELPER_FUNCTIONS_FOR_UNIT_TEST + // verity we can compute the inertia tensor of a box in two different ways: + // (a) as one box + // (b) as a combination of two partial boxes. +#ifdef VERBOSE_UNIT_TESTS + std::cout << "\n" << __FUNCTION__ << std::endl; +#endif // VERBOSE_UNIT_TESTS + + btScalar bigBoxX = 7.0f; + btScalar bigBoxY = 9.0f; + btScalar bigBoxZ = 11.0f; + btScalar bigBoxMass = bigBoxX * bigBoxY * bigBoxZ; + btMatrix3x3 bitBoxInertia; + computeBoxInertia(bigBoxMass, btVector3(bigBoxX, bigBoxY, bigBoxZ), bitBoxInertia); + + btScalar smallBoxX = bigBoxX / 2.0f; + btScalar smallBoxY = bigBoxY; + btScalar smallBoxZ = bigBoxZ; + btScalar smallBoxMass = smallBoxX * smallBoxY * smallBoxZ; + btMatrix3x3 smallBoxI; + computeBoxInertia(smallBoxMass, btVector3(smallBoxX, smallBoxY, smallBoxZ), smallBoxI); + + btVector3 smallBoxOffset(smallBoxX / 2.0f, 0.0f, 0.0f); + + btMatrix3x3 smallBoxShiftedRight = smallBoxI; + applyParallelAxisTheorem(smallBoxShiftedRight, smallBoxOffset, smallBoxMass); + + btMatrix3x3 smallBoxShiftedLeft = smallBoxI; + applyParallelAxisTheorem(smallBoxShiftedLeft, -smallBoxOffset, smallBoxMass); + + btMatrix3x3 twoSmallBoxesInertia = smallBoxShiftedRight + smallBoxShiftedLeft; + + // verify bigBox same as twoSmallBoxes + btScalar error; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + error = bitBoxInertia[i][j] - twoSmallBoxesInertia[i][j]; + if (fabsf(error) > acceptableAbsoluteError) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR : box inertia[" << i << "][" << j << "] off by = " + << error << std::endl; + } + } + } + +#ifdef VERBOSE_UNIT_TESTS + printMatrix("expected inertia", bitBoxInertia); + printMatrix("computed inertia", twoSmallBoxesInertia); +#endif // VERBOSE_UNIT_TESTS +#endif // EXPOSE_HELPER_FUNCTIONS_FOR_UNIT_TEST +} + +void MeshMassPropertiesTests::testTetrahedron(){ + // given the four vertices of a tetrahedron verify the analytic formula for inertia + // agrees with expected results +#ifdef VERBOSE_UNIT_TESTS + std::cout << "\n" << __FUNCTION__ << std::endl; +#endif // VERBOSE_UNIT_TESTS + + // these numbers from the Tonon paper: + btVector3 points[4]; + points[0] = btVector3(8.33220f, -11.86875f, 0.93355f); + points[1] = btVector3(0.75523f, 5.00000f, 16.37072f); + points[2] = btVector3(52.61236f, 5.00000f, -5.38580f); + points[3] = btVector3(2.00000f, 5.00000f, 3.00000f); + + btScalar expectedVolume = 1873.233236f; + + btMatrix3x3 expectedInertia; + expectedInertia[0][0] = 43520.33257f; + expectedInertia[1][1] = 194711.28938f; + expectedInertia[2][2] = 191168.76173f; + expectedInertia[1][2] = -4417.66150f; + expectedInertia[2][1] = -4417.66150f; + expectedInertia[0][2] = 46343.16662f; + expectedInertia[2][0] = 46343.16662f; + expectedInertia[0][1] = -11996.20119f; + expectedInertia[1][0] = -11996.20119f; + + // compute volume + btScalar volume = computeTetrahedronVolume(points); + btScalar error = (volume - expectedVolume) / expectedVolume; + if (fabsf(error) > acceptableRelativeError) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR : volume of tetrahedron off by = " + << error << std::endl; + } + + btVector3 centerOfMass = 0.25f * (points[0] + points[1] + points[2] + points[3]); + + // compute inertia tensor + // (shift the points so that tetrahedron's local centerOfMass is at origin) + for (int i = 0; i < 4; ++i) { + points[i] -= centerOfMass; + } + btMatrix3x3 inertia; + computeTetrahedronInertia(volume, points, inertia); + + // verify + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + error = (inertia[i][j] - expectedInertia[i][j]) / expectedInertia[i][j]; + if (fabsf(error) > acceptableRelativeError) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR : inertia[" << i << "][" << j << "] off by " + << error << std::endl; + } + } + } + +#ifdef VERBOSE_UNIT_TESTS + std::cout << "expected volume = " << expectedVolume << std::endl; + std::cout << "measured volume = " << volume << std::endl; + printMatrix("expected inertia", expectedInertia); + printMatrix("computed inertia", inertia); + + // when building VERBOSE you might be instrested in the results from the brute force method: + btMatrix3x3 bruteInertia; + computeTetrahedronInertiaByBruteForce(points, bruteInertia); + printMatrix("brute inertia", bruteInertia); +#endif // VERBOSE_UNIT_TESTS +} + +void MeshMassPropertiesTests::testOpenTetrahedonMesh() { + // given the simplest possible mesh (open, with one triangle) + // verify MeshMassProperties computes the right nubers +#ifdef VERBOSE_UNIT_TESTS + std::cout << "\n" << __FUNCTION__ << std::endl; +#endif // VERBOSE_UNIT_TESTS + + // these numbers from the Tonon paper: + VectorOfPoints points; + points.push_back(btVector3(8.33220f, -11.86875f, 0.93355f)); + points.push_back(btVector3(0.75523f, 5.00000f, 16.37072f)); + points.push_back(btVector3(52.61236f, 5.00000f, -5.38580f)); + points.push_back(btVector3(2.00000f, 5.00000f, 3.00000f)); + + btScalar expectedVolume = 1873.233236f; + + btMatrix3x3 expectedInertia; + expectedInertia[0][0] = 43520.33257f; + expectedInertia[1][1] = 194711.28938f; + expectedInertia[2][2] = 191168.76173f; + expectedInertia[1][2] = -4417.66150f; + expectedInertia[2][1] = -4417.66150f; + expectedInertia[0][2] = 46343.16662f; + expectedInertia[2][0] = 46343.16662f; + expectedInertia[0][1] = -11996.20119f; + expectedInertia[1][0] = -11996.20119f; + + // test as an open mesh with one triangle + VectorOfPoints shiftedPoints; + shiftedPoints.push_back(points[0] - points[0]); + shiftedPoints.push_back(points[1] - points[0]); + shiftedPoints.push_back(points[2] - points[0]); + shiftedPoints.push_back(points[3] - points[0]); + VectorOfIndices triangles; + pushTriangle(triangles, 1, 2, 3); + btVector3 expectedCenterOfMass = 0.25f * (shiftedPoints[0] + shiftedPoints[1] + shiftedPoints[2] + shiftedPoints[3]); + + // compute mass properties + MeshMassProperties mesh(shiftedPoints, triangles); + + // verify + btScalar error = (mesh._volume - expectedVolume) / expectedVolume; + if (fabsf(error) > acceptableRelativeError) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR : volume of tetrahedron off by = " + << error << std::endl; + } + + error = (mesh._centerOfMass - expectedCenterOfMass).length(); + if (fabsf(error) > acceptableAbsoluteError) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR : centerOfMass of tetrahedron off by = " + << error << std::endl; + } + + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + error = (mesh._inertia[i][j] - expectedInertia[i][j]) / expectedInertia[i][j]; + if (fabsf(error) > acceptableRelativeError) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR : inertia[" << i << "][" << j << "] off by " + << error << std::endl; + } + } + } + +#ifdef VERBOSE_UNIT_TESTS + std::cout << "expected volume = " << expectedVolume << std::endl; + std::cout << "measured volume = " << mesh._volume << std::endl; + printMatrix("expected inertia", expectedInertia); + printMatrix("computed inertia", mesh._inertia); +#endif // VERBOSE_UNIT_TESTS +} + +void MeshMassPropertiesTests::testClosedTetrahedronMesh() { + // given a tetrahedron as a closed mesh of four tiangles + // verify MeshMassProperties computes the right nubers +#ifdef VERBOSE_UNIT_TESTS + std::cout << "\n" << __FUNCTION__ << std::endl; +#endif // VERBOSE_UNIT_TESTS + + // these numbers from the Tonon paper: + VectorOfPoints points; + points.push_back(btVector3(8.33220f, -11.86875f, 0.93355f)); + points.push_back(btVector3(0.75523f, 5.00000f, 16.37072f)); + points.push_back(btVector3(52.61236f, 5.00000f, -5.38580f)); + points.push_back(btVector3(2.00000f, 5.00000f, 3.00000f)); + + btScalar expectedVolume = 1873.233236f; + + btMatrix3x3 expectedInertia; + expectedInertia[0][0] = 43520.33257f; + expectedInertia[1][1] = 194711.28938f; + expectedInertia[2][2] = 191168.76173f; + expectedInertia[1][2] = -4417.66150f; + expectedInertia[2][1] = -4417.66150f; + expectedInertia[0][2] = 46343.16662f; + expectedInertia[2][0] = 46343.16662f; + expectedInertia[0][1] = -11996.20119f; + expectedInertia[1][0] = -11996.20119f; + + btVector3 expectedCenterOfMass = 0.25f * (points[0] + points[1] + points[2] + points[3]); + + VectorOfIndices triangles; + pushTriangle(triangles, 0, 2, 1); + pushTriangle(triangles, 0, 3, 2); + pushTriangle(triangles, 0, 1, 3); + pushTriangle(triangles, 1, 2, 3); + + // compute mass properties + MeshMassProperties mesh(points, triangles); + + // verify + btScalar error; + error = (mesh._volume - expectedVolume) / expectedVolume; + if (fabsf(error) > acceptableRelativeError) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR : volume of tetrahedron off by = " + << error << std::endl; + } + + error = (mesh._centerOfMass - expectedCenterOfMass).length(); + if (fabsf(error) > acceptableAbsoluteError) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR : centerOfMass of tetrahedron off by = " + << error << std::endl; + } + + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + error = (mesh._inertia[i][j] - expectedInertia[i][j]) / expectedInertia[i][j]; + if (fabsf(error) > acceptableRelativeError) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR : inertia[" << i << "][" << j << "] off by " + << error << std::endl; + } + } + } + +#ifdef VERBOSE_UNIT_TESTS + std::cout << "(a) tetrahedron as mesh" << std::endl; + std::cout << "expected volume = " << expectedVolume << std::endl; + std::cout << "measured volume = " << mesh._volume << std::endl; + printMatrix("expected inertia", expectedInertia); + printMatrix("computed inertia", mesh._inertia); +#endif // VERBOSE_UNIT_TESTS + + // test again, but this time shift the points so that the origin is definitely OUTSIDE the mesh + btVector3 shift = points[0] + expectedCenterOfMass; + for (int i = 0; i < (int)points.size(); ++i) { + points[i] += shift; + } + expectedCenterOfMass = 0.25f * (points[0] + points[1] + points[2] + points[3]); + + // compute mass properties + mesh.computeMassProperties(points, triangles); + + // verify + error = (mesh._volume - expectedVolume) / expectedVolume; + if (fabsf(error) > acceptableRelativeError) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR : volume of tetrahedron off by = " + << error << std::endl; + } + + error = (mesh._centerOfMass - expectedCenterOfMass).length(); + if (fabsf(error) > acceptableAbsoluteError) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR : centerOfMass of tetrahedron off by = " + << error << std::endl; + } + + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + error = (mesh._inertia[i][j] - expectedInertia[i][j]) / expectedInertia[i][j]; + if (fabsf(error) > acceptableRelativeError) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR : inertia[" << i << "][" << j << "] off by " + << error << std::endl; + } + } + } + +#ifdef VERBOSE_UNIT_TESTS + std::cout << "(b) shifted tetrahedron as mesh" << std::endl; + std::cout << "expected volume = " << expectedVolume << std::endl; + std::cout << "measured volume = " << mesh._volume << std::endl; + printMatrix("expected inertia", expectedInertia); + printMatrix("computed inertia", mesh._inertia); +#endif // VERBOSE_UNIT_TESTS +} + +void MeshMassPropertiesTests::testBoxAsMesh() { + // verify that a mesh box produces the same mass properties as the analytic box. +#ifdef VERBOSE_UNIT_TESTS + std::cout << "\n" << __FUNCTION__ << std::endl; +#endif // VERBOSE_UNIT_TESTS + + + // build a box: + // / + // y + // / + // 6-------------------------7 + // /| /| + // / | / | + // / 2----------------------/--3 + // / / / / + // | 4-------------------------5 / --x-- + // z | / | / + // | |/ |/ + // 0 ------------------------1 + + btScalar x(5.0f); + btScalar y(3.0f); + btScalar z(2.0f); + + VectorOfPoints points; + points.reserve(8); + + points.push_back(btVector3(0.0f, 0.0f, 0.0f)); + points.push_back(btVector3(x, 0.0f, 0.0f)); + points.push_back(btVector3(0.0f, y, 0.0f)); + points.push_back(btVector3(x, y, 0.0f)); + points.push_back(btVector3(0.0f, 0.0f, z)); + points.push_back(btVector3(x, 0.0f, z)); + points.push_back(btVector3(0.0f, y, z)); + points.push_back(btVector3(x, y, z)); + + VectorOfIndices triangles; + pushTriangle(triangles, 0, 1, 4); + pushTriangle(triangles, 1, 5, 4); + pushTriangle(triangles, 1, 3, 5); + pushTriangle(triangles, 3, 7, 5); + pushTriangle(triangles, 2, 0, 6); + pushTriangle(triangles, 0, 4, 6); + pushTriangle(triangles, 3, 2, 7); + pushTriangle(triangles, 2, 6, 7); + pushTriangle(triangles, 4, 5, 6); + pushTriangle(triangles, 5, 7, 6); + pushTriangle(triangles, 0, 2, 1); + pushTriangle(triangles, 2, 3, 1); + + // compute expected mass properties analytically + btVector3 expectedCenterOfMass = 0.5f * btVector3(x, y, z); + btScalar expectedVolume = x * y * z; + btMatrix3x3 expectedInertia; + computeBoxInertia(expectedVolume, btVector3(x, y, z), expectedInertia); + + // compute the mass properties using the mesh + MeshMassProperties mesh(points, triangles); + + // verify + btScalar error; + error = (mesh._volume - expectedVolume) / expectedVolume; + if (fabsf(error) > acceptableRelativeError) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR : volume of tetrahedron off by = " + << error << std::endl; + } + + error = (mesh._centerOfMass - expectedCenterOfMass).length(); + if (fabsf(error) > acceptableAbsoluteError) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR : centerOfMass of tetrahedron off by = " + << error << std::endl; + } + + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + if (expectedInertia [i][j] == btScalar(0.0f)) { + error = mesh._inertia[i][j] - expectedInertia[i][j]; + if (fabsf(error) > acceptableAbsoluteError) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR : inertia[" << i << "][" << j << "] off by " + << error << " absolute"<< std::endl; + } + } else { + error = (mesh._inertia[i][j] - expectedInertia[i][j]) / expectedInertia[i][j]; + if (fabsf(error) > acceptableRelativeError) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR : inertia[" << i << "][" << j << "] off by " + << error << std::endl; + } + } + } + } + +#ifdef VERBOSE_UNIT_TESTS + std::cout << "expected volume = " << expectedVolume << std::endl; + std::cout << "measured volume = " << mesh._volume << std::endl; + std::cout << "expected center of mass = < " + << expectedCenterOfMass[0] << ", " + << expectedCenterOfMass[1] << ", " + << expectedCenterOfMass[2] << "> " << std::endl; + std::cout << "computed center of mass = < " + << mesh._centerOfMass[0] << ", " + << mesh._centerOfMass[1] << ", " + << mesh._centerOfMass[2] << "> " << std::endl; + printMatrix("expected inertia", expectedInertia); + printMatrix("computed inertia", mesh._inertia); +#endif // VERBOSE_UNIT_TESTS +} + +void MeshMassPropertiesTests::runAllTests() { + testParallelAxisTheorem(); + testTetrahedron(); + testOpenTetrahedonMesh(); + testClosedTetrahedronMesh(); + testBoxAsMesh(); + //testWithCube(); +} diff --git a/tests/physics/src/MeshMassPropertiesTests.h b/tests/physics/src/MeshMassPropertiesTests.h new file mode 100644 index 0000000000..ab352bfce2 --- /dev/null +++ b/tests/physics/src/MeshMassPropertiesTests.h @@ -0,0 +1,22 @@ +// +// MeshMassPropertiesTests.h +// tests/physics/src +// +// Created by Virendra Singh on 2015.03.02 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_MeshMassPropertiesTests_h +#define hifi_MeshMassPropertiesTests_h +namespace MeshMassPropertiesTests{ + void testParallelAxisTheorem(); + void testTetrahedron(); + void testOpenTetrahedonMesh(); + void testClosedTetrahedronMesh(); + void testBoxAsMesh(); + void runAllTests(); +} +#endif // hifi_MeshMassPropertiesTests_h diff --git a/tests/physics/src/main.cpp b/tests/physics/src/main.cpp index 0f35ed5002..f63925bb34 100644 --- a/tests/physics/src/main.cpp +++ b/tests/physics/src/main.cpp @@ -12,13 +12,13 @@ #include "ShapeInfoTests.h" #include "ShapeManagerTests.h" #include "BulletUtilTests.h" -#include "MeshInfoTests.h" +#include "MeshMassPropertiesTests.h" int main(int argc, char** argv) { ShapeColliderTests::runAllTests(); ShapeInfoTests::runAllTests(); ShapeManagerTests::runAllTests(); BulletUtilTests::runAllTests(); - MeshInfoTests::runAllTests(); + MeshMassPropertiesTests::runAllTests(); return 0; }