From da6986f759860fa619a1d7b9a98ef0d8e60050aa Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 7 Jul 2015 21:06:12 +0200 Subject: [PATCH 001/242] Planky refactorings and basic planky settings --- examples/example/games/planky.js | 282 +++++++++++++++++++++--------- examples/html/plankySettings.html | 112 ++++++++++++ 2 files changed, 312 insertions(+), 82 deletions(-) create mode 100644 examples/html/plankySettings.html diff --git a/examples/example/games/planky.js b/examples/example/games/planky.js index 00a4e7f61d..cd4a7295be 100644 --- a/examples/example/games/planky.js +++ b/examples/example/games/planky.js @@ -12,23 +12,28 @@ // HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; -const NUM_LAYERS = 16; -const BASE_DIMENSION = { x: 7, y: 2, z: 7 }; -const BLOCKS_PER_LAYER = 3; -const BLOCK_SIZE = {x: 0.2, y: 0.1, z: 0.8}; -const BLOCK_SPACING = BLOCK_SIZE.x / 3; +const DEFAULT_NUM_LAYERS = 16; +const DEFAULT_BASE_DIMENSION = { x: 7, y: 2, z: 7 }; +const DEFAULT_BLOCKS_PER_LAYER = 3; +const DEFAULT_BLOCK_SIZE = {x: 0.2, y: 0.1, z: 0.8}; +const DEFAULT_BLOCK_SPACING = DEFAULT_BLOCK_SIZE.x / DEFAULT_BLOCKS_PER_LAYER; // BLOCK_HEIGHT_VARIATION removes a random percentages of the default block height per block. (for example 0.001 %) -const BLOCK_HEIGHT_VARIATION = 0.001; -const GRAVITY = {x: 0, y: -2.8, z: 0}; -const DENSITY = 2000; -const DAMPING_FACTOR = 0.98; -const ANGULAR_DAMPING_FACTOR = 0.8; -const FRICTION = 0.99; -const RESTITUTION = 0.0; -const SPAWN_DISTANCE = 3; -const BLOCK_YAW_OFFSET = 45; +const DEFAULT_BLOCK_HEIGHT_VARIATION = 0.001; +const DEFAULT_GRAVITY = {x: 0, y: -2.8, z: 0}; +const DEFAULT_DENSITY = 2000; +const DEFAULT_DAMPING_FACTOR = 0.98; +const DEFAULT_ANGULAR_DAMPING_FACTOR = 0.8; +const DEFAULT_FRICTION = 0.99; +const DEFAULT_RESTITUTION = 0.0; +const DEFAULT_SPAWN_DISTANCE = 3; +const DEFAULT_BLOCK_YAW_OFFSET = 45; + +var editMode = false; + const BUTTON_DIMENSIONS = {width: 49, height: 49}; const MAXIMUM_PERCENTAGE = 100.0; +const NO_ANGLE = 0; +const RIGHT_ANGLE = 90; var windowWidth = Window.innerWidth; var size; @@ -36,6 +41,163 @@ var pieces = []; var ground = false; var layerRotated = false; +SettingsWindow = function() { + this.webWindow = null; + this.init = function() { + this.webWindow = new WebWindow('Planky', Script.resolvePath('../../html/plankySettings.html'), 255, 500, true); + this.webWindow.setVisible(false); + }; +}; + +PlankyOptions = function() { + var _this = this; + this.save = function() { + //TODO: save Planky Options here. + }; + this.load = function() { + //TODO: load Planky Options here. + }; + this.setDefaults = function() { + _this.numLayers = DEFAULT_NUM_LAYERS; + _this.baseDimension = DEFAULT_BASE_DIMENSION; + _this.blocksPerLayer = DEFAULT_BLOCKS_PER_LAYER; + _this.blockSize = DEFAULT_BLOCK_SIZE; + _this.blockSpacing = DEFAULT_BLOCK_SPACING; + _this.blockHeightVariation = DEFAULT_BLOCK_HEIGHT_VARIATION; + _this.gravity = DEFAULT_GRAVITY; + _this.density = DEFAULT_DENSITY; + _this.dampingFactor = DEFAULT_DAMPING_FACTOR; + _this.angularDampingFactor = DEFAULT_ANGULAR_DAMPING_FACTOR; + _this.friction = DEFAULT_FRICTION; + _this.restitution = DEFAULT_RESTITUTION; + _this.spawnDistance = DEFAULT_SPAWN_DISTANCE; + _this.blockYawOffset = DEFAULT_BLOCK_YAW_OFFSET; + }; + this.setDefaults(); +}; + +// The PlankyStack exists out of rows and layers +PlankyStack = function() { + var _this = this; + this.planks = []; + this.ground = false; + this.options = new PlankyOptions(); + this.deRez = function() { + _this.planks.forEach(function(plank) { + Entities.deleteEntity(plank.entity); + }); + _this.planks = []; + if (_this.ground) { + Entities.deleteEntity(_this.ground); + } + _this.ground = false; + }; + this.rez = function() { + if (_this.planks.length > 0) { + _this.deRez(); + } + _this.baseRotation = Quat.fromPitchYawRollDegrees(0.0, MyAvatar.bodyYaw, 0.0); + var basePosition = Vec3.sum(MyAvatar.position, Vec3.multiply(_this.options.spawnDistance, Quat.getFront(_this.baseRotation))); + basePosition.y = grabLowestJointY(); + _this.basePosition = basePosition; + _this.refresh(); + }; + + //private function + var refreshGround = function() { + if (!_this.ground) { + _this.ground = Entities.addEntity({ + type: 'Model', + modelURL: HIFI_PUBLIC_BUCKET + 'eric/models/woodFloor.fbx', + dimensions: _this.options.baseDimension, + position: Vec3.sum(_this.basePosition, {y: -(_this.options.baseDimension.y / 2)}), + rotation: _this.baseRotation, + shapeType: 'box' + }); + return; + } + // move ground to rez position/rotation + Entities.editEntity(_this.ground, {dimensions: _this.options.baseDimension, position: Vec3.sum(_this.basePosition, {y: -(_this.options.baseDimension.y / 2)}), rotation: _this.baseRotation}); + } + var trimDimension = function(dimension, maxIndex) { + _this.planks.forEach(function(plank, index, object) { + if (plank[dimension] > maxIndex) { + Entities.deleteEntity(plank.entity); + object.splice(index, 1); + } + }); + }; + var createOrUpdate = function(layer, row) { + var found = false; + var layerRotated = layer % 2 === 0; + var offset = -(_this.options.blockSpacing); + var layerRotation = Quat.fromPitchYawRollDegrees(0, layerRotated ? NO_ANGLE : RIGHT_ANGLE, 0.0); + var blockPositionXZ = (_this.options.blockSize.x * row) - (((_this.options.blockSize.x * _this.options.blocksPerLayer) / 2) - (_this.options.blockSize.x / 2)); + var localTransform = Vec3.multiplyQbyV(_this.offsetRot, { + x: (layerRotated ? blockPositionXZ + offset: 0), + y: (_this.options.blockSize.y / 2) + (_this.options.blockSize.y * layer), + z: (layerRotated ? 0 : blockPositionXZ + offset) + }); + var newProperties = { + type: 'Model', + modelURL: HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/block.fbx', + shapeType: 'box', + name: 'PlankyBlock' + layer + '-' + row, + dimensions: Vec3.sum(_this.options.blockSize, {x: 0, y: -((_this.options.blockSize.y * (_this.options.blockHeightVariation / MAXIMUM_PERCENTAGE)) * Math.random()), z: 0}), + position: Vec3.sum(_this.basePosition, localTransform), + rotation: Quat.multiply(layerRotation, _this.offsetRot), + //collisionsWillMove: false,//!editMode,//false,//!editMode, + damping: _this.options.dampingFactor, + restitution: _this.options.restitution, + friction: _this.options.friction, + angularDamping: _this.options.angularDampingFactor, + gravity: _this.options.gravity, + density: _this.options.density, + velocity: {x: 0, y: 0, z: 0}, + angularVelocity: Quat.fromPitchYawRollDegrees(0, 0, 0), + ignoreForCollisions: true + }; + _this.planks.forEach(function(plank, index, object) { + if (plank.layer === layer && plank.row === row) { + Entities.editEntity(plank.entity, newProperties); + found = true; + // break loop: + return false; + } + }); + if (!found) { + _this.planks.push({layer: layer, row: row, entity: Entities.addEntity(newProperties)}) + } + }; + this.refresh = function() { + refreshGround(); + trimDimension('layer', _this.options.numLayers); + trimDimension('row', _this.options.blocksPerLayer); + _this.offsetRot = Quat.multiply(_this.baseRotation, Quat.fromPitchYawRollDegrees(0.0, _this.options.blockYawOffset, 0.0)); + for (var layer = 0; layer < _this.options.numLayers; layer++) { + for (var row = 0; row < _this.options.blocksPerLayer; row++) { + createOrUpdate(layer, row); + } + } + if (!editMode) { + _this.planks.forEach(function(plank, index, object) { + print(index + " , " + plank); + Entities.editEntity(plank.entity, {ignoreForCollisions: false, collisionsWillMove: true}); + // Entities.editEntity(plank.entity, {collisionsWillMove: true}); + }); + } + }; + this.isFound = function() { + //TODO: identify entities here until one is found + return _this.planks.length > 0; + }; +}; + +var settingsWindow = new SettingsWindow(); +settingsWindow.init(); + +var plankyStack = new PlankyStack(); + function grabLowestJointY() { var jointNames = MyAvatar.getJointNames(); var floorY = MyAvatar.position.y; @@ -51,6 +213,10 @@ function getButtonPosX() { return windowWidth - ((BUTTON_DIMENSIONS.width / 2) + BUTTON_DIMENSIONS.width); } +function getCogButtonPosX() { + return getButtonPosX() - (BUTTON_DIMENSIONS.width * 1.1); +} + var button = Overlays.addOverlay('image', { x: getButtonPosX(), y: 10, @@ -60,73 +226,26 @@ var button = Overlays.addOverlay('image', { alpha: 0.8 }); - -function resetBlocks() { - pieces.forEach(function(piece) { - Entities.deleteEntity(piece); - }); - pieces = []; - var avatarRot = Quat.fromPitchYawRollDegrees(0.0, MyAvatar.bodyYaw, 0.0); - basePosition = Vec3.sum(MyAvatar.position, Vec3.multiply(SPAWN_DISTANCE, Quat.getFront(avatarRot))); - basePosition.y = grabLowestJointY() - (BASE_DIMENSION.y / 2); - if (!ground) { - ground = Entities.addEntity({ - type: 'Model', - modelURL: HIFI_PUBLIC_BUCKET + 'eric/models/woodFloor.fbx', - dimensions: BASE_DIMENSION, - position: basePosition, - rotation: avatarRot, - shapeType: 'box' - }); - } else { - Entities.editEntity(ground, {position: basePosition, rotation: avatarRot}); - } - var offsetRot = Quat.multiply(avatarRot, Quat.fromPitchYawRollDegrees(0.0, BLOCK_YAW_OFFSET, 0.0)); - basePosition.y += (BASE_DIMENSION.y / 2); - for (var layerIndex = 0; layerIndex < NUM_LAYERS; layerIndex++) { - var layerRotated = layerIndex % 2 === 0; - var offset = -(BLOCK_SPACING); - var layerRotation = Quat.fromPitchYawRollDegrees(0, layerRotated ? 0 : 90, 0.0); - for (var blockIndex = 0; blockIndex < BLOCKS_PER_LAYER; blockIndex++) { - var blockPositionXZ = BLOCK_SIZE.x * blockIndex - (BLOCK_SIZE.x * 3 / 2 - BLOCK_SIZE.x / 2); - var localTransform = Vec3.multiplyQbyV(offsetRot, { - x: (layerRotated ? blockPositionXZ + offset: 0), - y: (BLOCK_SIZE.y / 2) + (BLOCK_SIZE.y * layerIndex), - z: (layerRotated ? 0 : blockPositionXZ + offset) - }); - pieces.push(Entities.addEntity({ - type: 'Model', - modelURL: HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/block.fbx', - shapeType: 'box', - name: 'PlankyBlock' + ((layerIndex * BLOCKS_PER_LAYER) + blockIndex), - dimensions: { - x: BLOCK_SIZE.x, - y: BLOCK_SIZE.y - ((BLOCK_SIZE.y * (BLOCK_HEIGHT_VARIATION / MAXIMUM_PERCENTAGE)) * Math.random()), - z: BLOCK_SIZE.z - }, - position: { - x: basePosition.x + localTransform.x, - y: basePosition.y + localTransform.y, - z: basePosition.z + localTransform.z - }, - rotation: Quat.multiply(layerRotation, offsetRot), - collisionsWillMove: true, - damping: DAMPING_FACTOR, - restitution: RESTITUTION, - friction: FRICTION, - angularDamping: ANGULAR_DAMPING_FACTOR, - gravity: GRAVITY, - density: DENSITY - })); - offset += BLOCK_SPACING; - } - } -} +var cogButton = Overlays.addOverlay('image', { + x: getCogButtonPosX(), + y: 10, + width: BUTTON_DIMENSIONS.width, + height: BUTTON_DIMENSIONS.height, + imageURL: HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/cog.svg', + alpha: 0.8 +}); function mousePressEvent(event) { var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); if (clickedOverlay === button) { - resetBlocks(); + if (!plankyStack.isFound()) { + plankyStack.rez(); + return; + } + editMode = !editMode; + plankyStack.refresh(); + } else if (clickedOverlay === cogButton) { + settingsWindow.webWindow.setVisible(true); } } @@ -134,19 +253,18 @@ Controller.mousePressEvent.connect(mousePressEvent); function cleanup() { Overlays.deleteOverlay(button); + Overlays.deleteOverlay(cogButton); if (ground) { Entities.deleteEntity(ground); } - pieces.forEach(function(piece) { - Entities.deleteEntity(piece); - }); - pieces = []; + plankyStack.deRez(); } function onUpdate() { - if (windowWidth != Window.innerWidth) { + if (windowWidth !== Window.innerWidth) { windowWidth = Window.innerWidth; Overlays.editOverlay(button, {x: getButtonPosX()}); + Overlays.editOverlay(cogButton, {x: getCogButtonPosX()}); } } diff --git a/examples/html/plankySettings.html b/examples/html/plankySettings.html new file mode 100644 index 0000000000..9e2a08c650 --- /dev/null +++ b/examples/html/plankySettings.html @@ -0,0 +1,112 @@ + + + + + + + + +
+ + \ No newline at end of file From 90284b8bb46825aca0aee3bc13969b413af0609b Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 7 Jul 2015 21:33:28 +0200 Subject: [PATCH 002/242] fixes mysterious button display bug --- examples/example/games/planky.js | 41 +++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/examples/example/games/planky.js b/examples/example/games/planky.js index cd4a7295be..383e1d57d5 100644 --- a/examples/example/games/planky.js +++ b/examples/example/games/planky.js @@ -40,6 +40,8 @@ var size; var pieces = []; var ground = false; var layerRotated = false; +var button; +var cogButton; SettingsWindow = function() { this.webWindow = null; @@ -217,25 +219,38 @@ function getCogButtonPosX() { return getButtonPosX() - (BUTTON_DIMENSIONS.width * 1.1); } -var button = Overlays.addOverlay('image', { +print(JSON.stringify({ x: getButtonPosX(), y: 10, width: BUTTON_DIMENSIONS.width, height: BUTTON_DIMENSIONS.height, imageURL: HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/planky_button.svg', alpha: 0.8 -}); +})); -var cogButton = Overlays.addOverlay('image', { - x: getCogButtonPosX(), - y: 10, - width: BUTTON_DIMENSIONS.width, - height: BUTTON_DIMENSIONS.height, - imageURL: HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/cog.svg', - alpha: 0.8 -}); +function createButtons() { + button = Overlays.addOverlay('image', { + x: getButtonPosX(), + y: 10, + width: BUTTON_DIMENSIONS.width, + height: BUTTON_DIMENSIONS.height, + imageURL: HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/planky_button.svg', + alpha: 0.8 + }); -function mousePressEvent(event) { + cogButton = Overlays.addOverlay('image', { + x: getCogButtonPosX(), + y: 10, + width: BUTTON_DIMENSIONS.width, + height: BUTTON_DIMENSIONS.height, + imageURL: HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/cog.svg', + alpha: 0.8 + }); +} +// Fixes bug of not showing buttons on startup +Script.setTimeout(createButtons, 1000); + +Controller.mousePressEvent.connect(function(event) { var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); if (clickedOverlay === button) { if (!plankyStack.isFound()) { @@ -247,9 +262,7 @@ function mousePressEvent(event) { } else if (clickedOverlay === cogButton) { settingsWindow.webWindow.setVisible(true); } -} - -Controller.mousePressEvent.connect(mousePressEvent); +}); function cleanup() { Overlays.deleteOverlay(button); From c6b3801d0b2b3774678e1b1e2640597f84545f37 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 9 Jul 2015 20:53:09 +0200 Subject: [PATCH 003/242] proper block offset calculations --- examples/example/games/planky.js | 42 ++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/examples/example/games/planky.js b/examples/example/games/planky.js index 383e1d57d5..82ae4af9e0 100644 --- a/examples/example/games/planky.js +++ b/examples/example/games/planky.js @@ -83,6 +83,7 @@ PlankyStack = function() { var _this = this; this.planks = []; this.ground = false; + this.editLines = []; this.options = new PlankyOptions(); this.deRez = function() { _this.planks.forEach(function(plank) { @@ -92,7 +93,14 @@ PlankyStack = function() { if (_this.ground) { Entities.deleteEntity(_this.ground); } + this.editLines.forEach(function(line) { + Entities.deleteEntity(line); + }) + if (_this.centerLine) { + Entities.deleteEntity(_this.centerLine); + } _this.ground = false; + _this.centerLine = false; }; this.rez = function() { if (_this.planks.length > 0) { @@ -121,6 +129,20 @@ PlankyStack = function() { // move ground to rez position/rotation Entities.editEntity(_this.ground, {dimensions: _this.options.baseDimension, position: Vec3.sum(_this.basePosition, {y: -(_this.options.baseDimension.y / 2)}), rotation: _this.baseRotation}); } + + var refreshLines = function() { + if (_this.editLines.length === 0) { + _this.editLines.push(Entities.addEntity({ + type: 'Line', + dimensions: {x: 5, y: 21, z: 5}, + position: Vec3.sum(_this.basePosition, {y: -(_this.options.baseDimension.y / 2)}), + lineWidth: 7, + color: {red: 214, green: 91, blue: 67}, + linePoints: [{x: 0, y: 0, z: 0}, {x: 0, y: 10, z: 0}] + })); + return; + } + } var trimDimension = function(dimension, maxIndex) { _this.planks.forEach(function(plank, index, object) { if (plank[dimension] > maxIndex) { @@ -132,13 +154,12 @@ PlankyStack = function() { var createOrUpdate = function(layer, row) { var found = false; var layerRotated = layer % 2 === 0; - var offset = -(_this.options.blockSpacing); - var layerRotation = Quat.fromPitchYawRollDegrees(0, layerRotated ? NO_ANGLE : RIGHT_ANGLE, 0.0); - var blockPositionXZ = (_this.options.blockSize.x * row) - (((_this.options.blockSize.x * _this.options.blocksPerLayer) / 2) - (_this.options.blockSize.x / 2)); + var layerRotation = Quat.fromPitchYawRollDegrees(0, layerRotated ? NO_ANGLE : RIGHT_ANGLE, 0.0); + var blockPositionXZ = ((row - (_this.options.blocksPerLayer / 2) + 0.5) * (_this.options.blockSpacing + _this.options.blockSize.x)); var localTransform = Vec3.multiplyQbyV(_this.offsetRot, { - x: (layerRotated ? blockPositionXZ + offset: 0), + x: (layerRotated ? blockPositionXZ : 0), y: (_this.options.blockSize.y / 2) + (_this.options.blockSize.y * layer), - z: (layerRotated ? 0 : blockPositionXZ + offset) + z: (layerRotated ? 0 : blockPositionXZ) }); var newProperties = { type: 'Model', @@ -173,6 +194,7 @@ PlankyStack = function() { }; this.refresh = function() { refreshGround(); + refreshLines(); trimDimension('layer', _this.options.numLayers); trimDimension('row', _this.options.blocksPerLayer); _this.offsetRot = Quat.multiply(_this.baseRotation, Quat.fromPitchYawRollDegrees(0.0, _this.options.blockYawOffset, 0.0)); @@ -183,7 +205,6 @@ PlankyStack = function() { } if (!editMode) { _this.planks.forEach(function(plank, index, object) { - print(index + " , " + plank); Entities.editEntity(plank.entity, {ignoreForCollisions: false, collisionsWillMove: true}); // Entities.editEntity(plank.entity, {collisionsWillMove: true}); }); @@ -219,15 +240,6 @@ function getCogButtonPosX() { return getButtonPosX() - (BUTTON_DIMENSIONS.width * 1.1); } -print(JSON.stringify({ - x: getButtonPosX(), - y: 10, - width: BUTTON_DIMENSIONS.width, - height: BUTTON_DIMENSIONS.height, - imageURL: HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/planky_button.svg', - alpha: 0.8 -})); - function createButtons() { button = Overlays.addOverlay('image', { x: getButtonPosX(), From 321447ca6a13292e42f228bf54131f4862f3a28c Mon Sep 17 00:00:00 2001 From: Studio Date: Sun, 12 Jul 2015 14:44:38 -0400 Subject: [PATCH 004/242] Change default value of output buffer size frames to 20 as a default and added functionality to increase to 40. --- libraries/audio-client/src/AudioClient.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index aeea7c07c1..3a85adbc97 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -55,9 +55,9 @@ static const int NUM_AUDIO_CHANNELS = 2; -static const int DEFAULT_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 3; +static const int DEFAULT_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 20; static const int MIN_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 1; -static const int MAX_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 20; +static const int MAX_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 40; #if defined(Q_OS_ANDROID) || defined(Q_OS_WIN) static const int DEFAULT_AUDIO_OUTPUT_STARVE_DETECTION_ENABLED = false; #else From 1f453e07e33e20ccb7cf39bbbbdbe1969466d679 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 14 Jul 2015 02:18:17 +0200 Subject: [PATCH 005/242] connection between planky script and web-window -load settings -include toolbar --- examples/example/games/planky.js | 110 +++++++++------ examples/html/plankySettings.html | 214 ++++++++++++++++-------------- 2 files changed, 183 insertions(+), 141 deletions(-) diff --git a/examples/example/games/planky.js b/examples/example/games/planky.js index 82ae4af9e0..60c3b3ad3a 100644 --- a/examples/example/games/planky.js +++ b/examples/example/games/planky.js @@ -12,6 +12,8 @@ // HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; +Script.include("../../libraries/toolBars.js"); + const DEFAULT_NUM_LAYERS = 16; const DEFAULT_BASE_DIMENSION = { x: 7, y: 2, z: 7 }; const DEFAULT_BLOCKS_PER_LAYER = 3; @@ -30,7 +32,7 @@ const DEFAULT_BLOCK_YAW_OFFSET = 45; var editMode = false; -const BUTTON_DIMENSIONS = {width: 49, height: 49}; +const BUTTON_DIMENSIONS = {width: 50, height: 50}; const MAXIMUM_PERCENTAGE = 100.0; const NO_ANGLE = 0; const RIGHT_ANGLE = 90; @@ -42,12 +44,30 @@ var ground = false; var layerRotated = false; var button; var cogButton; +var toolBar; SettingsWindow = function() { + var _this = this; + this.plankyStack = null; this.webWindow = null; - this.init = function() { - this.webWindow = new WebWindow('Planky', Script.resolvePath('../../html/plankySettings.html'), 255, 500, true); - this.webWindow.setVisible(false); + this.init = function(plankyStack) { + _this.webWindow = new WebWindow('Planky', Script.resolvePath('../../html/plankySettings.html'), 255, 500, true); + _this.webWindow.setVisible(false); + _this.webWindow.eventBridge.webEventReceived.connect(_this.onWebEventReceived); + _this.plankyStack = plankyStack; + }; + this.sendData = function(data) { + _this.webWindow.eventBridge.emitScriptEvent(JSON.stringify(data)); + }; + this.onWebEventReceived = function(data) { + data = JSON.parse(data); + switch (data.action) { + case 'loaded': + _this.sendData({action: 'load', options: _this.plankyStack.options.getJSON()}) + break; + default: + Window.alert('unknown action ' + data.action); + } }; }; @@ -59,6 +79,24 @@ PlankyOptions = function() { this.load = function() { //TODO: load Planky Options here. }; + this.getJSON = function() { + return { + numLayers: _this.numLayers, + baseDimension: _this.baseDimension, + blocksPerLayer: _this.blocksPerLayer, + blockSize: _this.blockSize, + blockSpacing: _this.blockSpacing, + blockHeightVariation: _this.blockHeightVariation, + gravity: _this.gravity, + density: _this.density, + dampingFactor: _this.dampingFactor, + angularDampingFactor: _this.angularDampingFactor, + friction: _this.friction, + restitution: _this.restitution, + spawnDistance: _this.spawnDistance, + blockYawOffset: _this.blockYawOffset, + }; + } this.setDefaults = function() { _this.numLayers = DEFAULT_NUM_LAYERS; _this.baseDimension = DEFAULT_BASE_DIMENSION; @@ -74,6 +112,7 @@ PlankyOptions = function() { _this.restitution = DEFAULT_RESTITUTION; _this.spawnDistance = DEFAULT_SPAWN_DISTANCE; _this.blockYawOffset = DEFAULT_BLOCK_YAW_OFFSET; + }; this.setDefaults(); }; @@ -93,7 +132,7 @@ PlankyStack = function() { if (_this.ground) { Entities.deleteEntity(_this.ground); } - this.editLines.forEach(function(line) { + _this.editLines.forEach(function(line) { Entities.deleteEntity(line); }) if (_this.centerLine) { @@ -155,7 +194,7 @@ PlankyStack = function() { var found = false; var layerRotated = layer % 2 === 0; var layerRotation = Quat.fromPitchYawRollDegrees(0, layerRotated ? NO_ANGLE : RIGHT_ANGLE, 0.0); - var blockPositionXZ = ((row - (_this.options.blocksPerLayer / 2) + 0.5) * (_this.options.blockSpacing + _this.options.blockSize.x)); + var blockPositionXZ = (row - (_this.options.blocksPerLayer / 2) + 0.5) * (_this.options.blockSpacing + _this.options.blockSize.x); var localTransform = Vec3.multiplyQbyV(_this.offsetRot, { x: (layerRotated ? blockPositionXZ : 0), y: (_this.options.blockSize.y / 2) + (_this.options.blockSize.y * layer), @@ -217,9 +256,8 @@ PlankyStack = function() { }; var settingsWindow = new SettingsWindow(); -settingsWindow.init(); - var plankyStack = new PlankyStack(); +settingsWindow.init(plankyStack); function grabLowestJointY() { var jointNames = MyAvatar.getJointNames(); @@ -232,66 +270,58 @@ function grabLowestJointY() { return floorY; } -function getButtonPosX() { - return windowWidth - ((BUTTON_DIMENSIONS.width / 2) + BUTTON_DIMENSIONS.width); -} - -function getCogButtonPosX() { - return getButtonPosX() - (BUTTON_DIMENSIONS.width * 1.1); -} - function createButtons() { - button = Overlays.addOverlay('image', { - x: getButtonPosX(), - y: 10, + toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.games.planky", function (windowDimensions, toolbar) { + return { + x: windowDimensions.x - (toolbar.width * 1.1), + y: toolbar.height / 2 + }; + }); + button = toolBar.addTool({ width: BUTTON_DIMENSIONS.width, height: BUTTON_DIMENSIONS.height, imageURL: HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/planky_button.svg', - alpha: 0.8 + alpha: 0.8, + visible: true }); - cogButton = Overlays.addOverlay('image', { - x: getCogButtonPosX(), - y: 10, + cogButton = toolBar.addTool({ width: BUTTON_DIMENSIONS.width, height: BUTTON_DIMENSIONS.height, imageURL: HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/cog.svg', - alpha: 0.8 + alpha: 0.8, + visible: true }); } // Fixes bug of not showing buttons on startup -Script.setTimeout(createButtons, 1000); +Script.setTimeout(createButtons, 2000); Controller.mousePressEvent.connect(function(event) { var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); - if (clickedOverlay === button) { + if (toolBar.clicked(clickedOverlay) === button) { if (!plankyStack.isFound()) { plankyStack.rez(); return; } editMode = !editMode; plankyStack.refresh(); - } else if (clickedOverlay === cogButton) { + } else if (toolBar.clicked(clickedOverlay) === cogButton) { settingsWindow.webWindow.setVisible(true); } }); - -function cleanup() { - Overlays.deleteOverlay(button); - Overlays.deleteOverlay(cogButton); - if (ground) { - Entities.deleteEntity(ground); - } - plankyStack.deRez(); -} -function onUpdate() { +Script.update.connect(function() { if (windowWidth !== Window.innerWidth) { windowWidth = Window.innerWidth; Overlays.editOverlay(button, {x: getButtonPosX()}); Overlays.editOverlay(cogButton, {x: getCogButtonPosX()}); } -} +}) -Script.update.connect(onUpdate) -Script.scriptEnding.connect(cleanup); +Script.scriptEnding.connect(function() { + toolBar.cleanup(); + if (ground) { + Entities.deleteEntity(ground); + } + plankyStack.deRez(); +}); diff --git a/examples/html/plankySettings.html b/examples/html/plankySettings.html index 9e2a08c650..d5f6f78a71 100644 --- a/examples/html/plankySettings.html +++ b/examples/html/plankySettings.html @@ -1,110 +1,122 @@ - - - + + } + EventBridge.emitWebEvent(JSON.stringify({action: 'loaded'})); + +}); +
From 6d1df036174046d2139702bdf5dc64e882791191 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 14 Jul 2015 03:19:23 +0200 Subject: [PATCH 006/242] planky: load / save settings --- examples/example/games/planky.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/example/games/planky.js b/examples/example/games/planky.js index 60c3b3ad3a..abf1e63412 100644 --- a/examples/example/games/planky.js +++ b/examples/example/games/planky.js @@ -74,10 +74,18 @@ SettingsWindow = function() { PlankyOptions = function() { var _this = this; this.save = function() { - //TODO: save Planky Options here. + Settings.setValue('plankyOptions', JSON.stringify(_this.getJSON())); }; this.load = function() { - //TODO: load Planky Options here. + _this.setDefaults(); + var plankyOptions = Settings.getValue('plankyOptions') + if (plankyOptions === null || plankyOptions === '') { + return; + } + var options = JSON.parse(plankyOptions); + options.forEach(function(value, option, object) { + _this[option] = value; + }); }; this.getJSON = function() { return { @@ -112,9 +120,8 @@ PlankyOptions = function() { _this.restitution = DEFAULT_RESTITUTION; _this.spawnDistance = DEFAULT_SPAWN_DISTANCE; _this.blockYawOffset = DEFAULT_BLOCK_YAW_OFFSET; - }; - this.setDefaults(); + this.load(); }; // The PlankyStack exists out of rows and layers From 222234cf1d43576649526cafce4551804f174c26 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 14 Jul 2015 03:40:58 +0200 Subject: [PATCH 007/242] wild planky changes on value change --- examples/example/games/planky.js | 7 +++++++ examples/html/plankySettings.html | 11 +++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/examples/example/games/planky.js b/examples/example/games/planky.js index abf1e63412..0849481a61 100644 --- a/examples/example/games/planky.js +++ b/examples/example/games/planky.js @@ -65,6 +65,9 @@ SettingsWindow = function() { case 'loaded': _this.sendData({action: 'load', options: _this.plankyStack.options.getJSON()}) break; + case 'value_change': + _this.plankyStack.onValueChanged(data.option, data.value); + break; default: Window.alert('unknown action ' + data.action); } @@ -238,6 +241,10 @@ PlankyStack = function() { _this.planks.push({layer: layer, row: row, entity: Entities.addEntity(newProperties)}) } }; + this.onValueChanged = function(option, value) { + _this.options[option] = value; + _this.refresh(); + }; this.refresh = function() { refreshGround(); refreshLines(); diff --git a/examples/html/plankySettings.html b/examples/html/plankySettings.html index d5f6f78a71..a5ad2f2939 100644 --- a/examples/html/plankySettings.html +++ b/examples/html/plankySettings.html @@ -38,13 +38,21 @@ PropertyInput = function(key, label, value, attributes) { this.construct(); }; +var valueChangeHandler = function() { + EventBridge.emitWebEvent(JSON.stringify({ + action: 'value_change', + option: $(this).attr('name'), + value: properties[$(this).attr('name')].getValue() + })); +}; + NumberInput = function(key, label, value, attributes) { PropertyInput.call(this, key, label, value, attributes); }; NumberInput.prototype = Object.create(PropertyInput.prototype); NumberInput.prototype.constructor = NumberInput; NumberInput.prototype.createValue = function() { - this.input = $('').attr('name', this.key).attr('type', 'number').val(this.value); + this.input = $('').attr('name', this.key).attr('type', 'number').val(this.value).on('change', valueChangeHandler); if (this.attributes !== undefined) { this.input.attr(this.attributes); } @@ -114,7 +122,6 @@ $(function() { }); } EventBridge.emitWebEvent(JSON.stringify({action: 'loaded'})); - }); From c8453bec671cbec04f7245e6b2878d08900423d2 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 14 Jul 2015 03:50:06 +0200 Subject: [PATCH 008/242] planky: fix layer setting, restraints on values --- examples/html/plankySettings.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/html/plankySettings.html b/examples/html/plankySettings.html index a5ad2f2939..b357e83792 100644 --- a/examples/html/plankySettings.html +++ b/examples/html/plankySettings.html @@ -91,23 +91,23 @@ function addHeader(label) { $(function() { addHeader('Stack Settings'); - properties['numLayers'] = new NumberInput('layers', 'Layers', 17, {'min': 0, 'max': 300, 'step': 1}); + properties['numLayers'] = new NumberInput('numLayers', 'Layers', 17, {'min': 0, 'max': 300, 'step': 1}); properties['baseDimension'] = new CoordinateInput('baseDimension', 'Base dimension', {x: 7, y: 2, z: 7}); - properties['blocksPerLayer'] = new NumberInput('blocksPerLayer', 'Blocks per layer', 4); + properties['blocksPerLayer'] = new NumberInput('blocksPerLayer', 'Blocks per layer', 4, {'min': 1, 'max': 100, 'step': 1}); properties['blockSize'] = new CoordinateInput('blockSize', 'Block size', {x: 0.2, y: 0.1, z: 0.8}); properties['blockSpacing'] = new NumberInput('blockSpacing', 'Block spacing', properties['blockSize'].getValue().x / properties['blocksPerLayer'].getValue()); properties['blockSpacing'].addButton('btn-recalculate-spacing', 'Recalculate spacing'); $('#btn-recalculate-spacing').on('click', function() { properties['blockSpacing'].setValue(properties['blockSize'].getValue().x / properties['blocksPerLayer'].getValue()); }); - properties['blockHeightVariation'] = new NumberInput('blockHeightVariation', 'Block height variation (%)', 0.1); + properties['blockHeightVariation'] = new NumberInput('blockHeightVariation', 'Block height variation (%)', 0.1, {'min': 0, 'max': 1, 'step': 0.01}); addHeader('Physics Settings'); properties['gravity'] = new CoordinateInput('gravity', 'Gravity', {x: 0, y: -2.8, z: 0}); - properties['density'] = new NumberInput('density', 'Density', 4000); - properties['dampingFactor'] = new NumberInput('dampingFactor', 'Damping factor', 0.98); - properties['angularDampingFactor'] = new NumberInput('angularDampingFactor', 'Angular damping factor', 0.8); - properties['friction'] = new NumberInput('friction', 'Friction', 0.99); - properties['restitution'] = new NumberInput('restitution', 'Restitution', 0.0); + properties['density'] = new NumberInput('density', 'Density', 4000, {'min': 0, 'max': 4000, 'step': 1}); + properties['dampingFactor'] = new NumberInput('dampingFactor', 'Damping factor', 0.98, {'min': 0, 'max': 1, 'step': 0.01}); + properties['angularDampingFactor'] = new NumberInput('angularDampingFactor', 'Angular damping factor', 0.8, {'min': 0, 'max': 1, 'step': 0.01}); + properties['friction'] = new NumberInput('friction', 'Friction', 0.99, {'min': 0, 'max': 1, 'step': 0.01}); + properties['restitution'] = new NumberInput('restitution', 'Restitution', 0.0, {'min': 0, 'max': 1, 'step': 0.01}); addHeader('Spawn Settings'); properties['spawnDistance'] = new NumberInput('spawnDistance', 'Spawn distance (meters)', 3); properties['blockYawOffset'] = new NumberInput('blockYawOffset', 'Block yaw offset (degrees)', 45, {'min': 0, 'max': 360, 'step': 1}); From 95948851f1427767e2a8372153a70064aa4d3dda Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 14 Jul 2015 04:36:29 +0200 Subject: [PATCH 009/242] save, clear and reset button --- examples/html/plankySettings.html | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/examples/html/plankySettings.html b/examples/html/plankySettings.html index b357e83792..0a79df6c7b 100644 --- a/examples/html/plankySettings.html +++ b/examples/html/plankySettings.html @@ -5,6 +5,9 @@ From b7110227963d187aca08cfe06de26501c0243546 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 14 Jul 2015 12:28:49 +0200 Subject: [PATCH 010/242] planky: - removed workaround for delayed overlay loading - make buttons functional (reset, cleanup, save-default) - only show live changes for the visual planky properties: blocksize , numLayers etc. (no physical properties) --- examples/example/games/planky.js | 74 +++++++++++++++++++------------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/examples/example/games/planky.js b/examples/example/games/planky.js index 0849481a61..5232201449 100644 --- a/examples/example/games/planky.js +++ b/examples/example/games/planky.js @@ -68,14 +68,28 @@ SettingsWindow = function() { case 'value_change': _this.plankyStack.onValueChanged(data.option, data.value); break; + case 'factory-reset': + _this.plankyStack.options.factoryReset(); + _this.sendData({action: 'load', options: _this.plankyStack.options.getJSON()}) + break; + case 'save-default': + _this.plankyStack.options.save(); + break; + case 'cleanup': + _this.plankyStack.deRez(); + break; default: - Window.alert('unknown action ' + data.action); + Window.alert('[planky] unknown action ' + data.action); } }; }; PlankyOptions = function() { var _this = this; + this.factoryReset = function() { + _this.setDefaults(); + Settings.setValue('plankyOptions', ''); + }; this.save = function() { Settings.setValue('plankyOptions', JSON.stringify(_this.getJSON())); }; @@ -86,9 +100,9 @@ PlankyOptions = function() { return; } var options = JSON.parse(plankyOptions); - options.forEach(function(value, option, object) { - _this[option] = value; - }); + for (option in options) { + _this[option] = options[option]; + } }; this.getJSON = function() { return { @@ -147,9 +161,10 @@ PlankyStack = function() { }) if (_this.centerLine) { Entities.deleteEntity(_this.centerLine); - } + } _this.ground = false; _this.centerLine = false; + _this.editLines = []; }; this.rez = function() { if (_this.planks.length > 0) { @@ -243,7 +258,9 @@ PlankyStack = function() { }; this.onValueChanged = function(option, value) { _this.options[option] = value; - _this.refresh(); + if (['numLayers', 'blocksPerLayer', 'blockSize', 'blockSpacing', 'blockHeightVariation'].indexOf(option) !== -1) { + _this.refresh(); + } }; this.refresh = function() { refreshGround(); @@ -284,31 +301,28 @@ function grabLowestJointY() { return floorY; } -function createButtons() { - toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.games.planky", function (windowDimensions, toolbar) { - return { - x: windowDimensions.x - (toolbar.width * 1.1), - y: toolbar.height / 2 - }; - }); - button = toolBar.addTool({ - width: BUTTON_DIMENSIONS.width, - height: BUTTON_DIMENSIONS.height, - imageURL: HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/planky_button.svg', - alpha: 0.8, - visible: true - }); +toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.games.planky", function (windowDimensions, toolbar) { + return { + x: windowDimensions.x - (toolbar.width * 1.1), + y: toolbar.height / 2 + }; +}); - cogButton = toolBar.addTool({ - width: BUTTON_DIMENSIONS.width, - height: BUTTON_DIMENSIONS.height, - imageURL: HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/cog.svg', - alpha: 0.8, - visible: true - }); -} -// Fixes bug of not showing buttons on startup -Script.setTimeout(createButtons, 2000); +button = toolBar.addTool({ + width: BUTTON_DIMENSIONS.width, + height: BUTTON_DIMENSIONS.height, + imageURL: HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/planky_button.svg', + alpha: 0.8, + visible: true +}); + +cogButton = toolBar.addTool({ + width: BUTTON_DIMENSIONS.width, + height: BUTTON_DIMENSIONS.height, + imageURL: HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/cog.svg', + alpha: 0.8, + visible: true +}); Controller.mousePressEvent.connect(function(event) { var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); From fac497dadc139f3c1e649178c1ccf2fc2c0e4e14 Mon Sep 17 00:00:00 2001 From: Marcel Verhagen Date: Tue, 14 Jul 2015 15:31:56 +0200 Subject: [PATCH 011/242] Removed the file where the directory is stripped off the RelativeFilename, and replace \ with /. However this introduces a new problem with the current fbx files who do not have their textures in the original dir but moved it to the dir where the fbx file lives. Suppose there should be some logic when the texture is not found to look if the texture lives in the dir above the RelativeFilename. In any case it is not obvious when there is a texture missing on a model, it simply renders black. --- libraries/fbx/src/FBXReader.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 236e2979f5..24cbc983ed 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1781,9 +1781,8 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, TextureParam tex; foreach (const FBXNode& subobject, object.children) { if (subobject.name == "RelativeFilename") { - // trim off any path information QByteArray filename = subobject.properties.at(0).toByteArray(); - filename = filename.mid(qMax(filename.lastIndexOf('\\'), filename.lastIndexOf('/')) + 1); + filename = filename.replace('\\', '/'); textureFilenames.insert(getID(object.properties), filename); } else if (subobject.name == "TextureName") { // trim the name from the timestamp From 6926ae9aa3bcdb474360a89dff4f01ba24dbd5cd Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 14 Jul 2015 20:27:51 +0200 Subject: [PATCH 012/242] small planky improvements --- examples/example/games/planky.js | 19 +++++++++++++------ examples/html/plankySettings.html | 21 +++++++++++---------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/examples/example/games/planky.js b/examples/example/games/planky.js index 5232201449..6733749317 100644 --- a/examples/example/games/planky.js +++ b/examples/example/games/planky.js @@ -65,7 +65,7 @@ SettingsWindow = function() { case 'loaded': _this.sendData({action: 'load', options: _this.plankyStack.options.getJSON()}) break; - case 'value_change': + case 'value-change': _this.plankyStack.onValueChanged(data.option, data.value); break; case 'factory-reset': @@ -148,6 +148,7 @@ PlankyStack = function() { this.ground = false; this.editLines = []; this.options = new PlankyOptions(); + this.deRez = function() { _this.planks.forEach(function(plank) { Entities.deleteEntity(plank.entity); @@ -159,13 +160,14 @@ PlankyStack = function() { _this.editLines.forEach(function(line) { Entities.deleteEntity(line); }) + _this.editLines = []; if (_this.centerLine) { Entities.deleteEntity(_this.centerLine); } _this.ground = false; _this.centerLine = false; - _this.editLines = []; }; + this.rez = function() { if (_this.planks.length > 0) { _this.deRez(); @@ -192,7 +194,7 @@ PlankyStack = function() { } // move ground to rez position/rotation Entities.editEntity(_this.ground, {dimensions: _this.options.baseDimension, position: Vec3.sum(_this.basePosition, {y: -(_this.options.baseDimension.y / 2)}), rotation: _this.baseRotation}); - } + }; var refreshLines = function() { if (_this.editLines.length === 0) { @@ -206,7 +208,8 @@ PlankyStack = function() { })); return; } - } + }; + var trimDimension = function(dimension, maxIndex) { _this.planks.forEach(function(plank, index, object) { if (plank[dimension] > maxIndex) { @@ -215,6 +218,7 @@ PlankyStack = function() { } }); }; + var createOrUpdate = function(layer, row) { var found = false; var layerRotated = layer % 2 === 0; @@ -256,17 +260,19 @@ PlankyStack = function() { _this.planks.push({layer: layer, row: row, entity: Entities.addEntity(newProperties)}) } }; + this.onValueChanged = function(option, value) { _this.options[option] = value; if (['numLayers', 'blocksPerLayer', 'blockSize', 'blockSpacing', 'blockHeightVariation'].indexOf(option) !== -1) { _this.refresh(); } }; + this.refresh = function() { refreshGround(); refreshLines(); - trimDimension('layer', _this.options.numLayers); - trimDimension('row', _this.options.blocksPerLayer); + trimDimension('layer', _this.options.numLayers - 1); + trimDimension('row', _this.options.blocksPerLayer - 1); _this.offsetRot = Quat.multiply(_this.baseRotation, Quat.fromPitchYawRollDegrees(0.0, _this.options.blockYawOffset, 0.0)); for (var layer = 0; layer < _this.options.numLayers; layer++) { for (var row = 0; row < _this.options.blocksPerLayer; row++) { @@ -280,6 +286,7 @@ PlankyStack = function() { }); } }; + this.isFound = function() { //TODO: identify entities here until one is found return _this.planks.length > 0; diff --git a/examples/html/plankySettings.html b/examples/html/plankySettings.html index 0a79df6c7b..0eea4948ad 100644 --- a/examples/html/plankySettings.html +++ b/examples/html/plankySettings.html @@ -42,10 +42,11 @@ PropertyInput = function(key, label, value, attributes) { }; var valueChangeHandler = function() { + sendWebEvent({ - action: 'value_change', - option: $(this).attr('name'), - value: properties[$(this).attr('name')].getValue() + action: 'value-change', + option: $(this).data('var-name'), + value: properties[$(this).data('var-name')].getValue() }); }; @@ -55,14 +56,14 @@ NumberInput = function(key, label, value, attributes) { NumberInput.prototype = Object.create(PropertyInput.prototype); NumberInput.prototype.constructor = NumberInput; NumberInput.prototype.createValue = function() { - this.input = $('').attr('name', this.key).attr('type', 'number').val(this.value).on('change', valueChangeHandler); + this.input = $('').data('var-name', this.key).attr('name', this.key).attr('type', 'number').val(this.value).on('change', valueChangeHandler); if (this.attributes !== undefined) { this.input.attr(this.attributes); } return this.input; }; NumberInput.prototype.getValue = function() { - return this.input.val(); + return parseFloat(this.input.val()); }; CoordinateInput = function(key, label, value, attributes) { @@ -71,9 +72,9 @@ CoordinateInput = function(key, label, value, attributes) { CoordinateInput.prototype = Object.create(PropertyInput.prototype); CoordinateInput.prototype.constructor = CoordinateInput; CoordinateInput.prototype.createValue = function() { - this.inputX = $('').attr('name', this.key + '-x').attr('type', 'number').addClass('coord').val(this.value.x); - this.inputY = $('').attr('name', this.key + '-y').attr('type', 'number').addClass('coord').val(this.value.y); - this.inputZ = $('').attr('name', this.key + '-z').attr('type', 'number').addClass('coord').val(this.value.z); + this.inputX = $('').data('var-name', this.key).attr('name', this.key + '-x').attr('type', 'number').addClass('coord').val(this.value.x).on('change', valueChangeHandler); + this.inputY = $('').data('var-name', this.key).attr('name', this.key + '-y').attr('type', 'number').addClass('coord').val(this.value.y).on('change', valueChangeHandler); + this.inputZ = $('').data('var-name', this.key).attr('name', this.key + '-z').attr('type', 'number').addClass('coord').val(this.value.z).on('change', valueChangeHandler); if (this.attributes !== undefined) { this.inputX.attr(this.attributes); this.inputY.attr(this.attributes); @@ -82,7 +83,7 @@ CoordinateInput.prototype.createValue = function() { return [encapsulateInput(this.inputX, 'X'), encapsulateInput(this.inputY, 'Y'), encapsulateInput(this.inputZ, 'Z')]; }; CoordinateInput.prototype.getValue = function() { - return {x: this.inputX.val(), y: this.inputY.val(), z: this.inputZ.val()}; + return {x: parseFloat(this.inputX.val()), y: parseFloat(this.inputY.val()), z: parseFloat(this.inputZ.val())}; }; function encapsulateInput(input, label) { return $('
').addClass('input-area').append(label + ' ').append(input); @@ -95,7 +96,6 @@ function addHeader(label) { $(function() { addHeader('Stack Settings'); properties['numLayers'] = new NumberInput('numLayers', 'Layers', 17, {'min': 0, 'max': 300, 'step': 1}); - properties['baseDimension'] = new CoordinateInput('baseDimension', 'Base dimension', {x: 7, y: 2, z: 7}, {'min': 0.5, 'max': 200, 'step': 0.1}); properties['blocksPerLayer'] = new NumberInput('blocksPerLayer', 'Blocks per layer', 4, {'min': 1, 'max': 100, 'step': 1}); properties['blockSize'] = new CoordinateInput('blockSize', 'Block size', {x: 0.2, y: 0.1, z: 0.8}, {'min': 0.05, 'max': 20, 'step': 0.1}); properties['blockSpacing'] = new NumberInput('blockSpacing', 'Block spacing', properties['blockSize'].getValue().x / properties['blocksPerLayer'].getValue(), {'min': 0, 'max': 20, 'step': 0.01}); @@ -114,6 +114,7 @@ $(function() { addHeader('Spawn Settings'); properties['spawnDistance'] = new NumberInput('spawnDistance', 'Spawn distance (meters)', 3); properties['blockYawOffset'] = new NumberInput('blockYawOffset', 'Block yaw offset (degrees)', 45, {'min': 0, 'max': 360, 'step': 1}); + properties['baseDimension'] = new CoordinateInput('baseDimension', 'Base dimension', {x: 7, y: 2, z: 7}, {'min': 0.5, 'max': 200, 'step': 0.1}); addHeader('Actions'); $('#properties-list') .append($('').val('factory reset').attr('type', 'button').on('click', function() { sendWebEvent({action: 'factory-reset'}); })) From 1b13f837bd052cdc9441aff87795c89cb5b237f7 Mon Sep 17 00:00:00 2001 From: Niraj Venkat Date: Wed, 15 Jul 2015 14:10:17 -0700 Subject: [PATCH 013/242] Porting AO to new pipeline --- interface/src/Application.cpp | 10 +- .../src/AmbientOcclusionEffect.cpp | 121 ++++++++++++++---- .../render-utils/src/AmbientOcclusionEffect.h | 36 +++++- .../render-utils/src/RenderDeferredTask.cpp | 4 + .../render-utils/src/ambient_occlusion.slf | 22 ++++ .../render-utils/src/ambient_occlusion.slv | 25 ++++ 6 files changed, 179 insertions(+), 39 deletions(-) create mode 100644 libraries/render-utils/src/ambient_occlusion.slf create mode 100644 libraries/render-utils/src/ambient_occlusion.slv diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 00a4440920..51aec20481 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2154,7 +2154,6 @@ void Application::init() { _environment.init(); DependencyManager::get()->init(this); - DependencyManager::get()->init(this); // TODO: move _myAvatar out of Application. Move relevant code to MyAvataar or AvatarManager DependencyManager::get()->init(); @@ -3479,14 +3478,6 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se renderArgs->_debugFlags = renderDebugFlags; _entities.render(renderArgs); } - - // render the ambient occlusion effect if enabled - if (Menu::getInstance()->isOptionChecked(MenuOption::AmbientOcclusion)) { - PerformanceTimer perfTimer("ambientOcclusion"); - PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), - "Application::displaySide() ... AmbientOcclusion..."); - DependencyManager::get()->render(); - } } // Make sure the WorldBox is in the scene @@ -3564,6 +3555,7 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se sceneInterface->setEngineFeedOverlay3DItems(engineRC->_numFeedOverlay3DItems); sceneInterface->setEngineDrawnOverlay3DItems(engineRC->_numDrawnOverlay3DItems); } + //Render the sixense lasers if (Menu::getInstance()->isOptionChecked(MenuOption::SixenseLasers)) { _myAvatar->renderLaserPointers(*renderArgs->_batch); diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index f58419ec6e..82034cad12 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -25,12 +25,17 @@ #include "ProgramObject.h" #include "RenderUtil.h" #include "TextureCache.h" +#include "DependencyManager.h" +#include "ViewFrustum.h" + +#include "ambient_occlusion_vert.h" +#include "ambient_occlusion_frag.h" const int ROTATION_WIDTH = 4; const int ROTATION_HEIGHT = 4; void AmbientOcclusionEffect::init(AbstractViewStateInterface* viewState) { - _viewState = viewState; // we will use this for view state services + /*_viewState = viewState; // we will use this for view state services _occlusionProgram = new ProgramObject(); _occlusionProgram->addShaderFromSourceFile(QGLShader::Vertex, PathUtils::resourcesPath() @@ -93,27 +98,27 @@ void AmbientOcclusionEffect::init(AbstractViewStateInterface* viewState) { _blurProgram->setUniformValue("originalTexture", 0); _blurProgram->release(); - _blurScaleLocation = _blurProgram->uniformLocation("blurScale"); + _blurScaleLocation = _blurProgram->uniformLocation("blurScale");*/ } -void AmbientOcclusionEffect::render() { - glDisable(GL_BLEND); +void AmbientOcclusionEffect::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext){ + /*glDisable(GL_BLEND); glDisable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); - + glBindTexture(GL_TEXTURE_2D, DependencyManager::get()->getPrimaryDepthTextureID()); - + glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, _rotationTextureID); - + // render with the occlusion shader to the secondary/tertiary buffer auto freeFramebuffer = DependencyManager::get()->getFreeFramebuffer(); glBindFramebuffer(GL_FRAMEBUFFER, gpu::GLBackend::getFramebufferID(freeFramebuffer)); - + float left, right, bottom, top, nearVal, farVal; glm::vec4 nearClipPlane, farClipPlane; - _viewState->computeOffAxisFrustum(left, right, bottom, top, nearVal, farVal, nearClipPlane, farClipPlane); - + AbstractViewStateInterface::instance()->computeOffAxisFrustum(left, right, bottom, top, nearVal, farVal, nearClipPlane, farClipPlane); + int viewport[4]; glGetIntegerv(GL_VIEWPORT, viewport); const int VIEWPORT_X_INDEX = 0; @@ -122,7 +127,7 @@ void AmbientOcclusionEffect::render() { auto framebufferSize = DependencyManager::get()->getFrameBufferSize(); float sMin = viewport[VIEWPORT_X_INDEX] / (float)framebufferSize.width(); float sWidth = viewport[VIEWPORT_WIDTH_INDEX] / (float)framebufferSize.width(); - + _occlusionProgram->bind(); _occlusionProgram->setUniformValue(_nearLocation, nearVal); _occlusionProgram->setUniformValue(_farLocation, farVal); @@ -132,37 +137,107 @@ void AmbientOcclusionEffect::render() { framebufferSize.height() / (float)ROTATION_HEIGHT); _occlusionProgram->setUniformValue(_texCoordOffsetLocation, sMin, 0.0f); _occlusionProgram->setUniformValue(_texCoordScaleLocation, sWidth, 1.0f); - + renderFullscreenQuad(); - + _occlusionProgram->release(); - + glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindTexture(GL_TEXTURE_2D, 0); - + glActiveTexture(GL_TEXTURE0); - + // now render secondary to primary with 4x4 blur auto primaryFramebuffer = DependencyManager::get()->getPrimaryFramebuffer(); glBindFramebuffer(GL_FRAMEBUFFER, gpu::GLBackend::getFramebufferID(primaryFramebuffer)); glEnable(GL_BLEND); glBlendFuncSeparate(GL_ZERO, GL_SRC_COLOR, GL_ZERO, GL_ONE); - + auto freeFramebufferTexture = freeFramebuffer->getRenderBuffer(0); glBindTexture(GL_TEXTURE_2D, gpu::GLBackend::getTextureID(freeFramebufferTexture)); - + _blurProgram->bind(); _blurProgram->setUniformValue(_blurScaleLocation, 1.0f / framebufferSize.width(), 1.0f / framebufferSize.height()); - + renderFullscreenQuad(sMin, sMin + sWidth); - + _blurProgram->release(); - + glBindTexture(GL_TEXTURE_2D, 0); - + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); glEnable(GL_DEPTH_TEST); - glDepthMask(GL_TRUE); + glDepthMask(GL_TRUE);*/ } + + +AmbientOcclusion::AmbientOcclusion() { +} + +const gpu::PipelinePointer& AmbientOcclusion::getAOPipeline() { + if (!_AOPipeline) { + auto vs = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(ambient_occlusion_vert))); + auto ps = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(ambient_occlusion_frag))); + gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vs, ps)); + + gpu::Shader::BindingSet slotBindings; + gpu::Shader::makeProgram(*program, slotBindings); + + //_drawItemBoundPosLoc = program->getUniforms().findLocation("inBoundPos"); + //_drawItemBoundDimLoc = program->getUniforms().findLocation("inBoundDim"); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + state->setDepthTest(true, false, gpu::LESS_EQUAL); + + // Blend on transparent + state->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::DEST_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ZERO); + + // Good to go add the brand new pipeline + _AOPipeline.reset(gpu::Pipeline::create(program, state)); + } + return _AOPipeline; +} + +void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext) { + + // create a simple pipeline that does: + + assert(renderContext->args); + assert(renderContext->args->_viewFrustum); + RenderArgs* args = renderContext->args; + auto& scene = sceneContext->_scene; + + // Allright, something to render let's do it + gpu::Batch batch; + + glm::mat4 projMat; + Transform viewMat; + args->_viewFrustum->evalProjectionMatrix(projMat); + args->_viewFrustum->evalViewTransform(viewMat); + if (args->_renderMode == RenderArgs::MIRROR_RENDER_MODE) { + viewMat.postScale(glm::vec3(-1.0f, 1.0f, 1.0f)); + } + batch.setProjectionTransform(projMat); + batch.setViewTransform(viewMat); + batch.setModelTransform(Transform()); + + // bind the one gpu::Pipeline we need + batch.setPipeline(getAOPipeline()); + + //renderFullscreenQuad(); + + args->_context->syncCache(); + renderContext->args->_context->syncCache(); + args->_context->render((batch)); + + // need to fetch forom the z buffer and render something in a new render target a result that combine the z and produce a fake AO result + + + +} + diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.h b/libraries/render-utils/src/AmbientOcclusionEffect.h index c7acb90133..fbe4a09214 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.h +++ b/libraries/render-utils/src/AmbientOcclusionEffect.h @@ -14,23 +14,30 @@ #include +#include "render/DrawTask.h" + class AbstractViewStateInterface; class ProgramObject; /// A screen space ambient occlusion effect. See John Chapman's tutorial at /// http://john-chapman-graphics.blogspot.co.uk/2013/01/ssao-tutorial.html for reference. + class AmbientOcclusionEffect : public Dependency { SINGLETON_DEPENDENCY - + public: - + void init(AbstractViewStateInterface* viewState); - void render(); - -private: + + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); + typedef render::Job::Model JobModel; + + AmbientOcclusionEffect() {} virtual ~AmbientOcclusionEffect() {} +private: + ProgramObject* _occlusionProgram; int _nearLocation; int _farLocation; @@ -39,12 +46,27 @@ private: int _noiseScaleLocation; int _texCoordOffsetLocation; int _texCoordScaleLocation; - + ProgramObject* _blurProgram; int _blurScaleLocation; - + GLuint _rotationTextureID; AbstractViewStateInterface* _viewState; }; +class AmbientOcclusion { +public: + + AmbientOcclusion(); + + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); + typedef render::Job::Model JobModel; + + const gpu::PipelinePointer& AmbientOcclusion::getAOPipeline(); + +private: + + gpu::PipelinePointer _AOPipeline; +}; + #endif // hifi_AmbientOcclusionEffect_h diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 55f4f72574..e4740cc8de 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -18,6 +18,7 @@ #include "TextureCache.h" #include "render/DrawStatus.h" +#include "AmbientOcclusionEffect.h" #include @@ -71,10 +72,13 @@ RenderDeferredTask::RenderDeferredTask() : Task() { _jobs.push_back(Job(new DrawTransparentDeferred::JobModel("TransparentDeferred", _jobs.back().getOutput()))); _jobs.push_back(Job(new render::DrawStatus::JobModel("DrawStatus", renderedOpaques))); + + _jobs.back().setEnabled(false); _drawStatusJobIndex = _jobs.size() - 1; _jobs.push_back(Job(new DrawOverlay3D::JobModel("DrawOverlay3D"))); + _jobs.push_back(Job(new AmbientOcclusion::JobModel("AmbientOcclusion"))); _jobs.push_back(Job(new ResetGLState::JobModel())); // Give ourselves 3 frmaes of timer queries diff --git a/libraries/render-utils/src/ambient_occlusion.slf b/libraries/render-utils/src/ambient_occlusion.slf new file mode 100644 index 0000000000..67cf9d3c53 --- /dev/null +++ b/libraries/render-utils/src/ambient_occlusion.slf @@ -0,0 +1,22 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// simple.frag +// fragment shader +// +// Created by Andrzej Kapolka on 9/15/14. +// 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 DeferredBufferWrite.slh@> + +// the interpolated normal +//varying vec4 interpolatedNormal; + +void main(void) { + gl_FragColor = vec4(0.0, 1.0, 1.0, 1.0); +} diff --git a/libraries/render-utils/src/ambient_occlusion.slv b/libraries/render-utils/src/ambient_occlusion.slv new file mode 100644 index 0000000000..702b1bd59e --- /dev/null +++ b/libraries/render-utils/src/ambient_occlusion.slv @@ -0,0 +1,25 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// simple.vert +// vertex shader +// +// Created by Andrzej Kapolka on 9/15/14. +// 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 gpu/Transform.slh@> + +<$declareStandardTransform()$> + +// the interpolated normal +//varying vec4 interpolatedNormal; + +void main(void) { + //gl_TexCoord[0] = gl_MultiTexCoord0; + gl_Position = gl_Vertex; +} \ No newline at end of file From bbb5f832a2abcd79e9989d3bb39e5fd3b0890c47 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 15 Jul 2015 15:11:16 -0700 Subject: [PATCH 014/242] Empty Rig. --- libraries/animation/src/Rig.cpp | 9 +++++++++ libraries/animation/src/Rig.h | 14 ++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 libraries/animation/src/Rig.cpp create mode 100644 libraries/animation/src/Rig.h diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp new file mode 100644 index 0000000000..b6cda25966 --- /dev/null +++ b/libraries/animation/src/Rig.cpp @@ -0,0 +1,9 @@ +// +// Rig.cpp +// hifi +// +// Created by Howard Stearns on 7/15/15. +// +// + +#include "Rig.h" diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h new file mode 100644 index 0000000000..3ccea315b8 --- /dev/null +++ b/libraries/animation/src/Rig.h @@ -0,0 +1,14 @@ +// +// Rig.h +// hifi +// +// Created by Howard Stearns on 7/15/15. +// +// + +#ifndef __hifi__Rig__ +#define __hifi__Rig__ + +#include + +#endif /* defined(__hifi__Rig__) */ From 5312993e56ea34ebfc555f8ebccd9f6d1537acf4 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 15 Jul 2015 15:30:34 -0700 Subject: [PATCH 015/242] Hmm, last was missing some emptiness. --- libraries/animation/src/Rig.cpp | 7 +++++-- libraries/animation/src/Rig.h | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index b6cda25966..c8e86319cb 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1,9 +1,12 @@ // // Rig.cpp -// hifi +// libraries/script-engine/src/ // -// Created by Howard Stearns on 7/15/15. +// Created by Howard Stearns, Seth Alves, Anthony Thibault, Andrew Meadows on 7/15/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. // +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "Rig.h" diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 3ccea315b8..df4fe31d84 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -1,14 +1,23 @@ // // Rig.h -// hifi +// libraries/script-engine/src/ // -// Created by Howard Stearns on 7/15/15. +// Produces animation data and hip placement for the current timestamp. // +// Created by Howard Stearns, Seth Alves, Anthony Thibault, Andrew Meadows on 7/15/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// 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__Rig__ #define __hifi__Rig__ -#include +#include + +class AnimationObject : public QObject { + +}; #endif /* defined(__hifi__Rig__) */ From 6ca458b62469038f2c1b8b769611e5de0876b395 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 15 Jul 2015 15:59:24 -0700 Subject: [PATCH 016/242] Instantiated in MyAvatar --- interface/src/avatar/MyAvatar.h | 2 ++ libraries/animation/src/Rig.h | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index a2566118f2..0d94d327a9 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -14,6 +14,7 @@ #include #include +#include #include "Avatar.h" @@ -285,6 +286,7 @@ private: QString _bodyModelName; QString _fullAvatarModelName; + Rig _rig; // used for rendering when in first person view or when in an HMD. SkeletonModel _firstPersonSkeletonModel; bool _prevShouldDrawHead; diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index df4fe31d84..3e9307385b 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -16,7 +16,7 @@ #include -class AnimationObject : public QObject { +class Rig : public QObject { }; From b93ae7b5273dcf305fc9ebdc5e57b7b3261641e2 Mon Sep 17 00:00:00 2001 From: "Kevin M. Thomas" Date: Wed, 15 Jul 2015 19:25:18 -0400 Subject: [PATCH 017/242] Move reload content item to developer menu. --- interface/src/Menu.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 8b14830c2d..6fe3a4dff9 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -255,8 +255,6 @@ Menu::Menu() { avatar, SLOT(updateMotionBehavior())); MenuWrapper* viewMenu = addMenu("View"); - - addActionToQMenuAndActionHash(viewMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Fullscreen, @@ -329,6 +327,8 @@ Menu::Menu() { MenuWrapper* developerMenu = addMenu("Developer"); + addActionToQMenuAndActionHash(developerMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); + MenuWrapper* renderOptionsMenu = developerMenu->addMenu("Render"); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Atmosphere, 0, // QML Qt::SHIFT | Qt::Key_A, From 671263909a6db098f1f28c49d828682e7b9acbfb Mon Sep 17 00:00:00 2001 From: "Kevin M. Thomas" Date: Wed, 15 Jul 2015 19:47:37 -0400 Subject: [PATCH 018/242] Moved reload content item to nested Network menu item. --- interface/src/Menu.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 6fe3a4dff9..7315c02825 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -327,8 +327,6 @@ Menu::Menu() { MenuWrapper* developerMenu = addMenu("Developer"); - addActionToQMenuAndActionHash(developerMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); - MenuWrapper* renderOptionsMenu = developerMenu->addMenu("Render"); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Atmosphere, 0, // QML Qt::SHIFT | Qt::Key_A, @@ -491,6 +489,7 @@ Menu::Menu() { #endif MenuWrapper* networkMenu = developerMenu->addMenu("Network"); + addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableNackPackets, 0, false); addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableActivityLogger, From b87ed7dc189c3add5bb149f11164900ba44f871a Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 15 Jul 2015 17:05:57 -0700 Subject: [PATCH 019/242] Remove dead code. --- interface/src/avatar/Hand.cpp | 53 ----------------------------------- interface/src/avatar/Hand.h | 7 ----- interface/src/avatar/Head.h | 1 - 3 files changed, 61 deletions(-) diff --git a/interface/src/avatar/Hand.cpp b/interface/src/avatar/Hand.cpp index 7b2968973c..aee032aa93 100644 --- a/interface/src/avatar/Hand.cpp +++ b/interface/src/avatar/Hand.cpp @@ -14,7 +14,6 @@ #include #include -#include #include "AvatarManager.h" #include "Hand.h" @@ -43,58 +42,6 @@ void Hand::simulate(float deltaTime, bool isMine) { } } -// We create a static CollisionList that is recycled for each collision test. -const float MAX_COLLISIONS_PER_AVATAR = 32; -static CollisionList handCollisions(MAX_COLLISIONS_PER_AVATAR); - -void Hand::collideAgainstAvatar(Avatar* avatar, bool isMyHand) { - if (!avatar || avatar == _owningAvatar) { - // don't collide hands against ourself (that is done elsewhere) - return; - } - - const SkeletonModel& skeletonModel = _owningAvatar->getSkeletonModel(); - int jointIndices[2]; - jointIndices[0] = skeletonModel.getLeftHandJointIndex(); - jointIndices[1] = skeletonModel.getRightHandJointIndex(); - - for (size_t i = 0; i < 2; i++) { - int jointIndex = jointIndices[i]; - if (jointIndex < 0) { - continue; - } - - handCollisions.clear(); - QVector shapes; - skeletonModel.getHandShapes(jointIndex, shapes); - - if (avatar->findCollisions(shapes, handCollisions)) { - glm::vec3 totalPenetration(0.0f); - glm::vec3 averageContactPoint; - for (int j = 0; j < handCollisions.size(); ++j) { - CollisionInfo* collision = handCollisions.getCollision(j); - totalPenetration += collision->_penetration; - averageContactPoint += collision->_contactPoint; - } - if (isMyHand) { - // our hand against other avatar - // TODO: resolve this penetration when we don't think the other avatar will yield - //palm.addToPenetration(averagePenetration); - } else { - // someone else's hand against MyAvatar - // TODO: submit collision info to MyAvatar which should lean accordingly - averageContactPoint /= (float)handCollisions.size(); - avatar->applyCollision(averageContactPoint, totalPenetration); - - CollisionInfo collision; - collision._penetration = totalPenetration; - collision._contactPoint = averageContactPoint; - emit avatar->collisionWithAvatar(avatar->getSessionUUID(), _owningAvatar->getSessionUUID(), collision); - } - } - } -} - void Hand::resolvePenetrations() { for (size_t i = 0; i < getNumPalms(); ++i) { PalmData& palm = getPalms()[i]; diff --git a/interface/src/avatar/Hand.h b/interface/src/avatar/Hand.h index f6991c5a55..5e070d53e0 100644 --- a/interface/src/avatar/Hand.h +++ b/interface/src/avatar/Hand.h @@ -30,11 +30,6 @@ class Avatar; -class ProgramObject; - -const float HAND_PADDLE_OFFSET = 0.1f; -const float HAND_PADDLE_THICKNESS = 0.01f; -const float HAND_PADDLE_RADIUS = 0.15f; class Hand : public HandData { public: @@ -43,8 +38,6 @@ public: void simulate(float deltaTime, bool isMine); void render(RenderArgs* renderArgs, bool isMine); - void collideAgainstAvatar(Avatar* avatar, bool isMyHand); - void resolvePenetrations(); private: diff --git a/interface/src/avatar/Head.h b/interface/src/avatar/Head.h index 3f839d53bc..2baa16f90c 100644 --- a/interface/src/avatar/Head.h +++ b/interface/src/avatar/Head.h @@ -26,7 +26,6 @@ const float EYE_EAR_GAP = 0.08f; class Avatar; -class ProgramObject; class Head : public HeadData { public: From 022529f03f967216c83fc649262df07dbfce0986 Mon Sep 17 00:00:00 2001 From: Niraj Venkat Date: Thu, 16 Jul 2015 09:46:28 -0700 Subject: [PATCH 020/242] Render depth buffer to quad Useful for graphics debugging --- .../src/AmbientOcclusionEffect.cpp | 13 ++++++++++--- .../render-utils/src/RenderDeferredTask.cpp | 4 +++- .../render-utils/src/ambient_occlusion.slf | 18 ++++++++++++++---- .../render-utils/src/ambient_occlusion.slv | 9 +++++---- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index 82034cad12..07832179cc 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -27,6 +27,7 @@ #include "TextureCache.h" #include "DependencyManager.h" #include "ViewFrustum.h" +#include "GeometryCache.h" #include "ambient_occlusion_vert.h" #include "ambient_occlusion_frag.h" @@ -190,10 +191,10 @@ const gpu::PipelinePointer& AmbientOcclusion::getAOPipeline() { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - state->setDepthTest(true, false, gpu::LESS_EQUAL); + state->setDepthTest(false, false, gpu::LESS_EQUAL); // Blend on transparent - state->setBlendFunction(true, + state->setBlendFunction(false, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::DEST_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ZERO); @@ -225,11 +226,17 @@ void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, cons batch.setProjectionTransform(projMat); batch.setViewTransform(viewMat); batch.setModelTransform(Transform()); + batch.setResourceTexture(0, DependencyManager::get()->getPrimaryDepthTexture()); // bind the one gpu::Pipeline we need batch.setPipeline(getAOPipeline()); - //renderFullscreenQuad(); + glm::vec4 color(0.0f, 0.0f, 0.0f, 1.0f); + glm::vec2 bottomLeft(0.5f, -1.0f); + glm::vec2 topRight(1.0f, -0.5f); + glm::vec2 texCoordTopLeft(0.0f, 0.0f); + glm::vec2 texCoordBottomRight(1.0f, 1.0f); + DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); args->_context->syncCache(); renderContext->args->_context->syncCache(); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index e9a5689747..5b4d8e9d4f 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -59,6 +59,7 @@ RenderDeferredTask::RenderDeferredTask() : Task() { _jobs.push_back(Job(new ResetGLState::JobModel())); _jobs.push_back(Job(new RenderDeferred::JobModel("RenderDeferred"))); _jobs.push_back(Job(new ResolveDeferred::JobModel("ResolveDeferred"))); + _jobs.push_back(Job(new AmbientOcclusion::JobModel("AmbientOcclusion"))); _jobs.push_back(Job(new FetchItems::JobModel("FetchTransparent", FetchItems( ItemFilter::Builder::transparentShape().withoutLayered(), @@ -77,8 +78,9 @@ RenderDeferredTask::RenderDeferredTask() : Task() { _jobs.back().setEnabled(false); _drawStatusJobIndex = _jobs.size() - 1; + //_jobs.push_back(Job(new AmbientOcclusion::JobModel("AmbientOcclusion"))); _jobs.push_back(Job(new DrawOverlay3D::JobModel("DrawOverlay3D"))); - _jobs.push_back(Job(new AmbientOcclusion::JobModel("AmbientOcclusion"))); + _jobs.push_back(Job(new ResetGLState::JobModel())); // Give ourselves 3 frmaes of timer queries diff --git a/libraries/render-utils/src/ambient_occlusion.slf b/libraries/render-utils/src/ambient_occlusion.slf index 67cf9d3c53..b56e0eb9d5 100644 --- a/libraries/render-utils/src/ambient_occlusion.slf +++ b/libraries/render-utils/src/ambient_occlusion.slf @@ -2,11 +2,11 @@ <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> // -// simple.frag +// ambient_occlusion.frag // fragment shader // -// Created by Andrzej Kapolka on 9/15/14. -// Copyright 2014 High Fidelity, Inc. +// Created by Niraj Venkat on 7/15/15. +// 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 @@ -17,6 +17,16 @@ // the interpolated normal //varying vec4 interpolatedNormal; +varying vec2 varTexcoord; + +uniform sampler2D depthTexture; + void main(void) { - gl_FragColor = vec4(0.0, 1.0, 1.0, 1.0); + vec4 depthColor = texture2D(depthTexture, varTexcoord.xy); + float z = depthColor.r; // fetch the z-value from our depth texture + float n = 1.0; // the near plane + float f = 30.0; // the far plane + float c = (2.0 * n) / (f + n - z * (f - n)); // convert to linear values + + gl_FragColor = vec4(c, c, c, 1.0); } diff --git a/libraries/render-utils/src/ambient_occlusion.slv b/libraries/render-utils/src/ambient_occlusion.slv index 702b1bd59e..54cc608129 100644 --- a/libraries/render-utils/src/ambient_occlusion.slv +++ b/libraries/render-utils/src/ambient_occlusion.slv @@ -2,11 +2,11 @@ <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> // -// simple.vert +// ambient_occlusion.vert // vertex shader // -// Created by Andrzej Kapolka on 9/15/14. -// Copyright 2014 High Fidelity, Inc. +// Created by Niraj Venkat on 7/15/15. +// 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 @@ -18,8 +18,9 @@ // the interpolated normal //varying vec4 interpolatedNormal; +varying vec2 varTexcoord; void main(void) { - //gl_TexCoord[0] = gl_MultiTexCoord0; + varTexcoord = gl_MultiTexCoord0.xy; gl_Position = gl_Vertex; } \ No newline at end of file From a529cce626540513090b59b2730cf7c1894050e5 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 16 Jul 2015 14:36:28 -0700 Subject: [PATCH 021/242] Empty test suite and instructions. --- tests/rig/CMakeLists.txt | 9 ++++++ tests/rig/src/RigTests.cpp | 31 +++++++++++++++++++++ tests/rig/src/RigTests.h | 56 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 tests/rig/CMakeLists.txt create mode 100644 tests/rig/src/RigTests.cpp create mode 100644 tests/rig/src/RigTests.h diff --git a/tests/rig/CMakeLists.txt b/tests/rig/CMakeLists.txt new file mode 100644 index 0000000000..abf5da12c2 --- /dev/null +++ b/tests/rig/CMakeLists.txt @@ -0,0 +1,9 @@ +# Declare dependencies +macro (setup_testcase_dependencies) + # link in the shared libraries + link_hifi_libraries(shared animation) + + copy_dlls_beside_windows_executable() +endmacro () + +setup_hifi_testcase() diff --git a/tests/rig/src/RigTests.cpp b/tests/rig/src/RigTests.cpp new file mode 100644 index 0000000000..c32e136fd1 --- /dev/null +++ b/tests/rig/src/RigTests.cpp @@ -0,0 +1,31 @@ +// +// RigTests.cpp +// tests/rig/src +// +// Created by Howard Stearns on 6/16/15 +// 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 "RigTests.h" + +QTEST_MAIN(RigTests) + +void RigTests::initTestCase() { + _rig = new Rig(); +} + +void RigTests::dummyPassTest() { + bool x = true; + std::cout << "dummyPassTest x=" << x << std::endl; + QCOMPARE(x, true); +} + +void RigTests::dummyFailTest() { + bool x = false; + std::cout << "dummyFailTest x=" << x << std::endl; + QCOMPARE(x, true); +} diff --git a/tests/rig/src/RigTests.h b/tests/rig/src/RigTests.h new file mode 100644 index 0000000000..ddb8de9c2d --- /dev/null +++ b/tests/rig/src/RigTests.h @@ -0,0 +1,56 @@ +// +// RigTests.h +// tests/rig/src +// +// Created by Howard Stearns on 6/16/15 +// 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_RigTests_h +#define hifi_RigTests_h + +#include +#include + +#include "../QTestExtensions.h" + + +// The QTest terminology is not consistent with itself or with industry: +// The whole directory, and the rig-tests target, doesn't seem to be a QTest concept, an corresponds roughly to a toplevel suite of suites. +// The directory can contain any number of classes like this one. (Don't forget to wipe your build dir, and rerun cmake when you add one.): +// QTest doc (http://doc.qt.io/qt-5/qtest-overview.html) calls this a "test case". +// The output of QTest's 'ctest' runner calls this a "test" when run in the whole directory (e.g., when reporting success/failure counts). +// The test case (like this class) can contain any number of test slots: +// QTest doc calls these "test functions" +// When you run a single test case executable (e.g., "rig-RigTests"), the (unlabeled) count includes these test functions and also the before method, which is auto generated as initTestCase. + +// To build and run via make: +// make help | grep tests # shows all test targets, including all-tests and rig-tests. +// make all-tests # will compile and then die as soon as any test case dies, even if its not in your directory +// make rig-tests # will compile and run `ctest .` in the tests/rig directory, running all the test cases found there. +// Alas, only summary output is shown on stdout. The real results, including any stdout that your code does, is in tests/rig/Testing/Temporary/LastTest.log, or... +// tests/rig/rig-RigTests (or the executable corresponding to any test case you define here) will run just that case and give output directly. +// +// To build and run via Xcode: +// On some machines, xcode can't find cmake on the path it uses. I did, effectively: sudo ln -s `which cmake` /usr/bin +// Note the above make instructions. +// all-tests, rig-tests, and rig-RigTests are all targets: +// The first two of these show no output at all, but if there's a failure you can see it by clicking on the red failure in the "issue navigator" (or by externally viewing the .log above). +// The last (or any other individual test case executable) does show output in the Xcode output display. + +class RigTests : public QObject { + Q_OBJECT + + private slots: + void initTestCase(); + void dummyPassTest(); + void dummyFailTest(); + + private: + Rig* _rig; +}; + +#endif // hifi_RigTests_h From bcd6b30ec3c3c7c53a7b3b8a0de0ca1be5130ad0 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 17 Jul 2015 13:31:48 -0700 Subject: [PATCH 022/242] move AnimationHandle from render-utils to animation. give Rig some jointstates and animation lists --- interface/src/Camera.h | 2 +- interface/src/avatar/FaceModel.cpp | 1 - interface/src/avatar/MyAvatar.cpp | 6 +- interface/src/ui/AnimationsDialog.cpp | 2 +- interface/src/ui/overlays/Cube3DOverlay.cpp | 1 - .../src/ui/overlays/LocalModelsOverlay.cpp | 2 +- .../src/AnimationHandle.cpp | 100 +++++++++--------- .../src/AnimationHandle.h | 75 ++++++++----- .../src/JointState.cpp | 0 .../src/JointState.h | 0 libraries/animation/src/Rig.cpp | 81 ++++++++++++++ libraries/animation/src/Rig.h | 33 +++++- libraries/render-utils/src/Model.cpp | 15 +-- libraries/render-utils/src/Model.h | 1 - 14 files changed, 220 insertions(+), 99 deletions(-) rename libraries/{render-utils => animation}/src/AnimationHandle.cpp (69%) rename libraries/{render-utils => animation}/src/AnimationHandle.h (69%) rename libraries/{render-utils => animation}/src/JointState.cpp (100%) rename libraries/{render-utils => animation}/src/JointState.h (100%) diff --git a/interface/src/Camera.h b/interface/src/Camera.h index 6eed39cf16..bddb01ef21 100644 --- a/interface/src/Camera.h +++ b/interface/src/Camera.h @@ -28,7 +28,7 @@ enum CameraMode }; Q_DECLARE_METATYPE(CameraMode); -static int cameraModeId = qRegisterMetaType(); +// static int cameraModeId = qRegisterMetaType(); class Camera : public QObject { Q_OBJECT diff --git a/interface/src/avatar/FaceModel.cpp b/interface/src/avatar/FaceModel.cpp index 170965bb4d..16b370d459 100644 --- a/interface/src/avatar/FaceModel.cpp +++ b/interface/src/avatar/FaceModel.cpp @@ -49,7 +49,6 @@ void FaceModel::simulate(float deltaTime, bool fullUpdate) { } void FaceModel::maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, JointState& state) { - Avatar* owningAvatar = static_cast(_owningHead->_owningAvatar); // get the rotation axes in joint space and use them to adjust the rotation glm::mat3 axes = glm::mat3_cast(glm::quat()); glm::mat3 inverse = glm::mat3(glm::inverse(parentState.getTransform() * diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 053ce7b3cc..537b6f339a 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -488,7 +488,7 @@ void MyAvatar::loadLastRecording() { } AnimationHandlePointer MyAvatar::addAnimationHandle() { - AnimationHandlePointer handle = _skeletonModel.createAnimationHandle(); + AnimationHandlePointer handle = _rig.createAnimationHandle(); _animationHandles.append(handle); return handle; } @@ -506,7 +506,7 @@ void MyAvatar::startAnimation(const QString& url, float fps, float priority, Q_ARG(float, lastFrame), Q_ARG(const QStringList&, maskedJoints)); return; } - AnimationHandlePointer handle = _skeletonModel.createAnimationHandle(); + AnimationHandlePointer handle = _rig.createAnimationHandle(); handle->setURL(url); handle->setFPS(fps); handle->setPriority(priority); @@ -534,7 +534,7 @@ void MyAvatar::startAnimationByRole(const QString& role, const QString& url, flo } } // no joy; use the parameters provided - AnimationHandlePointer handle = _skeletonModel.createAnimationHandle(); + AnimationHandlePointer handle = _rig.createAnimationHandle(); handle->setRole(role); handle->setURL(url); handle->setFPS(fps); diff --git a/interface/src/ui/AnimationsDialog.cpp b/interface/src/ui/AnimationsDialog.cpp index 0428e79e6f..5960ffc1fa 100644 --- a/interface/src/ui/AnimationsDialog.cpp +++ b/interface/src/ui/AnimationsDialog.cpp @@ -156,7 +156,7 @@ AnimationPanel::AnimationPanel(AnimationsDialog* dialog, const AnimationHandlePo buttons->addWidget(remove); connect(remove, SIGNAL(clicked(bool)), SLOT(removeHandle())); - _stop->connect(_handle.data(), SIGNAL(runningChanged(bool)), SLOT(setEnabled(bool))); + _stop->connect(_handle.get(), SIGNAL(runningChanged(bool)), SLOT(setEnabled(bool))); _stop->setEnabled(_handle->isRunning()); } diff --git a/interface/src/ui/overlays/Cube3DOverlay.cpp b/interface/src/ui/overlays/Cube3DOverlay.cpp index 7f6fd5f971..6c32281e75 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.cpp +++ b/interface/src/ui/overlays/Cube3DOverlay.cpp @@ -35,7 +35,6 @@ void Cube3DOverlay::render(RenderArgs* args) { // TODO: handle registration point?? glm::vec3 position = getPosition(); - glm::vec3 center = getCenter(); glm::vec3 dimensions = getDimensions(); glm::quat rotation = getRotation(); diff --git a/interface/src/ui/overlays/LocalModelsOverlay.cpp b/interface/src/ui/overlays/LocalModelsOverlay.cpp index 06b27f8f22..7f86e339e8 100644 --- a/interface/src/ui/overlays/LocalModelsOverlay.cpp +++ b/interface/src/ui/overlays/LocalModelsOverlay.cpp @@ -30,7 +30,7 @@ void LocalModelsOverlay::update(float deltatime) { void LocalModelsOverlay::render(RenderArgs* args) { if (_visible) { - float glowLevel = getGlowLevel(); // FIXME, glowing removed for now + // float glowLevel = getGlowLevel(); // FIXME, glowing removed for now auto batch = args ->_batch; Application* app = Application::getInstance(); diff --git a/libraries/render-utils/src/AnimationHandle.cpp b/libraries/animation/src/AnimationHandle.cpp similarity index 69% rename from libraries/render-utils/src/AnimationHandle.cpp rename to libraries/animation/src/AnimationHandle.cpp index 6ad6730952..7f45fb600e 100644 --- a/libraries/render-utils/src/AnimationHandle.cpp +++ b/libraries/animation/src/AnimationHandle.cpp @@ -10,7 +10,7 @@ // #include "AnimationHandle.h" -#include "Model.h" + void AnimationHandle::setURL(const QUrl& url) { if (_url != url) { @@ -20,28 +20,17 @@ void AnimationHandle::setURL(const QUrl& url) { } } -static void insertSorted(QList& handles, const AnimationHandlePointer& handle) { - for (QList::iterator it = handles.begin(); it != handles.end(); it++) { - if (handle->getPriority() > (*it)->getPriority()) { - handles.insert(it, handle); - return; - } - } - handles.append(handle); -} - void AnimationHandle::setPriority(float priority) { if (_priority == priority) { return; } if (isRunning()) { - _model->_runningAnimations.removeOne(_self); + _rig->removeRunningAnimation(getAnimationHandlePointer()); if (priority < _priority) { replaceMatchingPriorities(priority); } _priority = priority; - insertSorted(_model->_runningAnimations, _self); - + _rig->addRunningAnimation(getAnimationHandlePointer()); } else { _priority = priority; } @@ -68,21 +57,21 @@ void AnimationHandle::setRunning(bool running) { } _animationLoop.setRunning(running); if (isRunning()) { - if (!_model->_runningAnimations.contains(_self)) { - insertSorted(_model->_runningAnimations, _self); + if (!_rig->isRunningAnimation(getAnimationHandlePointer())) { + _rig->addRunningAnimation(getAnimationHandlePointer()); } } else { - _model->_runningAnimations.removeOne(_self); + _rig->removeRunningAnimation(getAnimationHandlePointer()); restoreJoints(); replaceMatchingPriorities(0.0f); } emit runningChanged(isRunning()); } -AnimationHandle::AnimationHandle(Model* model) : - QObject(model), - _model(model), - _priority(1.0f) +AnimationHandle::AnimationHandle(RigPointer rig) : + QObject(rig.get()), + _rig(rig), + _priority(1.0f) { } @@ -110,42 +99,52 @@ void AnimationHandle::setAnimationDetails(const AnimationDetails& details) { } +void AnimationHandle::setJointMappings(QVector jointMappings) { + _jointMappings = jointMappings; +} + + void AnimationHandle::simulate(float deltaTime) { if (!_animation || !_animation->isLoaded()) { return; } - + _animationLoop.simulate(deltaTime); - - // update the joint mappings if necessary/possible + if (_jointMappings.isEmpty()) { - if (_model && _model->isActive()) { - _jointMappings = _model->getGeometry()->getJointMappings(_animation); - } - if (_jointMappings.isEmpty()) { - return; - } - if (!_maskedJoints.isEmpty()) { - const FBXGeometry& geometry = _model->getGeometry()->getFBXGeometry(); - for (int i = 0; i < _jointMappings.size(); i++) { - int& mapping = _jointMappings[i]; - if (mapping != -1 && _maskedJoints.contains(geometry.joints.at(mapping).name)) { - mapping = -1; - } - } - } + qDebug() << "AnimationHandle::simulate -- _jointMappings.isEmpty()"; + return; } - + + // // update the joint mappings if necessary/possible + // if (_jointMappings.isEmpty()) { + // if (_model && _model->isActive()) { + // _jointMappings = _model->getGeometry()->getJointMappings(_animation); + // } + // if (_jointMappings.isEmpty()) { + // return; + // } + // if (!_maskedJoints.isEmpty()) { + // const FBXGeometry& geometry = _model->getGeometry()->getFBXGeometry(); + // for (int i = 0; i < _jointMappings.size(); i++) { + // int& mapping = _jointMappings[i]; + // if (mapping != -1 && _maskedJoints.contains(geometry.joints.at(mapping).name)) { + // mapping = -1; + // } + // } + // } + // } + const FBXGeometry& animationGeometry = _animation->getGeometry(); if (animationGeometry.animationFrames.isEmpty()) { stop(); return; } - + if (_animationLoop.getMaxFrameIndexHint() != animationGeometry.animationFrames.size()) { _animationLoop.setMaxFrameIndexHint(animationGeometry.animationFrames.size()); } - + // blend between the closest two frames applyFrame(getFrameIndex()); } @@ -154,17 +153,21 @@ void AnimationHandle::applyFrame(float frameIndex) { if (!_animation || !_animation->isLoaded()) { return; } - + const FBXGeometry& animationGeometry = _animation->getGeometry(); int frameCount = animationGeometry.animationFrames.size(); const FBXAnimationFrame& floorFrame = animationGeometry.animationFrames.at((int)glm::floor(frameIndex) % frameCount); const FBXAnimationFrame& ceilFrame = animationGeometry.animationFrames.at((int)glm::ceil(frameIndex) % frameCount); float frameFraction = glm::fract(frameIndex); + QVector jointStates = _rig->getJointStates(); for (int i = 0; i < _jointMappings.size(); i++) { int mapping = _jointMappings.at(i); if (mapping != -1) { - JointState& state = _model->_jointStates[mapping]; - state.setRotationInConstrainedFrame(safeMix(floorFrame.rotations.at(i), ceilFrame.rotations.at(i), frameFraction), _priority); + JointState& state = jointStates[mapping]; + state.setRotationInConstrainedFrame(safeMix(floorFrame.rotations.at(i), + ceilFrame.rotations.at(i), + frameFraction), + _priority); } } } @@ -173,7 +176,8 @@ void AnimationHandle::replaceMatchingPriorities(float newPriority) { for (int i = 0; i < _jointMappings.size(); i++) { int mapping = _jointMappings.at(i); if (mapping != -1) { - JointState& state = _model->_jointStates[mapping]; + QVector jointStates = _rig->getJointStates(); + JointState& state = jointStates[mapping]; if (_priority == state._animationPriority) { state._animationPriority = newPriority; } @@ -185,9 +189,9 @@ void AnimationHandle::restoreJoints() { for (int i = 0; i < _jointMappings.size(); i++) { int mapping = _jointMappings.at(i); if (mapping != -1) { - JointState& state = _model->_jointStates[mapping]; + QVector jointStates = _rig->getJointStates(); + JointState& state = jointStates[mapping]; state.restoreRotation(1.0f, state._animationPriority); } } } - diff --git a/libraries/render-utils/src/AnimationHandle.h b/libraries/animation/src/AnimationHandle.h similarity index 69% rename from libraries/render-utils/src/AnimationHandle.h rename to libraries/animation/src/AnimationHandle.h index ca9c2eb6d0..a8c9d800a4 100644 --- a/libraries/render-utils/src/AnimationHandle.h +++ b/libraries/animation/src/AnimationHandle.h @@ -18,22 +18,43 @@ #include #include -#include -#include +#include "AnimationCache.h" +#include "AnimationLoop.h" +#include "Rig.h" class AnimationHandle; class Model; -typedef QSharedPointer AnimationHandlePointer; -typedef QWeakPointer WeakAnimationHandlePointer; +typedef std::shared_ptr AnimationHandlePointer; +typedef std::weak_ptr WeakAnimationHandlePointer; +inline uint qHash(const std::shared_ptr& a, uint seed) { + // return qHash(a.get(), seed); + AnimationHandle* strongRef = a ? a.get() : nullptr; + return qHash(strongRef, seed); +} +inline uint qHash(const std::weak_ptr& a, uint seed) { + AnimationHandlePointer strongPointer = a.lock(); + AnimationHandle* strongRef = strongPointer ? strongPointer.get() : nullptr; + return qHash(strongRef, seed); +} + + +// inline uint qHash(const WeakAnimationHandlePointer& handle, uint seed) { +// return qHash(handle.data(), seed); +// } + /// Represents a handle to a model animation. -class AnimationHandle : public QObject { +class AnimationHandle : public QObject, public std::enable_shared_from_this { Q_OBJECT - + public: + AnimationHandle(RigPointer rig); + + AnimationHandlePointer getAnimationHandlePointer() { return shared_from_this(); } + void setRole(const QString& role) { _role = role; } const QString& getRole() const { return _role; } @@ -45,26 +66,26 @@ public: void setMaskedJoints(const QStringList& maskedJoints); const QStringList& getMaskedJoints() const { return _maskedJoints; } - + void setFPS(float fps) { _animationLoop.setFPS(fps); } float getFPS() const { return _animationLoop.getFPS(); } void setLoop(bool loop) { _animationLoop.setLoop(loop); } bool getLoop() const { return _animationLoop.getLoop(); } - + void setHold(bool hold) { _animationLoop.setHold(hold); } bool getHold() const { return _animationLoop.getHold(); } - + void setStartAutomatically(bool startAutomatically); bool getStartAutomatically() const { return _animationLoop.getStartAutomatically(); } - + void setFirstFrame(float firstFrame) { _animationLoop.setFirstFrame(firstFrame); } float getFirstFrame() const { return _animationLoop.getFirstFrame(); } - + void setLastFrame(float lastFrame) { _animationLoop.setLastFrame(lastFrame); } float getLastFrame() const { return _animationLoop.getLastFrame(); } - + void setRunning(bool running); bool isRunning() const { return _animationLoop.isRunning(); } @@ -74,30 +95,25 @@ public: AnimationDetails getAnimationDetails() const; void setAnimationDetails(const AnimationDetails& details); + void setJointMappings(QVector jointMappings); + void simulate(float deltaTime); + void applyFrame(float frameIndex); + void replaceMatchingPriorities(float newPriority); + void restoreJoints(); + void clearJoints() { _jointMappings.clear(); } + signals: - + void runningChanged(bool running); public slots: void start() { setRunning(true); } void stop() { setRunning(false); } - + private: - friend class Model; - - AnimationHandle(Model* model); - - void simulate(float deltaTime); - void applyFrame(float frameIndex); - void replaceMatchingPriorities(float newPriority); - void restoreJoints(); - - void clearJoints() { _jointMappings.clear(); } - - Model* _model; - WeakAnimationHandlePointer _self; + RigPointer _rig; AnimationPointer _animation; QString _role; QUrl _url; @@ -105,8 +121,11 @@ private: QStringList _maskedJoints; QVector _jointMappings; - + AnimationLoop _animationLoop; + + static QHash, QVector> _jointMappingsCache; + static QVector getJointMappings(const AnimationPointer& animation); }; diff --git a/libraries/render-utils/src/JointState.cpp b/libraries/animation/src/JointState.cpp similarity index 100% rename from libraries/render-utils/src/JointState.cpp rename to libraries/animation/src/JointState.cpp diff --git a/libraries/render-utils/src/JointState.h b/libraries/animation/src/JointState.h similarity index 100% rename from libraries/render-utils/src/JointState.h rename to libraries/animation/src/JointState.h diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index c8e86319cb..0825a615b7 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -9,4 +9,85 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "AnimationHandle.h" + #include "Rig.h" + +bool Rig::removeRunningAnimation(AnimationHandlePointer animationHandle) { + return _runningAnimations.removeOne(animationHandle); +} + +static void insertSorted(QList& handles, const AnimationHandlePointer& handle) { + for (QList::iterator it = handles.begin(); it != handles.end(); it++) { + if (handle->getPriority() > (*it)->getPriority()) { + handles.insert(it, handle); + return; + } + } + handles.append(handle); +} + +void Rig::addRunningAnimation(AnimationHandlePointer animationHandle) { + insertSorted(_runningAnimations, animationHandle); +} + +bool Rig::isRunningAnimation(AnimationHandlePointer animationHandle) { + return _runningAnimations.contains(animationHandle); +} + +void Rig::initJointStates(glm::vec3 scale, glm::vec3 offset, QVector states) { + _jointStates = states; + initJointTransforms(scale, offset); + + int numStates = _jointStates.size(); + float radius = 0.0f; + for (int i = 0; i < numStates; ++i) { + float distance = glm::length(_jointStates[i].getPosition()); + if (distance > radius) { + radius = distance; + } + _jointStates[i].buildConstraint(); + } + for (int i = 0; i < _jointStates.size(); i++) { + _jointStates[i].slaveVisibleTransform(); + } + + // XXX update AnimationHandles from here? +} + +void Rig::initJointTransforms(glm::vec3 scale, glm::vec3 offset) { + // compute model transforms + int numStates = _jointStates.size(); + for (int i = 0; i < numStates; ++i) { + JointState& state = _jointStates[i]; + const FBXJoint& joint = state.getFBXJoint(); + int parentIndex = joint.parentIndex; + if (parentIndex == -1) { + // const FBXGeometry& geometry = _geometry->getFBXGeometry(); + // NOTE: in practice geometry.offset has a non-unity scale (rather than a translation) + glm::mat4 parentTransform = glm::scale(scale) * glm::translate(offset); // * geometry.offset; XXX + state.initTransform(parentTransform); + } else { + const JointState& parentState = _jointStates.at(parentIndex); + state.initTransform(parentState.getTransform()); + } + } +} + +void Rig::resetJoints() { + if (_jointStates.isEmpty()) { + return; + } + + // const FBXGeometry& geometry = _geometry->getFBXGeometry(); + for (int i = 0; i < _jointStates.size(); i++) { + const FBXJoint& fbxJoint = _jointStates[i].getFBXJoint(); + _jointStates[i].setRotationInConstrainedFrame(fbxJoint.rotation, 0.0f); + } +} + +AnimationHandlePointer Rig::createAnimationHandle() { + AnimationHandlePointer handle(new AnimationHandle(getRigPointer())); + _animationHandles.insert(handle); + return handle; +} diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 3e9307385b..eccb0960dd 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -16,8 +16,39 @@ #include -class Rig : public QObject { +#include "JointState.h" +class AnimationHandle; +typedef std::shared_ptr AnimationHandlePointer; +// typedef QWeakPointer WeakAnimationHandlePointer; + +class Rig; +typedef std::shared_ptr RigPointer; + + +class Rig : public QObject, public std::enable_shared_from_this { + +public: + RigPointer getRigPointer() { return shared_from_this(); } + + bool removeRunningAnimation(AnimationHandlePointer animationHandle); + void addRunningAnimation(AnimationHandlePointer animationHandle); + bool isRunningAnimation(AnimationHandlePointer animationHandle); + const QList& getRunningAnimations() const { return _runningAnimations; } + + void initJointStates(glm::vec3 scale, glm::vec3 offset, QVector states); + void initJointTransforms(glm::vec3 scale, glm::vec3 offset); + void resetJoints(); + + QVector getJointStates() { return _jointStates; } + + AnimationHandlePointer createAnimationHandle(); + +protected: + QVector _jointStates; + + QSet _animationHandles; + QList _runningAnimations; }; #endif /* defined(__hifi__Rig__) */ diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 07df7fbda4..409d24652c 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1255,17 +1255,6 @@ QStringList Model::getJointNames() const { return isActive() ? _geometry->getFBXGeometry().getJointNames() : QStringList(); } -uint qHash(const WeakAnimationHandlePointer& handle, uint seed) { - return qHash(handle.data(), seed); -} - -AnimationHandlePointer Model::createAnimationHandle() { - AnimationHandlePointer handle(new AnimationHandle(this)); - handle->_self = handle; - _animationHandles.insert(handle); - return handle; -} - // virtual override from PhysicsEntity void Model::buildShapes() { // TODO: figure out how to load/build collision shapes for general models @@ -1830,9 +1819,9 @@ void Model::deleteGeometry() { clearShapes(); for (QSet::iterator it = _animationHandles.begin(); it != _animationHandles.end(); ) { - AnimationHandlePointer handle = it->toStrongRef(); + AnimationHandlePointer handle = it->lock(); if (handle) { - handle->_jointMappings.clear(); + handle->clearJoints(); it++; } else { it = _animationHandles.erase(it); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index b3a62b8da7..2d9bf7f146 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -209,7 +209,6 @@ public: QStringList getJointNames() const; - AnimationHandlePointer createAnimationHandle(); const QList& getRunningAnimations() const { return _runningAnimations; } From d287817829ef932d4961cc8653fd10bbdece27db Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 17 Jul 2015 14:52:37 -0700 Subject: [PATCH 023/242] give SkeletonModel created by MyAvatar a Rig pointer --- interface/src/avatar/MyAvatar.cpp | 11 ++++++----- interface/src/avatar/MyAvatar.h | 2 +- interface/src/avatar/SkeletonModel.cpp | 4 ++-- interface/src/avatar/SkeletonModel.h | 2 +- libraries/render-utils/src/Model.cpp | 13 ++++++++----- libraries/render-utils/src/Model.h | 8 +++++--- 6 files changed, 23 insertions(+), 17 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 537b6f339a..896128aee8 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -79,7 +79,7 @@ const float MyAvatar::ZOOM_MAX = 25.0f; const float MyAvatar::ZOOM_DEFAULT = 1.5f; MyAvatar::MyAvatar() : - Avatar(), + Avatar(), _gravity(0.0f, 0.0f, 0.0f), _wasPushing(false), _isPushing(false), @@ -102,7 +102,8 @@ MyAvatar::MyAvatar() : _eyeContactTarget(LEFT_EYE), _realWorldFieldOfView("realWorldFieldOfView", DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), - _firstPersonSkeletonModel(this), + _rig(), + _firstPersonSkeletonModel(this, nullptr, _rig), _prevShouldDrawHead(true) { _firstPersonSkeletonModel.setIsFirstPerson(true); @@ -488,7 +489,7 @@ void MyAvatar::loadLastRecording() { } AnimationHandlePointer MyAvatar::addAnimationHandle() { - AnimationHandlePointer handle = _rig.createAnimationHandle(); + AnimationHandlePointer handle = _rig->createAnimationHandle(); _animationHandles.append(handle); return handle; } @@ -506,7 +507,7 @@ void MyAvatar::startAnimation(const QString& url, float fps, float priority, Q_ARG(float, lastFrame), Q_ARG(const QStringList&, maskedJoints)); return; } - AnimationHandlePointer handle = _rig.createAnimationHandle(); + AnimationHandlePointer handle = _rig->createAnimationHandle(); handle->setURL(url); handle->setFPS(fps); handle->setPriority(priority); @@ -534,7 +535,7 @@ void MyAvatar::startAnimationByRole(const QString& role, const QString& url, flo } } // no joy; use the parameters provided - AnimationHandlePointer handle = _rig.createAnimationHandle(); + AnimationHandlePointer handle = _rig->createAnimationHandle(); handle->setRole(role); handle->setURL(url); handle->setFPS(fps); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 0d94d327a9..8ad2f946ec 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -286,7 +286,7 @@ private: QString _bodyModelName; QString _fullAvatarModelName; - Rig _rig; + RigPointer _rig; // used for rendering when in first person view or when in an HMD. SkeletonModel _firstPersonSkeletonModel; bool _prevShouldDrawHead; diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 0a981706da..0347e217a3 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -29,8 +29,8 @@ enum StandingFootState { NO_FOOT }; -SkeletonModel::SkeletonModel(Avatar* owningAvatar, QObject* parent) : - Model(parent), +SkeletonModel::SkeletonModel(Avatar* owningAvatar, QObject* parent, RigPointer rig) : + Model(parent, rig), _triangleFanID(DependencyManager::get()->allocateID()), _owningAvatar(owningAvatar), _boundingShape(), diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 3d63238cf2..6d33b3da7b 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -25,7 +25,7 @@ class SkeletonModel : public Model { public: - SkeletonModel(Avatar* owningAvatar, QObject* parent = NULL); + SkeletonModel(Avatar* owningAvatar, QObject* parent = nullptr, RigPointer rig = nullptr); ~SkeletonModel(); virtual void initJointStates(QVector states); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 409d24652c..7bb96a9082 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -62,7 +62,7 @@ static int weakNetworkGeometryPointerTypeId = qRegisterMetaType >(); float Model::FAKE_DIMENSION_PLACEHOLDER = -1.0f; -Model::Model(QObject* parent) : +Model::Model(QObject* parent, RigPointer rig) : QObject(parent), _scale(1.0f, 1.0f, 1.0f), _scaleToFit(false), @@ -83,7 +83,8 @@ Model::Model(QObject* parent) : _calculatedMeshTrianglesValid(false), _meshGroupsKnown(false), _isWireframe(false), - _renderCollisionHull(false) { + _renderCollisionHull(false), + _rig(rig) { // we may have been created in the network thread, but we live in the main thread if (_viewState) { @@ -1518,8 +1519,9 @@ void Model::updateVisibleJointStates() { } } -bool Model::setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, bool useRotation, - int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, float priority) { +bool Model::setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, + bool useRotation, int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, + float priority) { if (jointIndex == -1 || _jointStates.isEmpty()) { return false; } @@ -1604,7 +1606,8 @@ bool Model::setJointPosition(int jointIndex, const glm::vec3& position, const gl return true; } -void Model::inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::quat& targetRotation, float priority) { +void Model::inverseKinematics(int endIndex, glm::vec3 targetPosition, + const glm::quat& targetRotation, float priority) { // NOTE: targetRotation is from bind- to model-frame if (endIndex == -1 || _jointStates.isEmpty()) { diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 2d9bf7f146..f34690e570 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -67,7 +67,7 @@ public: static void setAbstractViewStateInterface(AbstractViewStateInterface* viewState) { _viewState = viewState; } - Model(QObject* parent = NULL); + Model(QObject* parent = NULL, RigPointer rig = nullptr); virtual ~Model(); /// enables/disables scale to fit behavior, the model will be automatically scaled to the specified largest dimension @@ -296,8 +296,8 @@ protected: /// \param alignment /// \return true if joint exists bool setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation = glm::quat(), - bool useRotation = false, int lastFreeIndex = -1, bool allIntermediatesFree = false, - const glm::vec3& alignment = glm::vec3(0.0f, -1.0f, 0.0f), float priority = 1.0f); + bool useRotation = false, int lastFreeIndex = -1, bool allIntermediatesFree = false, + const glm::vec3& alignment = glm::vec3(0.0f, -1.0f, 0.0f), float priority = 1.0f); /// Restores the indexed joint to its default position. /// \param fraction the fraction of the default position to apply (i.e., 0.25f to slerp one fourth of the way to @@ -524,6 +524,8 @@ private: QMap _renderItems; bool _readyWhenAdded = false; bool _needsReload = true; + + RigPointer _rig; }; Q_DECLARE_METATYPE(QPointer) From 9c8e01f3ff64d1401a5706b2c27b644235000753 Mon Sep 17 00:00:00 2001 From: Bing Shearer Date: Fri, 17 Jul 2015 16:10:34 -0700 Subject: [PATCH 024/242] New falling leaf script based on the existing rain.js --- examples/leaves.js | 291 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100755 examples/leaves.js diff --git a/examples/leaves.js b/examples/leaves.js new file mode 100755 index 0000000000..347371ff5b --- /dev/null +++ b/examples/leaves.js @@ -0,0 +1,291 @@ +// +// Leaves.js +// examples +// +// Created by Bing Shearer on 14 Jul 2015 +// 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 +// +var leafName = "scriptLeaf"; +var leafSquall = function (properties) { + var // Properties + squallOrigin, + squallRadius, + leavesPerMinute = 60, + leafSize = { x: 0.1, y: 0.1, z: 0.1 }, + leafFallSpeed = 1, // m/s + leafLifetime = 60, // Seconds + leafSpinMax = 0, // Maximum angular velocity per axis; deg/s + debug = false, // Display origin circle; don't use running on Stack Manager + // Other + squallCircle, + SQUALL_CIRCLE_COLOR = { red: 255, green: 0, blue: 0 }, + SQUALL_CIRCLE_ALPHA = 0.5, + SQUALL_CIRCLE_ROTATION = Quat.fromPitchYawRollDegrees(90, 0, 0), + leafProperties, + leaf_MODEL_URL = "https://hifi-public.s3.amazonaws.com/ozan/support/forBing/palmLeaf.fbx", + leafTimer, + leaves = [], // HACK: Work around leaves not always getting velocities + leafVelocities = [], // HACK: Work around leaves not always getting velocities + DEGREES_TO_RADIANS = Math.PI / 180, + leafDeleteOnTearDown = true, + maxLeaves, + leafCount, + nearbyEntities, + complexMovement = false, + movementTime = 0, + maxSpinRadians = properties.leafSpinMax * DEGREES_TO_RADIANS, + windFactor, + leafDeleteOnGround = false, + floorHeight = null; + + + function processProperties() { + if (!properties.hasOwnProperty("origin")) { + print("ERROR: Leaf squall origin must be specified"); + return; + } + squallOrigin = properties.origin; + + if (!properties.hasOwnProperty("radius")) { + print("ERROR: Leaf squall radius must be specified"); + return; + } + squallRadius = properties.radius; + + if (properties.hasOwnProperty("leavesPerMinute")) { + leavesPerMinute = properties.leavesPerMinute; + } + + if (properties.hasOwnProperty("leafSize")) { + leafSize = properties.leafSize; + } + + if (properties.hasOwnProperty("leafFallSpeed")) { + leafFallSpeed = properties.leafFallSpeed; + } + + if (properties.hasOwnProperty("leafLifetime")) { + leafLifetime = properties.leafLifetime; + } + + if (properties.hasOwnProperty("leafSpinMax")) { + leafSpinMax = properties.leafSpinMax; + } + + if (properties.hasOwnProperty("debug")) { + debug = properties.debug; + } + if (properties.hasOwnProperty("floorHeight")) { + floorHeight = properties.floorHeight; + } + if (properties.hasOwnProperty("maxLeaves")) { + maxLeaves = properties.maxLeaves; + } + if (properties.hasOwnProperty("complexMovement")) { + complexMovement = properties.complexMovement; + } + if (properties.hasOwnProperty("leafDeleteOnGround")) { + leafDeleteOnGround = properties.leafDeleteOnGround; + } + if (properties.hasOwnProperty("windFactor")) { + windFactor = properties.windFactor; + } + else { + print("ERROR: Wind Factor must be defined for complex movement") + } + + leafProperties = { + type: "Model", + name: leafName, + modelURL: leaf_MODEL_URL, + lifetime: leafLifetime, + dimensions: leafSize, + velocity: { x: 0, y: -leafFallSpeed, z: 0 }, + damping: 0, + angularDamping: 0, + ignoreForCollisions: true + + }; + } + + function createleaf() { + var angle, + radius, + offset, + leaf, + spin = { x: 0, y: 0, z: 0 }, + i; + + // HACK: Work around leaves not always getting velocities set at creation + for (i = 0; i < leaves.length; i += 1) { + Entities.editEntity(leaves[i], leafVelocities[i]); + } + + angle = Math.random() * 10; + radius = Math.random() * squallRadius; + offset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, angle, 0), { x: 0, y: -0.1, z: radius }); + leafProperties.position = Vec3.sum(squallOrigin, offset); + if (properties.leafSpinMax > 0 && !complexMovement) { + spin = { + x: Math.random() * maxSpinRadians, + y: Math.random() * maxSpinRadians, + z: Math.random() * maxSpinRadians + }; + leafProperties.angularVelocity = spin; + } + else if (complexMovement) { + spin = { x: 0, y: 0, z: 0 }; + leafProperties.angularVelocity = spin + } + leaf = Entities.addEntity(leafProperties); + + // HACK: Work around leaves not always getting velocities set at creation + leaves.push(leaf); + leafVelocities.push({ + velocity: leafProperties.velocity, + angularVelocity: spin + }); + if (leaves.length > 5) { + leaves.shift(); + leafVelocities.shift(); + } + } + + function setUp() { + if (debug) { + squallCircle = Overlays.addOverlay("circle3d", { + size: { x: 2 * squallRadius, y: 2 * squallRadius }, + color: SQUALL_CIRCLE_COLOR, + alpha: SQUALL_CIRCLE_ALPHA, + solid: true, + visible: debug, + position: squallOrigin, + rotation: SQUALL_CIRCLE_ROTATION + }); + } + + leafTimer = Script.setInterval(function () { + if (leafCount <= maxLeaves - 1) { + createleaf() + } + }, 60000 / leavesPerMinute); + } + Script.setInterval(function () { + nearbyEntities = Entities.findEntities(squallOrigin, squallRadius); + newLeafMovement() + }, 100); + + function newLeafMovement() { //new additions to leaf code. Operates at 10 Hz or every 100 ms + movementTime += 0.1; + var currentLeaf, + randomRotationSpeed = { + x: maxSpinRadians * Math.sin(movementTime), + y: maxSpinRadians * Math.random(), + z: maxSpinRadians * Math.sin(movementTime / 7) + }; + for (var i = 0; i < nearbyEntities.length; i++) { + var entityProperties = Entities.getEntityProperties(nearbyEntities[i]); + var entityName = entityProperties.name; + if (leafName === entityName) { + currentLeaf = nearbyEntities[i]; + var leafHeight = entityProperties.position.y; + if (complexMovement && leafHeight > floorHeight || complexMovement && floorHeight == null) { //actual new movement code; + var currentLeafVelocity = entityProperties.velocity, + currentLeafRot = entityProperties.rotation, + yVec = { x: 0, y: 1, z: 0 }, + leafYinWFVec = Vec3.multiplyQbyV(currentLeafRot, yVec), + leafLocalHorVec = Vec3.cross(leafYinWFVec, yVec), + leafMostDownVec = Vec3.cross(leafYinWFVec, leafLocalHorVec), + leafDesiredVel = Vec3.multiply(leafMostDownVec, windFactor); + Entities.editEntity(currentLeaf, { + angularVelocity: randomRotationSpeed, + velocity: { + x: (leafDesiredVel.x - currentLeafVelocity.x) * windFactor + currentLeafVelocity.x, + y: (leafDesiredVel.y - currentLeafVelocity.y) * windFactor + currentLeafVelocity.y, + z: (leafDesiredVel.z - currentLeafVelocity.z) * windFactor + currentLeafVelocity.z + } + }) + } + else if (leafHeight <= floorHeight) { + if (!leafDeleteOnGround) { + Entities.editEntity(nearbyEntities[i], { + locked: false, + velocity: { x: 0, y: 0, z: 0 }, + angularVelocity: { x: 0, y: 0, z: 0 } + }) + } + else { + Entity.deleteEntity(currentLeaf); + } + } + } + } + } + + + + getLeafCount = Script.setInterval(function () { + leafCount = 0 + for (var i = 0; i < nearbyEntities.length; i++) { + var entityName = Entities.getEntityProperties(nearbyEntities[i]).name; + //Stop Leaves at floorHeight + if (leafName === entityName) { + leafCount++; + if (i == nearbyEntities.length - 1) { + //print(leafCount); + } + } + } + }, 1000) + + + + function tearDown() { + Script.clearInterval(leafTimer); + Overlays.deleteOverlay(squallCircle); + if (leafDeleteOnTearDown) { + for (var i = 0; i < nearbyEntities.length; i++) { + var entityName = Entities.getEntityProperties(nearbyEntities[i]).name; + if (leafName === entityName) { + //We have a match - delete this entity + Entities.editEntity(nearbyEntities[i], { + locked: false + }); + Entities.deleteEntity(nearbyEntities[i]); + } + } + } + } + + + + processProperties(); + setUp(); + Script.scriptEnding.connect(tearDown); + + return { + }; +}; + +var leafSquall1 = new leafSquall({ + origin: { x: 3071.5, y: 2170, z: 6765.3 }, + radius: 100, + leavesPerMinute: 30, + leafSize: { x: 0.3, y: 0.00, z: .3}, + leafFallSpeed: .4, + leafLifetime: 100, + leafSpinMax: 30, + debug: false, + maxLeaves: 100, + leafDeleteOnTearDown: true, + complexMovement: true, + floorHeight: 2143.5, + windFactor: .5, + leafDeleteOnGround: false +}); + +// todo +//deal with depth issue \ No newline at end of file From 0580c8477e1c3a89879176e21702765fe2e7eae0 Mon Sep 17 00:00:00 2001 From: Niraj Venkat Date: Fri, 17 Jul 2015 16:42:03 -0700 Subject: [PATCH 025/242] 3-step groundwork for AO in the pipeline --- interface/src/Application.cpp | 1 - libraries/gpu/src/gpu/GLBackendTexture.cpp | 32 ++++ .../src/AmbientOcclusionEffect.cpp | 149 ++++++++++++++++-- .../render-utils/src/AmbientOcclusionEffect.h | 18 ++- .../render-utils/src/ambient_occlusion.slf | 3 - .../render-utils/src/ambient_occlusion.slv | 4 +- libraries/render-utils/src/gaussian_blur.slf | 42 +++++ .../src/gaussian_blur_horizontal.slv | 41 +++++ .../src/gaussian_blur_vertical.slv | 41 +++++ 9 files changed, 306 insertions(+), 25 deletions(-) create mode 100644 libraries/render-utils/src/gaussian_blur.slf create mode 100644 libraries/render-utils/src/gaussian_blur_horizontal.slv create mode 100644 libraries/render-utils/src/gaussian_blur_vertical.slv diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 8b04223463..537003ec02 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -274,7 +274,6 @@ bool setupEssentials(int& argc, char** argv) { auto audio = DependencyManager::set(); auto audioScope = DependencyManager::set(); auto deferredLightingEffect = DependencyManager::set(); - auto ambientOcclusionEffect = DependencyManager::set(); auto textureCache = DependencyManager::set(); auto animationCache = DependencyManager::set(); auto ddeFaceTracker = DependencyManager::set(); diff --git a/libraries/gpu/src/gpu/GLBackendTexture.cpp b/libraries/gpu/src/gpu/GLBackendTexture.cpp index 72c7de8504..2696d17596 100755 --- a/libraries/gpu/src/gpu/GLBackendTexture.cpp +++ b/libraries/gpu/src/gpu/GLBackendTexture.cpp @@ -144,6 +144,38 @@ public: case gpu::RGB: case gpu::RGBA: texel.internalFormat = GL_RED; + /* switch (dstFormat.getType()) { + case gpu::UINT32: + case gpu::INT32: + case gpu::NUINT32: + case gpu::NINT32: { + texel.internalFormat = GL_DEPTH_COMPONENT32; + break; + } + case gpu::NFLOAT: + case gpu::FLOAT: { + texel.internalFormat = GL_DEPTH_COMPONENT32F; + break; + } + case gpu::UINT16: + case gpu::INT16: + case gpu::NUINT16: + case gpu::NINT16: + case gpu::HALF: + case gpu::NHALF: { + texel.internalFormat = GL_DEPTH_COMPONENT16; + break; + } + case gpu::UINT8: + case gpu::INT8: + case gpu::NUINT8: + case gpu::NINT8: { + texel.internalFormat = GL_DEPTH_COMPONENT24; + break; + } + case gpu::NUM_TYPES: { // quiet compiler + Q_UNREACHABLE(); + }*/ break; case gpu::DEPTH: texel.format = GL_DEPTH_COMPONENT; // It's depth component to load it diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index 07832179cc..7e6b5c7e75 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -31,12 +31,16 @@ #include "ambient_occlusion_vert.h" #include "ambient_occlusion_frag.h" +#include "gaussian_blur_vertical_vert.h" +#include "gaussian_blur_horizontal_vert.h" +#include "gaussian_blur_frag.h" const int ROTATION_WIDTH = 4; const int ROTATION_HEIGHT = 4; - + +/* void AmbientOcclusionEffect::init(AbstractViewStateInterface* viewState) { - /*_viewState = viewState; // we will use this for view state services + _viewState = viewState; // we will use this for view state services _occlusionProgram = new ProgramObject(); _occlusionProgram->addShaderFromSourceFile(QGLShader::Vertex, PathUtils::resourcesPath() @@ -99,11 +103,11 @@ void AmbientOcclusionEffect::init(AbstractViewStateInterface* viewState) { _blurProgram->setUniformValue("originalTexture", 0); _blurProgram->release(); - _blurScaleLocation = _blurProgram->uniformLocation("blurScale");*/ + _blurScaleLocation = _blurProgram->uniformLocation("blurScale"); } void AmbientOcclusionEffect::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext){ - /*glDisable(GL_BLEND); + glDisable(GL_BLEND); glDisable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); @@ -170,15 +174,15 @@ void AmbientOcclusionEffect::run(const render::SceneContextPointer& sceneContext glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); glEnable(GL_DEPTH_TEST); - glDepthMask(GL_TRUE);*/ + glDepthMask(GL_TRUE) } - +*/ AmbientOcclusion::AmbientOcclusion() { } -const gpu::PipelinePointer& AmbientOcclusion::getAOPipeline() { - if (!_AOPipeline) { +const gpu::PipelinePointer& AmbientOcclusion::getOcclusionPipeline() { + if (!_occlusionPipeline) { auto vs = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(ambient_occlusion_vert))); auto ps = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(ambient_occlusion_frag))); gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vs, ps)); @@ -198,10 +202,85 @@ const gpu::PipelinePointer& AmbientOcclusion::getAOPipeline() { gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::DEST_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ZERO); + // Link the occlusion FBO to texture + _occlusionBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, + DependencyManager::get()->getFrameBufferSize().width(), DependencyManager::get()->getFrameBufferSize().height())); + auto format = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); + auto width = _occlusionBuffer->getWidth(); + auto height = _occlusionBuffer->getHeight(); + auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); + _occlusionTexture = gpu::TexturePointer(gpu::Texture::create2D(format, width, height, defaultSampler)); + // Good to go add the brand new pipeline - _AOPipeline.reset(gpu::Pipeline::create(program, state)); + _occlusionPipeline.reset(gpu::Pipeline::create(program, state)); } - return _AOPipeline; + return _occlusionPipeline; +} + +const gpu::PipelinePointer& AmbientOcclusion::getVBlurPipeline() { + if (!_vBlurPipeline) { + auto vs = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(gaussian_blur_vertical_vert))); + auto ps = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(gaussian_blur_frag))); + gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vs, ps)); + + gpu::Shader::BindingSet slotBindings; + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + state->setDepthTest(false, false, gpu::LESS_EQUAL); + + // Blend on transparent + state->setBlendFunction(false, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::DEST_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ZERO); + + // Link the horizontal blur FBO to texture + _vBlurBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, + DependencyManager::get()->getFrameBufferSize().width(), DependencyManager::get()->getFrameBufferSize().height())); + auto format = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); + auto width = _vBlurBuffer->getWidth(); + auto height = _vBlurBuffer->getHeight(); + auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); + _vBlurTexture = gpu::TexturePointer(gpu::Texture::create2D(format, width, height, defaultSampler)); + + // Good to go add the brand new pipeline + _vBlurPipeline.reset(gpu::Pipeline::create(program, state)); + } + return _vBlurPipeline; +} + +const gpu::PipelinePointer& AmbientOcclusion::getHBlurPipeline() { + if (!_hBlurPipeline) { + auto vs = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(gaussian_blur_horizontal_vert))); + auto ps = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(gaussian_blur_frag))); + gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vs, ps)); + + gpu::Shader::BindingSet slotBindings; + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + state->setDepthTest(false, false, gpu::LESS_EQUAL); + + // Blend on transparent + state->setBlendFunction(false, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::DEST_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ZERO); + + // Link the horizontal blur FBO to texture + _hBlurBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, + DependencyManager::get()->getFrameBufferSize().width(), DependencyManager::get()->getFrameBufferSize().height())); + auto format = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); + auto width = _hBlurBuffer->getWidth(); + auto height = _hBlurBuffer->getHeight(); + auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); + _hBlurTexture = gpu::TexturePointer(gpu::Texture::create2D(format, width, height, defaultSampler)); + + // Good to go add the brand new pipeline + _hBlurPipeline.reset(gpu::Pipeline::create(program, state)); + } + return _hBlurPipeline; } void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext) { @@ -226,18 +305,57 @@ void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, cons batch.setProjectionTransform(projMat); batch.setViewTransform(viewMat); batch.setModelTransform(Transform()); - batch.setResourceTexture(0, DependencyManager::get()->getPrimaryDepthTexture()); - // bind the one gpu::Pipeline we need - batch.setPipeline(getAOPipeline()); + // Occlusion step + getOcclusionPipeline(); + batch.setResourceTexture(0, DependencyManager::get()->getPrimaryDepthTexture()); + _occlusionBuffer->setRenderBuffer(0, _occlusionTexture); + batch.setFramebuffer(_occlusionBuffer); + + // bind the first gpu::Pipeline we need - for calculating occlusion buffer + batch.setPipeline(getOcclusionPipeline()); glm::vec4 color(0.0f, 0.0f, 0.0f, 1.0f); - glm::vec2 bottomLeft(0.5f, -1.0f); - glm::vec2 topRight(1.0f, -0.5f); + glm::vec2 bottomLeft(-1.0f, -1.0f); + glm::vec2 topRight(1.0f, 1.0f); glm::vec2 texCoordTopLeft(0.0f, 0.0f); glm::vec2 texCoordBottomRight(1.0f, 1.0f); DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); + // Vertical blur step + getVBlurPipeline(); + batch.setResourceTexture(0, _occlusionTexture); + _vBlurBuffer->setRenderBuffer(0, _vBlurTexture); + batch.setFramebuffer(_vBlurBuffer); + + // bind the second gpu::Pipeline we need - for calculating blur buffer + batch.setPipeline(getVBlurPipeline()); + + DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); + + // Horizontal blur step + getHBlurPipeline(); + batch.setResourceTexture(0, _vBlurTexture); + _hBlurBuffer->setRenderBuffer(0, _hBlurTexture); + batch.setFramebuffer(_hBlurBuffer); + + // bind the second gpu::Pipeline we need - for calculating blur buffer + batch.setPipeline(getHBlurPipeline()); + + DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); + + // "Blend" step + batch.setResourceTexture(0, _hBlurTexture); + batch.setFramebuffer(DependencyManager::get()->getPrimaryFramebuffer()); + + // bind the second gpu::Pipeline we need + batch.setPipeline(getOcclusionPipeline()); + + glm::vec2 bottomLeftSmall(0.5f, -1.0f); + glm::vec2 topRightSmall(1.0f, -0.5f); + DependencyManager::get()->renderQuad(batch, bottomLeftSmall, topRightSmall, texCoordTopLeft, texCoordBottomRight, color); + + // Ready to render args->_context->syncCache(); renderContext->args->_context->syncCache(); args->_context->render((batch)); @@ -247,4 +365,3 @@ void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, cons } - diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.h b/libraries/render-utils/src/AmbientOcclusionEffect.h index fbe4a09214..8fce5e3d0c 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.h +++ b/libraries/render-utils/src/AmbientOcclusionEffect.h @@ -22,6 +22,7 @@ class ProgramObject; /// A screen space ambient occlusion effect. See John Chapman's tutorial at /// http://john-chapman-graphics.blogspot.co.uk/2013/01/ssao-tutorial.html for reference. +/* class AmbientOcclusionEffect : public Dependency { SINGLETON_DEPENDENCY @@ -53,6 +54,7 @@ private: GLuint _rotationTextureID; AbstractViewStateInterface* _viewState; }; +*/ class AmbientOcclusion { public: @@ -62,11 +64,23 @@ public: void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); typedef render::Job::Model JobModel; - const gpu::PipelinePointer& AmbientOcclusion::getAOPipeline(); + const gpu::PipelinePointer& AmbientOcclusion::getOcclusionPipeline(); + const gpu::PipelinePointer& AmbientOcclusion::getHBlurPipeline(); + const gpu::PipelinePointer& AmbientOcclusion::getVBlurPipeline(); private: - gpu::PipelinePointer _AOPipeline; + gpu::PipelinePointer _occlusionPipeline; + gpu::PipelinePointer _hBlurPipeline; + gpu::PipelinePointer _vBlurPipeline; + + gpu::FramebufferPointer _occlusionBuffer; + gpu::FramebufferPointer _hBlurBuffer; + gpu::FramebufferPointer _vBlurBuffer; + + gpu::TexturePointer _occlusionTexture; + gpu::TexturePointer _hBlurTexture; + gpu::TexturePointer _vBlurTexture; }; #endif // hifi_AmbientOcclusionEffect_h diff --git a/libraries/render-utils/src/ambient_occlusion.slf b/libraries/render-utils/src/ambient_occlusion.slf index b56e0eb9d5..bdeb59e82a 100644 --- a/libraries/render-utils/src/ambient_occlusion.slf +++ b/libraries/render-utils/src/ambient_occlusion.slf @@ -14,9 +14,6 @@ <@include DeferredBufferWrite.slh@> -// the interpolated normal -//varying vec4 interpolatedNormal; - varying vec2 varTexcoord; uniform sampler2D depthTexture; diff --git a/libraries/render-utils/src/ambient_occlusion.slv b/libraries/render-utils/src/ambient_occlusion.slv index 54cc608129..81f196dd46 100644 --- a/libraries/render-utils/src/ambient_occlusion.slv +++ b/libraries/render-utils/src/ambient_occlusion.slv @@ -16,11 +16,9 @@ <$declareStandardTransform()$> -// the interpolated normal -//varying vec4 interpolatedNormal; varying vec2 varTexcoord; void main(void) { varTexcoord = gl_MultiTexCoord0.xy; gl_Position = gl_Vertex; -} \ No newline at end of file +} diff --git a/libraries/render-utils/src/gaussian_blur.slf b/libraries/render-utils/src/gaussian_blur.slf new file mode 100644 index 0000000000..5b66a0b751 --- /dev/null +++ b/libraries/render-utils/src/gaussian_blur.slf @@ -0,0 +1,42 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// gaussian_blur.frag +// fragment shader +// +// Created by Niraj Venkat on 7/17/15. +// 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 DeferredBufferWrite.slh@> + +// the interpolated normal +//varying vec4 interpolatedNormal; + +varying vec2 varTexcoord; +varying vec2 varBlurTexcoords[14]; + +uniform sampler2D occlusionTexture; + +void main(void) { + gl_FragColor = vec4(0.0); + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 0])*0.0044299121055113265; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 1])*0.00895781211794; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 2])*0.0215963866053; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 3])*0.0443683338718; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 4])*0.0776744219933; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 5])*0.115876621105; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 6])*0.147308056121; + gl_FragColor += texture2D(occlusionTexture, varTexcoord )*0.159576912161; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 7])*0.147308056121; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 8])*0.115876621105; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 9])*0.0776744219933; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[10])*0.0443683338718; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[11])*0.0215963866053; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[12])*0.00895781211794; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[13])*0.0044299121055113265; +} diff --git a/libraries/render-utils/src/gaussian_blur_horizontal.slv b/libraries/render-utils/src/gaussian_blur_horizontal.slv new file mode 100644 index 0000000000..94631b4b08 --- /dev/null +++ b/libraries/render-utils/src/gaussian_blur_horizontal.slv @@ -0,0 +1,41 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// guassian_blur_horizontal.vert +// vertex shader +// +// Created by Niraj Venkat on 7/17/15. +// 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 gpu/Transform.slh@> + +<$declareStandardTransform()$> + +varying vec2 varTexcoord; +varying vec2 varBlurTexcoords[14]; + +void main(void) { + varTexcoord = gl_MultiTexCoord0.xy; + gl_Position = gl_Vertex; + + varBlurTexcoords[ 0] = varTexcoord + vec2(-0.028, 0.0); + varBlurTexcoords[ 1] = varTexcoord + vec2(-0.024, 0.0); + varBlurTexcoords[ 2] = varTexcoord + vec2(-0.020, 0.0); + varBlurTexcoords[ 3] = varTexcoord + vec2(-0.016, 0.0); + varBlurTexcoords[ 4] = varTexcoord + vec2(-0.012, 0.0); + varBlurTexcoords[ 5] = varTexcoord + vec2(-0.008, 0.0); + varBlurTexcoords[ 6] = varTexcoord + vec2(-0.004, 0.0); + varBlurTexcoords[ 7] = varTexcoord + vec2( 0.004, 0.0); + varBlurTexcoords[ 8] = varTexcoord + vec2( 0.008, 0.0); + varBlurTexcoords[ 9] = varTexcoord + vec2( 0.012, 0.0); + varBlurTexcoords[10] = varTexcoord + vec2( 0.016, 0.0); + varBlurTexcoords[11] = varTexcoord + vec2( 0.020, 0.0); + varBlurTexcoords[12] = varTexcoord + vec2( 0.024, 0.0); + varBlurTexcoords[13] = varTexcoord + vec2( 0.028, 0.0); +} + \ No newline at end of file diff --git a/libraries/render-utils/src/gaussian_blur_vertical.slv b/libraries/render-utils/src/gaussian_blur_vertical.slv new file mode 100644 index 0000000000..0d6de35b85 --- /dev/null +++ b/libraries/render-utils/src/gaussian_blur_vertical.slv @@ -0,0 +1,41 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// guassian_blur_vertical.vert +// vertex shader +// +// Created by Niraj Venkat on 7/17/15. +// 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 gpu/Transform.slh@> + +<$declareStandardTransform()$> + +varying vec2 varTexcoord; +varying vec2 varBlurTexcoords[14]; + +void main(void) { + varTexcoord = gl_MultiTexCoord0.xy; + gl_Position = gl_Vertex; + + varBlurTexcoords[ 0] = varTexcoord + vec2(0.0, -0.028); + varBlurTexcoords[ 1] = varTexcoord + vec2(0.0, -0.024); + varBlurTexcoords[ 2] = varTexcoord + vec2(0.0, -0.020); + varBlurTexcoords[ 3] = varTexcoord + vec2(0.0, -0.016); + varBlurTexcoords[ 4] = varTexcoord + vec2(0.0, -0.012); + varBlurTexcoords[ 5] = varTexcoord + vec2(0.0, -0.008); + varBlurTexcoords[ 6] = varTexcoord + vec2(0.0, -0.004); + varBlurTexcoords[ 7] = varTexcoord + vec2(0.0, 0.004); + varBlurTexcoords[ 8] = varTexcoord + vec2(0.0, 0.008); + varBlurTexcoords[ 9] = varTexcoord + vec2(0.0, 0.012); + varBlurTexcoords[10] = varTexcoord + vec2(0.0, 0.016); + varBlurTexcoords[11] = varTexcoord + vec2(0.0, 0.020); + varBlurTexcoords[12] = varTexcoord + vec2(0.0, 0.024); + varBlurTexcoords[13] = varTexcoord + vec2(0.0, 0.028); +} + \ No newline at end of file From ed815a0573ff67af090bafe55e3736860e532a8c Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 17 Jul 2015 16:53:41 -0700 Subject: [PATCH 026/242] Reflect dependencies in unit test setup. --- libraries/animation/src/Rig.h | 2 +- tests/rig/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index eccb0960dd..c3e1edd415 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -16,7 +16,7 @@ #include -#include "JointState.h" +#include "JointState.h" // We might want to change this (later) to something that doesn't depend on gpu, fbx and model. -HRS class AnimationHandle; typedef std::shared_ptr AnimationHandlePointer; diff --git a/tests/rig/CMakeLists.txt b/tests/rig/CMakeLists.txt index abf5da12c2..2e9dbc9424 100644 --- a/tests/rig/CMakeLists.txt +++ b/tests/rig/CMakeLists.txt @@ -1,7 +1,7 @@ # Declare dependencies macro (setup_testcase_dependencies) # link in the shared libraries - link_hifi_libraries(shared animation) + link_hifi_libraries(shared animation gpu fbx model) copy_dlls_beside_windows_executable() endmacro () From 1d48eeab10085969028092d2d0ffcaf1334a673f Mon Sep 17 00:00:00 2001 From: "Kevin M. Thomas" Date: Sat, 18 Jul 2015 18:32:39 -0400 Subject: [PATCH 027/242] commit 20485 --- examples/afk.js | 120 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 examples/afk.js diff --git a/examples/afk.js b/examples/afk.js new file mode 100644 index 0000000000..5e0c443e11 --- /dev/null +++ b/examples/afk.js @@ -0,0 +1,120 @@ +// +// #20485: AFK - Away From Keyboard Setting +// ***************************************** +// +// Created by Kevin M. Thomas and Thoys 07/16/15. +// Copyright 2015 High Fidelity, Inc. +// kevintown.net +// +// JavaScript for the High Fidelity interface that creates an away from keyboard functionality by providing a UI and keyPressEvent which will mute toggle the connected microphone, face tracking dde and set the avatar to a hand raise pose. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +var originalOutputDevice; +var originalName; +var muted = false; +var wasAudioEnabled; +var afkText = "AFK - I Will Return!\n"; + +// Set up toggleMuteButton text overlay. +var toggleMuteButton = Overlays.addOverlay("text", { + x: 10, + y: 275, + width: 60, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8 +}); + +// Function that overlays text upon state change. +function onMuteStateChanged() { + Overlays.editOverlay(toggleMuteButton, muted ? {text: "Go Live", leftMargin: 5} : {text: "Go AFK", leftMargin: 5}); +} + +// Function that adds mousePressEvent functionality to toggle mic mute, AFK message above display name and toggle avatar arms upward. +function mousePressEvent(event) { + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleMuteButton) { + if (!muted) { + if (!AudioDevice.getMuted()) { + AudioDevice.toggleMute(); + } + originalOutputDevice = AudioDevice.getOutputDevice(); + Menu.setIsOptionChecked("Mute Face Tracking", true); + originalName = MyAvatar.displayName; + AudioDevice.setOutputDevice("none"); + MyAvatar.displayName = afkText + MyAvatar.displayName; + MyAvatar.setJointData("LeftShoulder", Quat.fromPitchYawRollDegrees(0, 180, 0)); + MyAvatar.setJointData("RightShoulder", Quat.fromPitchYawRollDegrees(0, 180, 0)); + } else { + if (AudioDevice.getMuted()) { + AudioDevice.toggleMute(); + } + AudioDevice.setOutputDevice(originalOutputDevice); + Menu.setIsOptionChecked("Mute Face Tracking", false); + MyAvatar.setJointData("LeftShoulder", Quat.fromPitchYawRollDegrees(0, 0, 0)); + MyAvatar.setJointData("RightShoulder", Quat.fromPitchYawRollDegrees(0, 0, 0)); + MyAvatar.clearJointData("LeftShoulder"); + MyAvatar.clearJointData("RightShoulder"); + MyAvatar.displayName = originalName; + } + onMuteStateChanged(); + muted = !muted; + } +} + +// Call functions. +onMuteStateChanged(); + +//AudioDevice.muteToggled.connect(onMuteStateChanged); +Controller.mousePressEvent.connect(mousePressEvent); + +// Function that adds keyPressEvent functionality to toggle mic mute, AFK message above display name and toggle avatar arms upward. +Controller.keyPressEvent.connect(function(event) { + if (event.text == "y") { + if (!muted) { + if (!AudioDevice.getMuted()) { + AudioDevice.toggleMute(); + } + originalOutputDevice = AudioDevice.getOutputDevice(); + Menu.setIsOptionChecked("Mute Face Tracking", true); + originalName = MyAvatar.displayName; + AudioDevice.setOutputDevice("none"); + MyAvatar.displayName = afkText + MyAvatar.displayName; + MyAvatar.setJointData("LeftShoulder", Quat.fromPitchYawRollDegrees(0, 180, 0)); + MyAvatar.setJointData("RightShoulder", Quat.fromPitchYawRollDegrees(0, 180, 0)); + } else { + if (AudioDevice.getMuted()) { + AudioDevice.toggleMute(); + } + AudioDevice.setOutputDevice(originalOutputDevice); + Menu.setIsOptionChecked("Mute Face Tracking", false); + MyAvatar.setJointData("LeftShoulder", Quat.fromPitchYawRollDegrees(0, 0, 0)); + MyAvatar.setJointData("RightShoulder", Quat.fromPitchYawRollDegrees(0, 0, 0)); + MyAvatar.clearJointData("LeftShoulder"); + MyAvatar.clearJointData("RightShoulder"); + MyAvatar.displayName = originalName; + } + onMuteStateChanged(); + muted = !muted; + } +}); + +// Function that sets a timeout value of 1 second so that the display name does not get overwritten in the event of a crash. +Script.setTimeout(function() { + MyAvatar.displayName = MyAvatar.displayName.replace(afkText, ""); +}, 1000); + +// Function that calls upon exit to restore avatar display name to original state. +Script.scriptEnding.connect(function(){ + if (muted) { + AudioDevice.setOutputDevice(originalOutputDevice); + Overlays.deleteOverlay(toggleMuteButton); + MyAvatar.displayName = originalName; + } + Overlays.deleteOverlay(toggleMuteButton); +}); \ No newline at end of file From eb48aa10185e8b5323ea2b3e2365fd1b6259e43d Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Sun, 19 Jul 2015 12:10:37 -0700 Subject: [PATCH 028/242] Update edit.js to swing up on activate --- examples/libraries/entityCameraTool.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/libraries/entityCameraTool.js b/examples/libraries/entityCameraTool.js index d27d95c161..a3b817e300 100644 --- a/examples/libraries/entityCameraTool.js +++ b/examples/libraries/entityCameraTool.js @@ -141,7 +141,7 @@ CameraManager = function() { // Pick a point INITIAL_ZOOM_DISTANCE in front of the camera to use as a focal point that.zoomDistance = INITIAL_ZOOM_DISTANCE; - that.targetZoomDistance = that.zoomDistance; + that.targetZoomDistance = that.zoomDistance + 3.0; var focalPoint = Vec3.sum(Camera.getPosition(), Vec3.multiply(that.zoomDistance, Quat.getFront(Camera.getOrientation()))); @@ -150,6 +150,7 @@ CameraManager = function() { var xzDist = Math.sqrt(dPos.x * dPos.x + dPos.z * dPos.z); that.targetPitch = -Math.atan2(dPos.y, xzDist) * 180 / Math.PI; + that.targetPitch += (90 - that.targetPitch) / 3.0; // Swing camera "up" to look down at the focal point that.targetYaw = Math.atan2(dPos.x, dPos.z) * 180 / Math.PI; that.pitch = that.targetPitch; that.yaw = that.targetYaw; From 48524b1c98999326b4b25ee782e16e107a851124 Mon Sep 17 00:00:00 2001 From: "Kevin M. Thomas" Date: Sun, 19 Jul 2015 18:22:58 -0400 Subject: [PATCH 029/242] Reverted changes from other PR. --- libraries/audio-client/src/AudioClient.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 3a85adbc97..aeea7c07c1 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -55,9 +55,9 @@ static const int NUM_AUDIO_CHANNELS = 2; -static const int DEFAULT_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 20; +static const int DEFAULT_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 3; static const int MIN_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 1; -static const int MAX_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 40; +static const int MAX_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 20; #if defined(Q_OS_ANDROID) || defined(Q_OS_WIN) static const int DEFAULT_AUDIO_OUTPUT_STARVE_DETECTION_ENABLED = false; #else From 8ec51f981464eac0dbde60eae033c3ae1092629d Mon Sep 17 00:00:00 2001 From: "Kevin M. Thomas" Date: Sun, 19 Jul 2015 18:24:29 -0400 Subject: [PATCH 030/242] Updated afk.js file based on advise from PR. --- examples/afk.js | 80 +++++++++++++++++++------------------------------ 1 file changed, 30 insertions(+), 50 deletions(-) diff --git a/examples/afk.js b/examples/afk.js index 5e0c443e11..5977c6384a 100644 --- a/examples/afk.js +++ b/examples/afk.js @@ -36,34 +36,38 @@ function onMuteStateChanged() { Overlays.editOverlay(toggleMuteButton, muted ? {text: "Go Live", leftMargin: 5} : {text: "Go AFK", leftMargin: 5}); } +function toggleMute() { + if (!muted) { + if (!AudioDevice.getMuted()) { + AudioDevice.toggleMute(); + } + originalOutputDevice = AudioDevice.getOutputDevice(); + Menu.setIsOptionChecked("Mute Face Tracking", true); + originalName = MyAvatar.displayName; + AudioDevice.setOutputDevice("none"); + MyAvatar.displayName = afkText + MyAvatar.displayName; + MyAvatar.setJointData("LeftShoulder", Quat.fromPitchYawRollDegrees(0, 180, 0)); + MyAvatar.setJointData("RightShoulder", Quat.fromPitchYawRollDegrees(0, 180, 0)); + } else { + if (AudioDevice.getMuted()) { + AudioDevice.toggleMute(); + } + AudioDevice.setOutputDevice(originalOutputDevice); + Menu.setIsOptionChecked("Mute Face Tracking", false); + MyAvatar.setJointData("LeftShoulder", Quat.fromPitchYawRollDegrees(0, 0, 0)); + MyAvatar.setJointData("RightShoulder", Quat.fromPitchYawRollDegrees(0, 0, 0)); + MyAvatar.clearJointData("LeftShoulder"); + MyAvatar.clearJointData("RightShoulder"); + MyAvatar.displayName = originalName; + } + muted = !muted; + onMuteStateChanged(); +} + // Function that adds mousePressEvent functionality to toggle mic mute, AFK message above display name and toggle avatar arms upward. function mousePressEvent(event) { if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleMuteButton) { - if (!muted) { - if (!AudioDevice.getMuted()) { - AudioDevice.toggleMute(); - } - originalOutputDevice = AudioDevice.getOutputDevice(); - Menu.setIsOptionChecked("Mute Face Tracking", true); - originalName = MyAvatar.displayName; - AudioDevice.setOutputDevice("none"); - MyAvatar.displayName = afkText + MyAvatar.displayName; - MyAvatar.setJointData("LeftShoulder", Quat.fromPitchYawRollDegrees(0, 180, 0)); - MyAvatar.setJointData("RightShoulder", Quat.fromPitchYawRollDegrees(0, 180, 0)); - } else { - if (AudioDevice.getMuted()) { - AudioDevice.toggleMute(); - } - AudioDevice.setOutputDevice(originalOutputDevice); - Menu.setIsOptionChecked("Mute Face Tracking", false); - MyAvatar.setJointData("LeftShoulder", Quat.fromPitchYawRollDegrees(0, 0, 0)); - MyAvatar.setJointData("RightShoulder", Quat.fromPitchYawRollDegrees(0, 0, 0)); - MyAvatar.clearJointData("LeftShoulder"); - MyAvatar.clearJointData("RightShoulder"); - MyAvatar.displayName = originalName; - } - onMuteStateChanged(); - muted = !muted; + toggleMute(); } } @@ -76,31 +80,7 @@ Controller.mousePressEvent.connect(mousePressEvent); // Function that adds keyPressEvent functionality to toggle mic mute, AFK message above display name and toggle avatar arms upward. Controller.keyPressEvent.connect(function(event) { if (event.text == "y") { - if (!muted) { - if (!AudioDevice.getMuted()) { - AudioDevice.toggleMute(); - } - originalOutputDevice = AudioDevice.getOutputDevice(); - Menu.setIsOptionChecked("Mute Face Tracking", true); - originalName = MyAvatar.displayName; - AudioDevice.setOutputDevice("none"); - MyAvatar.displayName = afkText + MyAvatar.displayName; - MyAvatar.setJointData("LeftShoulder", Quat.fromPitchYawRollDegrees(0, 180, 0)); - MyAvatar.setJointData("RightShoulder", Quat.fromPitchYawRollDegrees(0, 180, 0)); - } else { - if (AudioDevice.getMuted()) { - AudioDevice.toggleMute(); - } - AudioDevice.setOutputDevice(originalOutputDevice); - Menu.setIsOptionChecked("Mute Face Tracking", false); - MyAvatar.setJointData("LeftShoulder", Quat.fromPitchYawRollDegrees(0, 0, 0)); - MyAvatar.setJointData("RightShoulder", Quat.fromPitchYawRollDegrees(0, 0, 0)); - MyAvatar.clearJointData("LeftShoulder"); - MyAvatar.clearJointData("RightShoulder"); - MyAvatar.displayName = originalName; - } - onMuteStateChanged(); - muted = !muted; + toggleMute(); } }); From 3439d45e0ee6b53027dcea062adfec2bf1566d93 Mon Sep 17 00:00:00 2001 From: ericrius1 Date: Mon, 20 Jul 2015 10:44:38 -0700 Subject: [PATCH 031/242] adding hit effect logic --- libraries/render-utils/src/HitEffect.cpp | 105 ++++++++++++++++++ libraries/render-utils/src/HitEffect.h | 34 ++++++ .../render-utils/src/RenderDeferredTask.cpp | 4 + libraries/render-utils/src/hit_effect.slf | 29 +++++ libraries/render-utils/src/hit_effect.slv | 21 ++++ 5 files changed, 193 insertions(+) create mode 100644 libraries/render-utils/src/HitEffect.cpp create mode 100644 libraries/render-utils/src/HitEffect.h create mode 100644 libraries/render-utils/src/hit_effect.slf create mode 100644 libraries/render-utils/src/hit_effect.slv diff --git a/libraries/render-utils/src/HitEffect.cpp b/libraries/render-utils/src/HitEffect.cpp new file mode 100644 index 0000000000..1b81c3b85f --- /dev/null +++ b/libraries/render-utils/src/HitEffect.cpp @@ -0,0 +1,105 @@ +// +// HitEffect.cpp +// interface/src/renderer +// +// Created by Andrzej Kapolka on 7/14/13. +// Copyright 2013 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 this before QOpenGLFramebufferObject, which includes an earlier version of OpenGL +#include + +#include + +#include + +#include +#include + +#include "AbstractViewStateInterface.h" +#include "HitEffect.h" +#include "ProgramObject.h" +#include "RenderUtil.h" +#include "TextureCache.h" +#include "DependencyManager.h" +#include "ViewFrustum.h" +#include "GeometryCache.h" + +#include "hit_effect_vert.h" +#include "hit_effect_frag.h" + + +HitEffect::HitEffect() { +} + +const gpu::PipelinePointer& HitEffect::getHitEffectPipeline() { + if (!_hitEffectPipeline) { + auto vs = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(hit_effect_vert))); + auto ps = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(hit_effect_frag))); + gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vs, ps)); + + + gpu::Shader::BindingSet slotBindings; + gpu::Shader::makeProgram(*program, slotBindings); + + _screenSizeLocation = program->getUniforms().findLocation("screenSize"); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + state->setDepthTest(false, false, gpu::LESS_EQUAL); + + // Blend on transparent + state->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); + + + // Good to go add the brand new pipeline + _hitEffectPipeline.reset(gpu::Pipeline::create(program, state)); + } + return _hitEffectPipeline; +} + + + + +void HitEffect::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext) { + + // create a simple pipeline that does: + + assert(renderContext->args); + assert(renderContext->args->_viewFrustum); + RenderArgs* args = renderContext->args; + + // Allright, something to render let's do it + gpu::Batch batch; + + glm::mat4 projMat; + Transform viewMat; + args->_viewFrustum->evalProjectionMatrix(projMat); + args->_viewFrustum->evalViewTransform(viewMat); + batch.setProjectionTransform(projMat); + batch.setViewTransform(viewMat); + batch.setModelTransform(Transform()); + + + + + // bind the first gpu::Pipeline we need - for calculating occlusion buffer + batch.setPipeline(getHitEffectPipeline()); + + + glm::vec4 color(0.0f, 0.0f, 0.0f, 1.0f); + glm::vec2 bottomLeft(-1.0f, -1.0f); + glm::vec2 topRight(1.0f, 1.0f); + DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, color); + + + // Ready to render + renderContext->args->_context->syncCache(); + args->_context->render((batch)); + +} + diff --git a/libraries/render-utils/src/HitEffect.h b/libraries/render-utils/src/HitEffect.h new file mode 100644 index 0000000000..3ee05a27aa --- /dev/null +++ b/libraries/render-utils/src/HitEffect.h @@ -0,0 +1,34 @@ +// +// hitEffect.h +// hifi +// +// Created by eric levin on 7/17/15. +// +// + +#ifndef hifi_hitEffect_h +#define hifi_hitEffect_h + +#include +#include "render/DrawTask.h" + +class AbstractViewStateInterface; +class ProgramObject; + +class HitEffect { +public: + + HitEffect(); + + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); + typedef render::Job::Model JobModel; + + const gpu::PipelinePointer& getHitEffectPipeline(); + +private: + + gpu::PipelinePointer _hitEffectPipeline; + GLuint _screenSizeLocation; +}; + +#endif diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index fab134913d..95e0344313 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -16,6 +16,7 @@ #include "ViewFrustum.h" #include "RenderArgs.h" #include "TextureCache.h" +#include "HitEffect.h" #include "render/DrawStatus.h" @@ -54,6 +55,7 @@ RenderDeferredTask::RenderDeferredTask() : Task() { _jobs.push_back(Job(new DepthSortItems::JobModel("DepthSortOpaque", _jobs.back().getOutput()))); auto& renderedOpaques = _jobs.back().getOutput(); _jobs.push_back(Job(new DrawOpaqueDeferred::JobModel("DrawOpaqueDeferred", _jobs.back().getOutput()))); + _jobs.push_back(Job(new DrawLight::JobModel("DrawLight"))); _jobs.push_back(Job(new ResetGLState::JobModel())); _jobs.push_back(Job(new RenderDeferred::JobModel("RenderDeferred"))); @@ -75,7 +77,9 @@ RenderDeferredTask::RenderDeferredTask() : Task() { _drawStatusJobIndex = _jobs.size() - 1; _jobs.push_back(Job(new DrawOverlay3D::JobModel("DrawOverlay3D"))); + _jobs.push_back(Job(new HitEffect::JobModel("HitEffect"))); _jobs.push_back(Job(new ResetGLState::JobModel())); + // Give ourselves 3 frmaes of timer queries _timerQueries.push_back(gpu::QueryPointer(new gpu::Query())); diff --git a/libraries/render-utils/src/hit_effect.slf b/libraries/render-utils/src/hit_effect.slf new file mode 100644 index 0000000000..cd94de3620 --- /dev/null +++ b/libraries/render-utils/src/hit_effect.slf @@ -0,0 +1,29 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// ambient_occlusion.frag +// fragment shader +// +// Created by Eric Levin on 7/20 +// 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 gpu/Transform.slh@> +<$declareStandardTransform()$> +<@include DeferredBufferWrite.slh@> + +void main(void) { + + TransformCamera cam = getTransformCamera(); + vec4 myViewport; + <$transformCameraViewport(cam, myViewport)$> + vec2 center = vec2(myViewport.z/2.0, myViewport.w/2.0); + float distFromCenter = distance(center, gl_FragCoord.xy); + //normalize + distFromCenter = distFromCenter/myViewport.z; + float alpha = mix(0.0, 1.0, distFromCenter); + gl_FragColor = vec4(0.7, 0.0, 0.0, alpha); +} \ No newline at end of file diff --git a/libraries/render-utils/src/hit_effect.slv b/libraries/render-utils/src/hit_effect.slv new file mode 100644 index 0000000000..c12e3e71f8 --- /dev/null +++ b/libraries/render-utils/src/hit_effect.slv @@ -0,0 +1,21 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// ambient_occlusion.vert +// vertex shader +// +// Created by Niraj Venkat on 7/20/15. +// 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 gpu/Transform.slh@> + +<$declareStandardTransform()$> + +void main(void) { + gl_Position = gl_Vertex; +} \ No newline at end of file From 9623ccf7929fc716726ba5bd7b8a2a0f4b615768 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 20 Jul 2015 11:07:25 -0700 Subject: [PATCH 032/242] Change instantiation of Rig. --- interface/src/avatar/MyAvatar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 896128aee8..5433dda1c6 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -102,7 +102,7 @@ MyAvatar::MyAvatar() : _eyeContactTarget(LEFT_EYE), _realWorldFieldOfView("realWorldFieldOfView", DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), - _rig(), + _rig(new Rig()), _firstPersonSkeletonModel(this, nullptr, _rig), _prevShouldDrawHead(true) { From d76d380512c1f2cc17f125b0cffe35654fbb106d Mon Sep 17 00:00:00 2001 From: "Kevin M. Thomas" Date: Mon, 20 Jul 2015 14:19:16 -0400 Subject: [PATCH 033/242] Added jsstreamplayer. --- examples/html/jsstreamplayer.html | 42 +++++++++ examples/jsstreamplayer.js | 137 ++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 examples/html/jsstreamplayer.html create mode 100644 examples/jsstreamplayer.js diff --git a/examples/html/jsstreamplayer.html b/examples/html/jsstreamplayer.html new file mode 100644 index 0000000000..7cf827309c --- /dev/null +++ b/examples/html/jsstreamplayer.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/jsstreamplayer.js b/examples/jsstreamplayer.js new file mode 100644 index 0000000000..05ebc7d968 --- /dev/null +++ b/examples/jsstreamplayer.js @@ -0,0 +1,137 @@ +// +// #20622: JS Stream Player +// ************************* +// +// Created by Kevin M. Thomas and Thoys 07/17/15. +// Copyright 2015 High Fidelity, Inc. +// kevintown.net +// +// JavaScript for the High Fidelity interface that creates a stream player with a UI and keyPressEvents for adding a stream URL in addition to play, stop and volume functionality. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +// Declare variables and set up new WebWindow. +var stream; +var volume = 1; +var streamWindow = new WebWindow('Stream', "https://dl.dropboxusercontent.com/u/17344741/jsstreamplayer/jsstreamplayer.html", 0, 0, false); + +// Set up toggleStreamURLButton overlay. +var toggleStreamURLButton = Overlays.addOverlay("text", { + x: 76, + y: 275, + width: 40, + height: 28, + backgroundColor: {red: 0, green: 0, blue: 0}, + color: {red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + text: " URL" +}); + +// Set up toggleStreamPlayButton overlay. +var toggleStreamPlayButton = Overlays.addOverlay("text", { + x: 122, + y: 275, + width: 38, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + text: " Play" +}); + +// Set up toggleStreamStopButton overlay. +var toggleStreamStopButton = Overlays.addOverlay("text", { + x: 166, + y: 275, + width: 40, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + text: " Stop" +}); + +// Set up increaseVolumeButton overlay. +var toggleIncreaseVolumeButton = Overlays.addOverlay("text", { + x: 211, + y: 275, + width: 18, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + text: " +" +}); + +// Set up decreaseVolumeButton overlay. +var toggleDecreaseVolumeButton = Overlays.addOverlay("text", { + x: 234, + y: 275, + width: 15, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + text: " -" +}); + +// Function that adds mousePressEvent functionality to connect UI to enter stream URL, play and stop stream. +function mousePressEvent(event) { + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleStreamURLButton) { + stream = Window.prompt("Enter Stream: "); + var streamJSON = { + action: "changeStream", + stream: stream + } + streamWindow.eventBridge.emitScriptEvent(JSON.stringify(streamJSON)); + } + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleStreamPlayButton) { + var streamJSON = { + action: "changeStream", + stream: stream + } + streamWindow.eventBridge.emitScriptEvent(JSON.stringify(streamJSON)); + } + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleStreamStopButton) { + var streamJSON = { + action: "changeStream", + stream: "" + } + streamWindow.eventBridge.emitScriptEvent(JSON.stringify(streamJSON)); + } + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleIncreaseVolumeButton) { + volume += 0.2; + var volumeJSON = { + action: "changeVolume", + volume: volume + } + streamWindow.eventBridge.emitScriptEvent(JSON.stringify(volumeJSON)); + } + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleDecreaseVolumeButton) { + volume -= 0.2; + var volumeJSON = { + action: "changeVolume", + volume: volume + } + streamWindow.eventBridge.emitScriptEvent(JSON.stringify(volumeJSON)); + } +} + +// Call function. +Controller.mousePressEvent.connect(mousePressEvent); +streamWindow.setVisible(false); + +// Function to delete overlays upon exit. +function scriptEnding() { + Overlays.deleteOverlay(toggleStreamURLButton); + Overlays.deleteOverlay(toggleStreamPlayButton); + Overlays.deleteOverlay(toggleStreamStopButton); +} \ No newline at end of file From 2c088ec5a98d76f5786439314dcea635c3e7fc4b Mon Sep 17 00:00:00 2001 From: "Kevin M. Thomas" Date: Mon, 20 Jul 2015 14:21:42 -0400 Subject: [PATCH 034/242] Updated overlay clearing functionality. --- examples/jsstreamplayer.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/jsstreamplayer.js b/examples/jsstreamplayer.js index 05ebc7d968..6bd941f677 100644 --- a/examples/jsstreamplayer.js +++ b/examples/jsstreamplayer.js @@ -130,8 +130,13 @@ Controller.mousePressEvent.connect(mousePressEvent); streamWindow.setVisible(false); // Function to delete overlays upon exit. -function scriptEnding() { +function onScriptEnding() { Overlays.deleteOverlay(toggleStreamURLButton); Overlays.deleteOverlay(toggleStreamPlayButton); Overlays.deleteOverlay(toggleStreamStopButton); -} \ No newline at end of file + Overlays.deleteOverlay(toggleIncreaseVolumeButton); + Overlays.deleteOverlay(toggleDecreaseVolumeButton); +} + +// Call function. +Script.scriptEnding.connect(onScriptEnding); \ No newline at end of file From e75a6feafe3fd73fe599e47bdaf814399a70d547 Mon Sep 17 00:00:00 2001 From: ericrius1 Date: Mon, 20 Jul 2015 11:58:26 -0700 Subject: [PATCH 035/242] can toggle hit effect on and off from a script --- interface/src/Application.cpp | 1 + libraries/render-utils/src/RenderDeferredTask.cpp | 7 +++++++ libraries/render-utils/src/RenderDeferredTask.h | 4 ++++ libraries/render-utils/src/hit_effect.slf | 9 +++++---- libraries/render/src/render/Engine.h | 1 + libraries/script-engine/src/SceneScriptingInterface.h | 5 +++++ 6 files changed, 23 insertions(+), 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 342580e03c..0fb4bbcc74 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3570,6 +3570,7 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se renderContext._maxDrawnOverlay3DItems = sceneInterface->getEngineMaxDrawnOverlay3DItems(); renderContext._drawItemStatus = sceneInterface->doEngineDisplayItemStatus(); + renderContext._drawHitEffect = sceneInterface->doEngineDisplayHitEffect(); renderArgs->_shouldRender = LODManager::shouldRender; diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 95e0344313..b9767105c3 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -78,6 +78,9 @@ RenderDeferredTask::RenderDeferredTask() : Task() { _jobs.push_back(Job(new DrawOverlay3D::JobModel("DrawOverlay3D"))); _jobs.push_back(Job(new HitEffect::JobModel("HitEffect"))); + _jobs.back().setEnabled(false); + _drawHitEffectJobIndex = _jobs.size() -1; + _jobs.push_back(Job(new ResetGLState::JobModel())); @@ -106,6 +109,10 @@ void RenderDeferredTask::run(const SceneContextPointer& sceneContext, const Rend // Make sure we turn the displayItemStatus on/off setDrawItemStatus(renderContext->_drawItemStatus); + + //Make sure we display hit effect on screen, as desired from a script + setDrawHitEffect(renderContext->_drawHitEffect); + renderContext->args->_context->syncCache(); diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 4040606c62..b3957fe7dc 100755 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -71,9 +71,13 @@ public: render::Jobs _jobs; int _drawStatusJobIndex = -1; + int _drawHitEffectJobIndex = -1; void setDrawItemStatus(bool draw) { if (_drawStatusJobIndex >= 0) { _jobs[_drawStatusJobIndex].setEnabled(draw); } } bool doDrawItemStatus() const { if (_drawStatusJobIndex >= 0) { return _jobs[_drawStatusJobIndex].isEnabled(); } else { return false; } } + + void setDrawHitEffect(bool draw) { if (_drawHitEffectJobIndex >= 0) { _jobs[_drawHitEffectJobIndex].setEnabled(draw); } } + bool doDrawHitEffect() const { if (_drawHitEffectJobIndex >=0) { return _jobs[_drawHitEffectJobIndex].isEnabled(); } else { return false; } } virtual void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); diff --git a/libraries/render-utils/src/hit_effect.slf b/libraries/render-utils/src/hit_effect.slf index cd94de3620..5a5d85b21f 100644 --- a/libraries/render-utils/src/hit_effect.slf +++ b/libraries/render-utils/src/hit_effect.slf @@ -22,8 +22,9 @@ void main(void) { <$transformCameraViewport(cam, myViewport)$> vec2 center = vec2(myViewport.z/2.0, myViewport.w/2.0); float distFromCenter = distance(center, gl_FragCoord.xy); - //normalize - distFromCenter = distFromCenter/myViewport.z; - float alpha = mix(0.0, 1.0, distFromCenter); - gl_FragColor = vec4(0.7, 0.0, 0.0, alpha); + //normalize distance from center based on average of screen width and height + float normalizationFactor = (myViewport.z + myViewport.w)/2.0; + distFromCenter = distFromCenter/normalizationFactor; + float alpha = mix(0.0, 1.0, pow(distFromCenter, 1.5)); + gl_FragColor = vec4(1.0, 0.0, 0.0, alpha); } \ No newline at end of file diff --git a/libraries/render/src/render/Engine.h b/libraries/render/src/render/Engine.h index 1c600b13d6..e12d37118c 100644 --- a/libraries/render/src/render/Engine.h +++ b/libraries/render/src/render/Engine.h @@ -50,6 +50,7 @@ public: int _maxDrawnOverlay3DItems = -1; bool _drawItemStatus = false; + bool _drawHitEffect = false; RenderContext() {} }; diff --git a/libraries/script-engine/src/SceneScriptingInterface.h b/libraries/script-engine/src/SceneScriptingInterface.h index 674b452528..a3ce2a7300 100644 --- a/libraries/script-engine/src/SceneScriptingInterface.h +++ b/libraries/script-engine/src/SceneScriptingInterface.h @@ -109,6 +109,9 @@ public: Q_INVOKABLE void setEngineDisplayItemStatus(bool display) { _drawItemStatus = display; } Q_INVOKABLE bool doEngineDisplayItemStatus() { return _drawItemStatus; } + + Q_INVOKABLE void setEngineDisplayHitEffect(bool display) { _drawHitEffect = display; } + Q_INVOKABLE bool doEngineDisplayHitEffect() { return _drawHitEffect; } signals: void shouldRenderAvatarsChanged(bool shouldRenderAvatars); @@ -141,6 +144,8 @@ protected: int _maxDrawnOverlay3DItems = -1; bool _drawItemStatus = false; + + bool _drawHitEffect = false; }; From dcb20120703a48df6a9fa59072db819ea03b2435 Mon Sep 17 00:00:00 2001 From: ericrius1 Date: Mon, 20 Jul 2015 12:02:01 -0700 Subject: [PATCH 036/242] can toggle hit effect on and off from a script --- examples/example/games/hitEffect.js | 28 +++++++++++++++++++++++ libraries/render-utils/src/hit_effect.slf | 2 +- libraries/render-utils/src/hit_effect.slv | 4 ++-- 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 examples/example/games/hitEffect.js diff --git a/examples/example/games/hitEffect.js b/examples/example/games/hitEffect.js new file mode 100644 index 0000000000..0ba9ea14d1 --- /dev/null +++ b/examples/example/games/hitEffect.js @@ -0,0 +1,28 @@ +// +// hitEffect.js +// examples +// +// Created by Eric Levin on July 20, 2015 +// Copyright 2015 High Fidelity, Inc. +// +// An example of how to toggle a screen-space hit effect using the Scene global object. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +var hitEffectEnabled = false; + +toggleHitEffect(); + +function toggleHitEffect() { + Script.setTimeout(function() { + hitEffectEnabled = !hitEffectEnabled; + Scene.setEngineDisplayHitEffect(hitEffectEnabled); + toggleHitEffect(); + }, 1000); +} + + + + + diff --git a/libraries/render-utils/src/hit_effect.slf b/libraries/render-utils/src/hit_effect.slf index 5a5d85b21f..2c308f3711 100644 --- a/libraries/render-utils/src/hit_effect.slf +++ b/libraries/render-utils/src/hit_effect.slf @@ -2,7 +2,7 @@ <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> // -// ambient_occlusion.frag +// hit_effect.frag // fragment shader // // Created by Eric Levin on 7/20 diff --git a/libraries/render-utils/src/hit_effect.slv b/libraries/render-utils/src/hit_effect.slv index c12e3e71f8..e7c3062667 100644 --- a/libraries/render-utils/src/hit_effect.slv +++ b/libraries/render-utils/src/hit_effect.slv @@ -2,10 +2,10 @@ <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> // -// ambient_occlusion.vert +// hit_effect.vert // vertex shader // -// Created by Niraj Venkat on 7/20/15. +// Created by Eric Levin on 7/20/15. // Copyright 2015 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. From 075c9f05de9a3b340554f1792c35a74288ca7fcd Mon Sep 17 00:00:00 2001 From: Niraj Venkat Date: Mon, 20 Jul 2015 16:34:02 -0700 Subject: [PATCH 037/242] Introducing blend stage in the AO pipeline --- .../src/AmbientOcclusionEffect.cpp | 47 +++++++++++++++++-- .../render-utils/src/AmbientOcclusionEffect.h | 42 +---------------- .../render-utils/src/ambient_occlusion.slf | 4 +- .../render-utils/src/occlusion_blend.slf | 24 ++++++++++ .../render-utils/src/occlusion_blend.slv | 24 ++++++++++ 5 files changed, 94 insertions(+), 47 deletions(-) create mode 100644 libraries/render-utils/src/occlusion_blend.slf create mode 100644 libraries/render-utils/src/occlusion_blend.slv diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index 65d7313efb..a6646d8e4a 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -21,7 +21,6 @@ #include "AbstractViewStateInterface.h" #include "AmbientOcclusionEffect.h" -#include "ProgramObject.h" #include "RenderUtil.h" #include "TextureCache.h" #include "DependencyManager.h" @@ -33,6 +32,8 @@ #include "gaussian_blur_vertical_vert.h" #include "gaussian_blur_horizontal_vert.h" #include "gaussian_blur_frag.h" +#include "occlusion_blend_vert.h" +#include "occlusion_blend_frag.h" const int ROTATION_WIDTH = 4; const int ROTATION_HEIGHT = 4; @@ -187,6 +188,9 @@ const gpu::PipelinePointer& AmbientOcclusion::getOcclusionPipeline() { gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vs, ps)); gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("depthTexture"), 0)); + slotBindings.insert(gpu::Shader::Binding(std::string("normalTexture"), 1)); + gpu::Shader::makeProgram(*program, slotBindings); //_drawItemBoundPosLoc = program->getUniforms().findLocation("inBoundPos"); @@ -282,6 +286,39 @@ const gpu::PipelinePointer& AmbientOcclusion::getHBlurPipeline() { return _hBlurPipeline; } +const gpu::PipelinePointer& AmbientOcclusion::getBlendPipeline() { + if (!_blendPipeline) { + auto vs = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(occlusion_blend_vert))); + auto ps = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(occlusion_blend_frag))); + gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vs, ps)); + + gpu::Shader::BindingSet slotBindings; + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + state->setDepthTest(false, false, gpu::LESS_EQUAL); + + // Blend on transparent + state->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::DEST_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ZERO); + + // Link the horizontal blur FBO to texture + _hBlurBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, + DependencyManager::get()->getFrameBufferSize().width(), DependencyManager::get()->getFrameBufferSize().height())); + auto format = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); + auto width = _hBlurBuffer->getWidth(); + auto height = _hBlurBuffer->getHeight(); + auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); + _hBlurTexture = gpu::TexturePointer(gpu::Texture::create2D(format, width, height, defaultSampler)); + + // Good to go add the brand new pipeline + _blendPipeline.reset(gpu::Pipeline::create(program, state)); + } + return _blendPipeline; +} + void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext) { // create a simple pipeline that does: @@ -308,7 +345,7 @@ void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, cons // Occlusion step getOcclusionPipeline(); batch.setResourceTexture(0, DependencyManager::get()->getPrimaryDepthTexture()); - batch.setResourceTexture(1, DependencyManager::get()->getPrimaryNormalTexture()); + batch.setResourceTexture(1, DependencyManager::get()->getPrimaryFramebuffer()->getRenderBuffer(0)); _occlusionBuffer->setRenderBuffer(0, _occlusionTexture); batch.setFramebuffer(_occlusionBuffer); @@ -339,7 +376,7 @@ void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, cons _hBlurBuffer->setRenderBuffer(0, _hBlurTexture); batch.setFramebuffer(_hBlurBuffer); - // bind the second gpu::Pipeline we need - for calculating blur buffer + // bind the third gpu::Pipeline we need - for calculating blur buffer batch.setPipeline(getHBlurPipeline()); DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); @@ -348,8 +385,8 @@ void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, cons batch.setResourceTexture(0, _hBlurTexture); batch.setFramebuffer(DependencyManager::get()->getPrimaryFramebuffer()); - // bind the second gpu::Pipeline we need - batch.setPipeline(getOcclusionPipeline()); + // bind the fourth gpu::Pipeline we need - for + batch.setPipeline(getBlendPipeline()); glm::vec2 bottomLeftSmall(0.5f, -1.0f); glm::vec2 topRightSmall(1.0f, -0.5f); diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.h b/libraries/render-utils/src/AmbientOcclusionEffect.h index a8ae33b690..d38624f24c 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.h +++ b/libraries/render-utils/src/AmbientOcclusionEffect.h @@ -16,46 +16,6 @@ #include "render/DrawTask.h" -class AbstractViewStateInterface; -class ProgramObject; - -/// A screen space ambient occlusion effect. See John Chapman's tutorial at -/// http://john-chapman-graphics.blogspot.co.uk/2013/01/ssao-tutorial.html for reference. - -/* -class AmbientOcclusionEffect : public Dependency { - SINGLETON_DEPENDENCY - -public: - - void init(AbstractViewStateInterface* viewState); - - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); - typedef render::Job::Model JobModel; - - - AmbientOcclusionEffect() {} - virtual ~AmbientOcclusionEffect() {} - -private: - - ProgramObject* _occlusionProgram; - int _nearLocation; - int _farLocation; - int _leftBottomLocation; - int _rightTopLocation; - int _noiseScaleLocation; - int _texCoordOffsetLocation; - int _texCoordScaleLocation; - - ProgramObject* _blurProgram; - int _blurScaleLocation; - - GLuint _rotationTextureID; - AbstractViewStateInterface* _viewState; -}; -*/ - class AmbientOcclusion { public: @@ -67,12 +27,14 @@ public: const gpu::PipelinePointer& getOcclusionPipeline(); const gpu::PipelinePointer& getHBlurPipeline(); const gpu::PipelinePointer& getVBlurPipeline(); + const gpu::PipelinePointer& getBlendPipeline(); private: gpu::PipelinePointer _occlusionPipeline; gpu::PipelinePointer _hBlurPipeline; gpu::PipelinePointer _vBlurPipeline; + gpu::PipelinePointer _blendPipeline; gpu::FramebufferPointer _occlusionBuffer; gpu::FramebufferPointer _hBlurBuffer; diff --git a/libraries/render-utils/src/ambient_occlusion.slf b/libraries/render-utils/src/ambient_occlusion.slf index 4b7d6bfe2b..342f4148d2 100644 --- a/libraries/render-utils/src/ambient_occlusion.slf +++ b/libraries/render-utils/src/ambient_occlusion.slf @@ -40,9 +40,9 @@ void main(void) { float c = (2.0 * n) / (f + n - z * (f - n)); // convert to linear values //gl_FragColor = vec4(c, c, c, 1.0); - gl_FragColor = normalColor; + gl_FragColor = mix(depthColor, normalColor, normalColor.a); //vec3 p = getPosition(i.uv); //vec3 n = getNormal(i.uv); - vec2 rand = vec2(getRandom(varTexcoord.xy), getRandom(varTexcoord.yx)); + //vec2 rand = vec2(getRandom(varTexcoord.xy), getRandom(varTexcoord.yx)); } diff --git a/libraries/render-utils/src/occlusion_blend.slf b/libraries/render-utils/src/occlusion_blend.slf new file mode 100644 index 0000000000..30d3173f90 --- /dev/null +++ b/libraries/render-utils/src/occlusion_blend.slf @@ -0,0 +1,24 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// occlusion_blend.frag +// fragment shader +// +// Created by Niraj Venkat on 7/20/15. +// 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 DeferredBufferWrite.slh@> + +varying vec2 varTexcoord; + +uniform sampler2D blurredOcclusionTexture; + +void main(void) { + vec4 depthColor = texture2D(blurredOcclusionTexture, varTexcoord.xy); + gl_FragColor = depthColor; +} diff --git a/libraries/render-utils/src/occlusion_blend.slv b/libraries/render-utils/src/occlusion_blend.slv new file mode 100644 index 0000000000..53380a494f --- /dev/null +++ b/libraries/render-utils/src/occlusion_blend.slv @@ -0,0 +1,24 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// occlusion_blend.vert +// vertex shader +// +// Created by Niraj Venkat on 7/20/15. +// 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 gpu/Transform.slh@> + +<$declareStandardTransform()$> + +varying vec2 varTexcoord; + +void main(void) { + varTexcoord = gl_MultiTexCoord0.xy; + gl_Position = gl_Vertex; +} From abfe60aa20d326484323bb0039c45685aeae428a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 20 Jul 2015 18:31:42 -0700 Subject: [PATCH 038/242] whenever Model class uses _jointState, use the Rig version if there is a Rig. --- interface/src/avatar/SkeletonModel.cpp | 12 +- interface/src/avatar/SkeletonModel.h | 2 +- libraries/animation/src/Rig.cpp | 355 ++++++++++++++++- libraries/animation/src/Rig.h | 27 +- libraries/render-utils/src/Model.cpp | 522 +++++++++++++++---------- libraries/render-utils/src/Model.h | 7 +- 6 files changed, 719 insertions(+), 206 deletions(-) diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 0347e217a3..3d76b11bac 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -269,21 +269,22 @@ void SkeletonModel::applyPalmData(int jointIndex, PalmData& palm) { } } + void SkeletonModel::updateJointState(int index) { - if (index > _jointStates.size()) { + if (index < 0 && index >= _jointStates.size()) { return; // bail } JointState& state = _jointStates[index]; const FBXJoint& joint = state.getFBXJoint(); - if (joint.parentIndex != -1 && joint.parentIndex <= _jointStates.size()) { + if (joint.parentIndex >= 0 && joint.parentIndex < _jointStates.size()) { const JointState& parentState = _jointStates.at(joint.parentIndex); const FBXGeometry& geometry = _geometry->getFBXGeometry(); if (index == geometry.leanJointIndex) { maybeUpdateLeanRotation(parentState, state); - + } else if (index == geometry.neckJointIndex) { - maybeUpdateNeckRotation(parentState, joint, state); - + maybeUpdateNeckRotation(parentState, joint, state); + } else if (index == geometry.leftEyeJointIndex || index == geometry.rightEyeJointIndex) { maybeUpdateEyeRotation(parentState, joint, state); } @@ -296,6 +297,7 @@ void SkeletonModel::updateJointState(int index) { } } + void SkeletonModel::maybeUpdateLeanRotation(const JointState& parentState, JointState& state) { if (!_owningAvatar->isMyAvatar()) { return; diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 6d33b3da7b..ae8ffe66a9 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -161,7 +161,7 @@ private: void setHandPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation); bool getEyeModelPositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const; - + Avatar* _owningAvatar; CapsuleShape _boundingShape; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 0825a615b7..a76c866140 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -35,7 +35,7 @@ bool Rig::isRunningAnimation(AnimationHandlePointer animationHandle) { return _runningAnimations.contains(animationHandle); } -void Rig::initJointStates(glm::vec3 scale, glm::vec3 offset, QVector states) { +float Rig::initJointStates(glm::vec3 scale, glm::vec3 offset, QVector states) { _jointStates = states; initJointTransforms(scale, offset); @@ -53,6 +53,7 @@ void Rig::initJointStates(glm::vec3 scale, glm::vec3 offset, QVector } // XXX update AnimationHandles from here? + return radius; } void Rig::initJointTransforms(glm::vec3 scale, glm::vec3 offset) { @@ -86,8 +87,360 @@ void Rig::resetJoints() { } } +bool Rig::getJointState(int index, glm::quat& rotation) const { + if (index == -1 || index >= _jointStates.size()) { + return false; + } + const JointState& state = _jointStates.at(index); + rotation = state.getRotationInConstrainedFrame(); + return !state.rotationIsDefault(rotation); +} + +bool Rig::getVisibleJointState(int index, glm::quat& rotation) const { + if (index == -1 || index >= _jointStates.size()) { + return false; + } + const JointState& state = _jointStates.at(index); + rotation = state.getVisibleRotationInConstrainedFrame(); + return !state.rotationIsDefault(rotation); +} + +void Rig::updateVisibleJointStates() { + for (int i = 0; i < _jointStates.size(); i++) { + _jointStates[i].slaveVisibleTransform(); + } +} + +void Rig::clearJointState(int index) { + if (index != -1 && index < _jointStates.size()) { + JointState& state = _jointStates[index]; + state.setRotationInConstrainedFrame(glm::quat(), 0.0f); + } +} + +void Rig::clearJointStates() { + _jointStates.clear(); +} + +void Rig::setJointState(int index, bool valid, const glm::quat& rotation, float priority) { + if (index != -1 && index < _jointStates.size()) { + JointState& state = _jointStates[index]; + if (valid) { + state.setRotationInConstrainedFrame(rotation, priority); + } else { + state.restoreRotation(1.0f, priority); + } + } +} + + +void Rig::clearJointAnimationPriority(int index) { + if (index != -1 && index < _jointStates.size()) { + _jointStates[index]._animationPriority = 0.0f; + } +} + AnimationHandlePointer Rig::createAnimationHandle() { AnimationHandlePointer handle(new AnimationHandle(getRigPointer())); _animationHandles.insert(handle); return handle; } + +bool Rig::getJointStateAtIndex(int jointIndex, JointState& jointState) const { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + return false; + } + jointState = _jointStates[jointIndex]; + return true; +} + +void Rig::updateJointStates(glm::mat4 parentTransform) { + for (int i = 0; i < _jointStates.size(); i++) { + updateJointState(i, parentTransform); + } +} + +void Rig::updateJointState(int index, glm::mat4 parentTransform) { + JointState& state = _jointStates[index]; + const FBXJoint& joint = state.getFBXJoint(); + + // compute model transforms + int parentIndex = joint.parentIndex; + if (parentIndex == -1) { + // glm::mat4 parentTransform = glm::scale(scale) * glm::translate(offset) * geometryOffset; + state.computeTransform(parentTransform); + } else { + // guard against out-of-bounds access to _jointStates + if (joint.parentIndex >= 0 && joint.parentIndex < _jointStates.size()) { + const JointState& parentState = _jointStates.at(parentIndex); + state.computeTransform(parentState.getTransform(), parentState.getTransformChanged()); + } + } +} + +void Rig::resetAllTransformsChanged() { + for (int i = 0; i < _jointStates.size(); i++) { + _jointStates[i].resetTransformChanged(); + } +} + +glm::quat Rig::setJointRotationInBindFrame(int jointIndex, const glm::quat& rotation, float priority) { + glm::quat endRotation; + if (jointIndex == -1 || _jointStates.isEmpty()) { + return endRotation; + } + JointState& state = _jointStates[jointIndex]; + state.setRotationInBindFrame(rotation, priority); + endRotation = state.getRotationInBindFrame(); + return endRotation; +} + +void Rig::applyJointRotationDelta(int jointIndex, const glm::quat& delta, bool constrain, float priority) { + if (jointIndex == -1 || _jointStates.isEmpty()) { + return; + } + _jointStates[jointIndex].applyRotationDelta(delta, constrain, priority); +} + +bool Rig::setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, + bool useRotation, int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, + float priority, glm::mat4 parentTransform) { + if (jointIndex == -1 || _jointStates.isEmpty()) { + return false; + } + + // const FBXGeometry& geometry = _geometry->getFBXGeometry(); + // const QVector& freeLineage = geometry.joints.at(jointIndex).freeLineage; + const FBXJoint& fbxJoint = _jointStates[jointIndex].getFBXJoint(); + const QVector& freeLineage = fbxJoint.freeLineage; + + + if (freeLineage.isEmpty()) { + return false; + } + if (lastFreeIndex == -1) { + lastFreeIndex = freeLineage.last(); + } + + // this is a cyclic coordinate descent algorithm: see + // http://www.ryanjuckett.com/programming/animation/21-cyclic-coordinate-descent-in-2d + const int ITERATION_COUNT = 1; + glm::vec3 worldAlignment = alignment; + for (int i = 0; i < ITERATION_COUNT; i++) { + // first, try to rotate the end effector as close as possible to the target rotation, if any + glm::quat endRotation; + if (useRotation) { + JointState& state = _jointStates[jointIndex]; + + state.setRotationInBindFrame(rotation, priority); + endRotation = state.getRotationInBindFrame(); + } + + // then, we go from the joint upwards, rotating the end as close as possible to the target + glm::vec3 endPosition = extractTranslation(_jointStates[jointIndex].getTransform()); + for (int j = 1; freeLineage.at(j - 1) != lastFreeIndex; j++) { + int index = freeLineage.at(j); + JointState& state = _jointStates[index]; + const FBXJoint& joint = state.getFBXJoint(); + if (!(joint.isFree || allIntermediatesFree)) { + continue; + } + glm::vec3 jointPosition = extractTranslation(state.getTransform()); + glm::vec3 jointVector = endPosition - jointPosition; + glm::quat oldCombinedRotation = state.getRotation(); + glm::quat combinedDelta; + float combinedWeight; + if (useRotation) { + combinedDelta = safeMix(rotation * glm::inverse(endRotation), + rotationBetween(jointVector, position - jointPosition), 0.5f); + combinedWeight = 2.0f; + + } else { + combinedDelta = rotationBetween(jointVector, position - jointPosition); + combinedWeight = 1.0f; + } + if (alignment != glm::vec3() && j > 1) { + jointVector = endPosition - jointPosition; + glm::vec3 positionSum; + for (int k = j - 1; k > 0; k--) { + int index = freeLineage.at(k); + updateJointState(index, parentTransform); + positionSum += extractTranslation(_jointStates.at(index).getTransform()); + } + glm::vec3 projectedCenterOfMass = glm::cross(jointVector, + glm::cross(positionSum / (j - 1.0f) - jointPosition, jointVector)); + glm::vec3 projectedAlignment = glm::cross(jointVector, glm::cross(worldAlignment, jointVector)); + const float LENGTH_EPSILON = 0.001f; + if (glm::length(projectedCenterOfMass) > LENGTH_EPSILON && glm::length(projectedAlignment) > LENGTH_EPSILON) { + combinedDelta = safeMix(combinedDelta, rotationBetween(projectedCenterOfMass, projectedAlignment), + 1.0f / (combinedWeight + 1.0f)); + } + } + state.applyRotationDelta(combinedDelta, true, priority); + glm::quat actualDelta = state.getRotation() * glm::inverse(oldCombinedRotation); + endPosition = actualDelta * jointVector + jointPosition; + if (useRotation) { + endRotation = actualDelta * endRotation; + } + } + } + + // now update the joint states from the top + for (int j = freeLineage.size() - 1; j >= 0; j--) { + updateJointState(freeLineage.at(j), parentTransform); + } + + return true; +} + +void Rig::inverseKinematics(int endIndex, glm::vec3 targetPosition, + const glm::quat& targetRotation, float priority, glm::mat4 parentTransform) { + // NOTE: targetRotation is from bind- to model-frame + + if (endIndex == -1 || _jointStates.isEmpty()) { + return; + } + + // const FBXGeometry& geometry = _geometry->getFBXGeometry(); + // const QVector& freeLineage = geometry.joints.at(endIndex).freeLineage; + const FBXJoint& fbxJoint = _jointStates[endIndex].getFBXJoint(); + const QVector& freeLineage = fbxJoint.freeLineage; + + if (freeLineage.isEmpty()) { + return; + } + int numFree = freeLineage.size(); + + // store and remember topmost parent transform + glm::mat4 topParentTransform; + { + int index = freeLineage.last(); + const JointState& state = _jointStates.at(index); + const FBXJoint& joint = state.getFBXJoint(); + int parentIndex = joint.parentIndex; + if (parentIndex == -1) { + topParentTransform = parentTransform; + } else { + topParentTransform = _jointStates[parentIndex].getTransform(); + } + } + + // this is a cyclic coordinate descent algorithm: see + // http://www.ryanjuckett.com/programming/animation/21-cyclic-coordinate-descent-in-2d + + // keep track of the position of the end-effector + JointState& endState = _jointStates[endIndex]; + glm::vec3 endPosition = endState.getPosition(); + float distanceToGo = glm::distance(targetPosition, endPosition); + + const int MAX_ITERATION_COUNT = 2; + const float ACCEPTABLE_IK_ERROR = 0.005f; // 5mm + int numIterations = 0; + do { + ++numIterations; + // moving up, rotate each free joint to get endPosition closer to target + for (int j = 1; j < numFree; j++) { + int nextIndex = freeLineage.at(j); + JointState& nextState = _jointStates[nextIndex]; + FBXJoint nextJoint = nextState.getFBXJoint(); + if (! nextJoint.isFree) { + continue; + } + + glm::vec3 pivot = nextState.getPosition(); + glm::vec3 leverArm = endPosition - pivot; + float leverLength = glm::length(leverArm); + if (leverLength < EPSILON) { + continue; + } + glm::quat deltaRotation = rotationBetween(leverArm, targetPosition - pivot); + + // We want to mix the shortest rotation with one that will pull the system down with gravity + // so that limbs don't float unrealistically. To do this we compute a simplified center of mass + // where each joint has unit mass and we don't bother averaging it because we only need direction. + if (j > 1) { + + glm::vec3 centerOfMass(0.0f); + for (int k = 0; k < j; ++k) { + int massIndex = freeLineage.at(k); + centerOfMass += _jointStates[massIndex].getPosition() - pivot; + } + // the gravitational effect is a rotation that tends to align the two cross products + const glm::vec3 worldAlignment = glm::vec3(0.0f, -1.0f, 0.0f); + glm::quat gravityDelta = rotationBetween(glm::cross(centerOfMass, leverArm), + glm::cross(worldAlignment, leverArm)); + + float gravityAngle = glm::angle(gravityDelta); + const float MIN_GRAVITY_ANGLE = 0.1f; + float mixFactor = 0.5f; + if (gravityAngle < MIN_GRAVITY_ANGLE) { + // the final rotation is a mix of the two + mixFactor = 0.5f * gravityAngle / MIN_GRAVITY_ANGLE; + } + deltaRotation = safeMix(deltaRotation, gravityDelta, mixFactor); + } + + // Apply the rotation, but use mixRotationDelta() which blends a bit of the default pose + // in the process. This provides stability to the IK solution for most models. + glm::quat oldNextRotation = nextState.getRotation(); + float mixFactor = 0.03f; + nextState.mixRotationDelta(deltaRotation, mixFactor, priority); + + // measure the result of the rotation which may have been modified by + // blending and constraints + glm::quat actualDelta = nextState.getRotation() * glm::inverse(oldNextRotation); + endPosition = pivot + actualDelta * leverArm; + } + + // recompute transforms from the top down + glm::mat4 parentTransform = topParentTransform; + for (int j = numFree - 1; j >= 0; --j) { + JointState& freeState = _jointStates[freeLineage.at(j)]; + freeState.computeTransform(parentTransform); + parentTransform = freeState.getTransform(); + } + + // measure our success + endPosition = endState.getPosition(); + distanceToGo = glm::distance(targetPosition, endPosition); + } while (numIterations < MAX_ITERATION_COUNT && distanceToGo < ACCEPTABLE_IK_ERROR); + + // set final rotation of the end joint + endState.setRotationInBindFrame(targetRotation, priority, true); +} + +bool Rig::restoreJointPosition(int jointIndex, float fraction, float priority) { + if (jointIndex == -1 || _jointStates.isEmpty()) { + return false; + } + // const FBXGeometry& geometry = _geometry->getFBXGeometry(); + // const QVector& freeLineage = geometry.joints.at(jointIndex).freeLineage; + const FBXJoint& fbxJoint = _jointStates[jointIndex].getFBXJoint(); + const QVector& freeLineage = fbxJoint.freeLineage; + + foreach (int index, freeLineage) { + JointState& state = _jointStates[index]; + state.restoreRotation(fraction, priority); + } + return true; +} + +float Rig::getLimbLength(int jointIndex, glm::vec3 scale) const { + if (jointIndex == -1 || _jointStates.isEmpty()) { + return 0.0f; + } + + // const FBXGeometry& geometry = _geometry->getFBXGeometry(); + // const QVector& freeLineage = geometry.joints.at(jointIndex).freeLineage; + const FBXJoint& fbxJoint = _jointStates[jointIndex].getFBXJoint(); + const QVector& freeLineage = fbxJoint.freeLineage; + + float length = 0.0f; + float lengthScale = (scale.x + scale.y + scale.z) / 3.0f; + for (int i = freeLineage.size() - 2; i >= 0; i--) { + int something = freeLineage.at(i); + const FBXJoint& fbxJointI = _jointStates[something].getFBXJoint(); + length += fbxJointI.distanceToParent * lengthScale; + } + return length; +} diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index c3e1edd415..552601f44f 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -36,14 +36,39 @@ public: bool isRunningAnimation(AnimationHandlePointer animationHandle); const QList& getRunningAnimations() const { return _runningAnimations; } - void initJointStates(glm::vec3 scale, glm::vec3 offset, QVector states); + float initJointStates(glm::vec3 scale, glm::vec3 offset, QVector states); void initJointTransforms(glm::vec3 scale, glm::vec3 offset); void resetJoints(); + bool jointStatesEmpty() { return _jointStates.isEmpty(); }; + int jointStateCount() const { return _jointStates.size(); } + bool getJointStateAtIndex(int jointIndex, JointState& jointState) const; + + void updateJointStates(glm::mat4 parentTransform); + void updateJointState(int index, glm::mat4 parentTransform); + void resetAllTransformsChanged(); + + bool getJointState(int index, glm::quat& rotation) const; + bool getVisibleJointState(int index, glm::quat& rotation) const; + void updateVisibleJointStates(); + void clearJointState(int index); + void clearJointStates(); + void setJointState(int index, bool valid, const glm::quat& rotation, float priority); + void clearJointAnimationPriority(int index); + glm::quat setJointRotationInBindFrame(int jointIndex, const glm::quat& rotation, float priority); + void applyJointRotationDelta(int jointIndex, const glm::quat& delta, bool constrain, float priority); QVector getJointStates() { return _jointStates; } AnimationHandlePointer createAnimationHandle(); + bool setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, + bool useRotation, int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, + float priority, glm::mat4 parentTransform); + void inverseKinematics(int endIndex, glm::vec3 targetPosition, + const glm::quat& targetRotation, float priority, glm::mat4 parentTransform); + bool restoreJointPosition(int jointIndex, float fraction, float priority); + float getLimbLength(int jointIndex, glm::vec3 scale) const; + protected: QVector _jointStates; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 7bb96a9082..52503a0154 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -85,12 +85,12 @@ Model::Model(QObject* parent, RigPointer rig) : _isWireframe(false), _renderCollisionHull(false), _rig(rig) { - + // we may have been created in the network thread, but we live in the main thread if (_viewState) { moveToThread(_viewState->getMainThread()); } - + setSnapModelToRegistrationPoint(true, glm::vec3(0.5f)); } @@ -115,14 +115,14 @@ void Model::RenderPipelineLib::addRenderPipeline(Model::RenderKey key, gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vertexShader, pixelShader)); gpu::Shader::makeProgram(*program, slotBindings); - - + + auto locations = std::shared_ptr(new Locations()); initLocations(program, *locations); - + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - + // Backface on shadow if (key.isShadow()) { state->setCullMode(gpu::State::CULL_FRONT); @@ -137,29 +137,30 @@ void Model::RenderPipelineLib::addRenderPipeline(Model::RenderKey key, // Blend on transparent state->setBlendFunction(key.isTranslucent(), - gpu::State::ONE, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, // For transparent only, this keep the highlight intensity - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + gpu::State::ONE, gpu::State::BLEND_OP_ADD, + gpu::State::INV_SRC_ALPHA, // For transparent only, this keep the highlight intensity + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); // Good to go add the brand new pipeline auto pipeline = gpu::PipelinePointer(gpu::Pipeline::create(program, state)); insert(value_type(key.getRaw(), RenderPipeline(pipeline, locations))); - - + + if (!key.isWireFrame()) { - + RenderKey wireframeKey(key.getRaw() | RenderKey::IS_WIREFRAME); gpu::StatePointer wireframeState = gpu::StatePointer(new gpu::State(state->getValues())); - + wireframeState->setFillMode(gpu::State::FILL_LINE); - + // create a new RenderPipeline with the same shader side and the mirrorState auto wireframePipeline = gpu::PipelinePointer(gpu::Pipeline::create(program, wireframeState)); insert(value_type(wireframeKey.getRaw(), RenderPipeline(wireframePipeline, locations))); } - + // If not a shadow pass, create the mirror version from the same state, just change the FrontFace if (!key.isShadow()) { - + RenderKey mirrorKey(key.getRaw() | RenderKey::IS_MIRROR); gpu::StatePointer mirrorState = gpu::StatePointer(new gpu::State(state->getValues())); @@ -168,13 +169,13 @@ void Model::RenderPipelineLib::addRenderPipeline(Model::RenderKey key, // create a new RenderPipeline with the same shader side and the mirrorState auto mirrorPipeline = gpu::PipelinePointer(gpu::Pipeline::create(program, mirrorState)); insert(value_type(mirrorKey.getRaw(), RenderPipeline(mirrorPipeline, locations))); - + if (!key.isWireFrame()) { RenderKey wireframeKey(key.getRaw() | RenderKey::IS_MIRROR | RenderKey::IS_WIREFRAME); gpu::StatePointer wireframeState = gpu::StatePointer(new gpu::State(state->getValues()));; - + wireframeState->setFillMode(gpu::State::FILL_LINE); - + // create a new RenderPipeline with the same shader side and the mirrorState auto wireframePipeline = gpu::PipelinePointer(gpu::Pipeline::create(program, wireframeState)); insert(value_type(wireframeKey.getRaw(), RenderPipeline(wireframePipeline, locations))); @@ -203,7 +204,7 @@ void Model::RenderPipelineLib::initLocations(gpu::ShaderPointer& program, Model: locations.clusterIndices = program->getInputs().findLocation("clusterIndices");; locations.clusterWeights = program->getInputs().findLocation("clusterWeights");; - + } @@ -233,12 +234,12 @@ void Model::setScaleInternal(const glm::vec3& scale) { } } -void Model::setOffset(const glm::vec3& offset) { - _offset = offset; - +void Model::setOffset(const glm::vec3& offset) { + _offset = offset; + // if someone manually sets our offset, then we are no longer snapped to center - _snapModelToRegistrationPoint = false; - _snappedToRegistrationPoint = false; + _snapModelToRegistrationPoint = false; + _snappedToRegistrationPoint = false; } QVector Model::createJointStates(const FBXGeometry& geometry) { @@ -255,20 +256,24 @@ QVector Model::createJointStates(const FBXGeometry& geometry) { }; void Model::initJointTransforms() { - // compute model transforms - int numStates = _jointStates.size(); - for (int i = 0; i < numStates; ++i) { - JointState& state = _jointStates[i]; - const FBXJoint& joint = state.getFBXJoint(); - int parentIndex = joint.parentIndex; - if (parentIndex == -1) { - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - // NOTE: in practice geometry.offset has a non-unity scale (rather than a translation) - glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - state.initTransform(parentTransform); - } else { - const JointState& parentState = _jointStates.at(parentIndex); - state.initTransform(parentState.getTransform()); + if (_rig) { + _rig->initJointTransforms(_scale, _offset); + } else { + // compute model transforms + int numStates = _jointStates.size(); + for (int i = 0; i < numStates; ++i) { + JointState& state = _jointStates[i]; + const FBXJoint& joint = state.getFBXJoint(); + int parentIndex = joint.parentIndex; + if (parentIndex == -1) { + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + // NOTE: in practice geometry.offset has a non-unity scale (rather than a translation) + glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; + state.initTransform(parentTransform); + } else { + const JointState& parentState = _jointStates.at(parentIndex); + state.initTransform(parentState.getTransform()); + } } } } @@ -298,7 +303,7 @@ void Model::init() { auto modelLightmapNormalSpecularMapPixel = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(model_lightmap_normal_specular_map_frag))); // Fill the renderPipelineLib - + _renderPipelineLib.addRenderPipeline( RenderKey(0), modelVertex, modelPixel); @@ -315,7 +320,7 @@ void Model::init() { RenderKey(RenderKey::HAS_TANGENTS | RenderKey::HAS_SPECULAR), modelNormalMapVertex, modelNormalSpecularMapPixel); - + _renderPipelineLib.addRenderPipeline( RenderKey(RenderKey::IS_TRANSLUCENT), modelVertex, modelTranslucentPixel); @@ -323,7 +328,7 @@ void Model::init() { _renderPipelineLib.addRenderPipeline( RenderKey(RenderKey::IS_TRANSLUCENT | RenderKey::HAS_LIGHTMAP), modelVertex, modelTranslucentPixel); - + _renderPipelineLib.addRenderPipeline( RenderKey(RenderKey::HAS_TANGENTS | RenderKey::IS_TRANSLUCENT), modelNormalMapVertex, modelTranslucentPixel); @@ -399,14 +404,18 @@ void Model::init() { } void Model::reset() { - if (_jointStates.isEmpty()) { - return; + if (_rig) { + _rig->resetJoints(); + } else { + if (_jointStates.isEmpty()) { + return; + } + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + for (int i = 0; i < _jointStates.size(); i++) { + _jointStates[i].setRotationInConstrainedFrame(geometry.joints.at(i).rotation, 0.0f); + } } - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - for (int i = 0; i < _jointStates.size(); i++) { - _jointStates[i].setRotationInConstrainedFrame(geometry.joints.at(i).rotation, 0.0f); - } - + _meshGroupsKnown = false; _readyWhenAdded = false; // in case any of our users are using scenes invalidCalculatedMeshBoxes(); // if we have to reload, we need to assume our mesh boxes are all invalid @@ -431,6 +440,8 @@ bool Model::updateGeometry() { return false; } + bool jointStatesEmpty = _rig ? _rig->jointStatesEmpty() : _jointStates.isEmpty(); + QSharedPointer geometry = _geometry->getLODOrFallback(_lodDistance, _lodHysteresis); if (_geometry != geometry) { @@ -439,7 +450,7 @@ bool Model::updateGeometry() { const FBXGeometry& newGeometry = geometry->getFBXGeometry(); QVector newJointStates = createJointStates(newGeometry); - if (! _jointStates.isEmpty()) { + if (! jointStatesEmpty) { // copy the existing joint states const FBXGeometry& oldGeometry = _geometry->getFBXGeometry(); for (QHash::const_iterator it = oldGeometry.jointIndices.constBegin(); @@ -447,20 +458,24 @@ bool Model::updateGeometry() { int oldIndex = it.value() - 1; int newIndex = newGeometry.getJointIndex(it.key()); if (newIndex != -1) { - newJointStates[newIndex].copyState(_jointStates[oldIndex]); + JointState jointState; + if (!getJointStateAtIndex(oldIndex, jointState)) { + return false; + } + newJointStates[newIndex].copyState(jointState); } } - } + } deleteGeometry(); _dilatedTextures.clear(); setGeometry(geometry); - + _meshGroupsKnown = false; _readyWhenAdded = false; // in case any of our users are using scenes invalidCalculatedMeshBoxes(); // if we have to reload, we need to assume our mesh boxes are all invalid initJointStates(newJointStates); needToRebuild = true; - } else if (_jointStates.isEmpty()) { + } else if (jointStatesEmpty) { const FBXGeometry& fbxGeometry = geometry->getFBXGeometry(); if (fbxGeometry.joints.size() > 0) { initJointStates(createJointStates(fbxGeometry)); @@ -472,13 +487,13 @@ bool Model::updateGeometry() { } _geometry->setLoadPriority(this, -_lodDistance); _geometry->ensureLoading(); - + if (needToRebuild) { const FBXGeometry& fbxGeometry = geometry->getFBXGeometry(); foreach (const FBXMesh& mesh, fbxGeometry.meshes) { MeshState state; state.clusterMatrices.resize(mesh.clusters.size()); - _meshStates.append(state); + _meshStates.append(state); gpu::BufferPointer buffer(new gpu::Buffer()); if (!mesh.blendshapes.isEmpty()) { @@ -496,25 +511,29 @@ bool Model::updateGeometry() { // virtual void Model::initJointStates(QVector states) { - _jointStates = states; - initJointTransforms(); + if (_rig) { + _boundingRadius = _rig->initJointStates(_scale, _offset, states); + } else { + _jointStates = states; + initJointTransforms(); - int numStates = _jointStates.size(); - float radius = 0.0f; - for (int i = 0; i < numStates; ++i) { - float distance = glm::length(_jointStates[i].getPosition()); - if (distance > radius) { - radius = distance; + int numStates = _jointStates.size(); + float radius = 0.0f; + for (int i = 0; i < numStates; ++i) { + float distance = glm::length(_jointStates[i].getPosition()); + if (distance > radius) { + radius = distance; + } + _jointStates[i].buildConstraint(); } - _jointStates[i].buildConstraint(); + for (int i = 0; i < _jointStates.size(); i++) { + _jointStates[i].slaveVisibleTransform(); + } + _boundingRadius = radius; } - for (int i = 0; i < _jointStates.size(); i++) { - _jointStates[i].slaveVisibleTransform(); - } - _boundingRadius = radius; } -bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, +bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, QString& extraInfo, bool pickAgainstTriangles) { bool intersectedSomething = false; @@ -523,7 +542,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g if (!isActive()) { return intersectedSomething; } - + // extents is the entity relative, scaled, centered extents of the entity glm::vec3 position = _translation; glm::mat4 rotation = glm::mat4_cast(_rotation); @@ -532,7 +551,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g glm::mat4 worldToModelMatrix = glm::inverse(modelToWorldMatrix); Extents modelExtents = getMeshExtents(); // NOTE: unrotated - + glm::vec3 dimensions = modelExtents.maximum - modelExtents.minimum; glm::vec3 corner = -(dimensions * _registrationPoint); // since we're going to do the ray picking in the model frame of reference AABox modelFrameBox(corner, dimensions); @@ -571,7 +590,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g int t = 0; foreach (const Triangle& triangle, meshTriangles) { t++; - + float thisTriangleDistance; if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance)) { if (thisTriangleDistance < bestDistance) { @@ -590,7 +609,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g extraInfo = geometry.getModelNameOfMesh(subMeshIndex); } } - } + } subMeshIndex++; } _mutex.unlock(); @@ -598,7 +617,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g if (intersectedSomething) { distance = bestDistance; } - + return intersectedSomething; } @@ -610,22 +629,22 @@ bool Model::convexHullContains(glm::vec3 point) { if (!isActive()) { return false; } - + // extents is the entity relative, scaled, centered extents of the entity glm::vec3 position = _translation; glm::mat4 rotation = glm::mat4_cast(_rotation); glm::mat4 translation = glm::translate(position); glm::mat4 modelToWorldMatrix = translation * rotation; glm::mat4 worldToModelMatrix = glm::inverse(modelToWorldMatrix); - + Extents modelExtents = getMeshExtents(); // NOTE: unrotated - + glm::vec3 dimensions = modelExtents.maximum - modelExtents.minimum; glm::vec3 corner = -(dimensions * _registrationPoint); AABox modelFrameBox(corner, dimensions); - + glm::vec3 modelFramePoint = glm::vec3(worldToModelMatrix * glm::vec4(point, 1.0f)); - + // we can use the AABox's contains() by mapping our point into the model frame // and testing there. if (modelFrameBox.contains(modelFramePoint)){ @@ -633,7 +652,7 @@ bool Model::convexHullContains(glm::vec3 point) { if (!_calculatedMeshTrianglesValid) { recalculateMeshBoxes(true); } - + // If we are inside the models box, then consider the submeshes... int subMeshIndex = 0; foreach(const AABox& subMeshBox, _calculatedMeshBoxes) { @@ -647,7 +666,7 @@ bool Model::convexHullContains(glm::vec3 point) { insideMesh = false; break; } - + } if (insideMesh) { // It's inside this mesh, return true. @@ -686,7 +705,7 @@ void Model::recalculateMeshPartOffsets() { // Any script might trigger findRayIntersectionAgainstSubMeshes (and maybe convexHullContains), so these // can occur multiple times. In addition, rendering does it's own ray picking in order to decide which // entity-scripts to call. I think it would be best to do the picking once-per-frame (in cpu, or gpu if possible) -// and then the calls use the most recent such result. +// and then the calls use the most recent such result. void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { PROFILE_RANGE(__FUNCTION__); bool calculatedMeshTrianglesNeeded = pickAgainstTriangles && !_calculatedMeshTrianglesValid; @@ -731,7 +750,7 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); glm::vec3 mv3 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i3], 1.0f)); - + // track the mesh parts in model space if (!atLeastOnePointInBounds) { thisPartBounds.setBox(mv0, 0.0f); @@ -747,18 +766,18 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { glm::vec3 v1 = calculateScaledOffsetPoint(mv1); glm::vec3 v2 = calculateScaledOffsetPoint(mv2); glm::vec3 v3 = calculateScaledOffsetPoint(mv3); - + // Sam's recommended triangle slices Triangle tri1 = { v0, v1, v3 }; Triangle tri2 = { v1, v2, v3 }; - + // NOTE: Random guy on the internet's recommended triangle slices //Triangle tri1 = { v0, v1, v2 }; //Triangle tri2 = { v2, v3, v0 }; - + thisMeshTriangles.push_back(tri1); thisMeshTriangles.push_back(tri2); - + } } @@ -820,7 +839,7 @@ void Model::renderSetup(RenderArgs* args) { _dilatedTextures.append(dilated); } } - + if (!_meshGroupsKnown && isLoaded()) { segregateMeshGroups(); } @@ -833,7 +852,7 @@ public: transparent(transparent), model(model), url(model->getURL()), meshIndex(meshIndex), partIndex(partIndex) { } typedef render::Payload Payload; typedef Payload::DataPointer Pointer; - + bool transparent; Model* model; QUrl url; @@ -842,14 +861,14 @@ public: }; namespace render { - template <> const ItemKey payloadGetKey(const MeshPartPayload::Pointer& payload) { + template <> const ItemKey payloadGetKey(const MeshPartPayload::Pointer& payload) { if (!payload->model->isVisible()) { return ItemKey::Builder().withInvisible().build(); } return payload->transparent ? ItemKey::Builder::transparentShape() : ItemKey::Builder::opaqueShape(); } - - template <> const Item::Bound payloadGetBound(const MeshPartPayload::Pointer& payload) { + + template <> const Item::Bound payloadGetBound(const MeshPartPayload::Pointer& payload) { if (payload) { return payload->model->getPartBounds(payload->meshIndex, payload->partIndex); } @@ -903,7 +922,7 @@ bool Model::addToScene(std::shared_ptr scene, render::PendingChan _renderItems.insert(item, renderPayload); somethingAdded = true; } - + _readyWhenAdded = readyToAddToScene(); return somethingAdded; @@ -935,7 +954,7 @@ bool Model::addToScene(std::shared_ptr scene, render::PendingChan _renderItems.insert(item, renderPayload); somethingAdded = true; } - + _readyWhenAdded = readyToAddToScene(); return somethingAdded; @@ -957,7 +976,7 @@ void Model::renderDebugMeshBoxes() { _debugMeshBoxesID = DependencyManager::get()->allocateID(); } QVector points; - + glm::vec3 brn = box.getCorner(); glm::vec3 bln = brn + glm::vec3(box.getDimensions().x, 0, 0); glm::vec3 brf = brn + glm::vec3(0, 0, box.getDimensions().z); @@ -991,12 +1010,12 @@ void Model::renderDebugMeshBoxes() { { 1.0f, 1.0f, 0.0f, 1.0f }, // yellow { 0.0f, 1.0f, 1.0f, 1.0f }, // cyan { 1.0f, 1.0f, 1.0f, 1.0f }, // white - { 0.0f, 0.5f, 0.0f, 1.0f }, - { 0.0f, 0.0f, 0.5f, 1.0f }, - { 0.5f, 0.0f, 0.5f, 1.0f }, - { 0.5f, 0.5f, 0.0f, 1.0f }, + { 0.0f, 0.5f, 0.0f, 1.0f }, + { 0.0f, 0.0f, 0.5f, 1.0f }, + { 0.5f, 0.0f, 0.5f, 1.0f }, + { 0.5f, 0.5f, 0.0f, 1.0f }, { 0.0f, 0.5f, 0.5f, 1.0f } }; - + DependencyManager::get()->updateVertices(_debugMeshBoxesID, points, color[colorNdx]); DependencyManager::get()->renderVertices(gpu::LINES, _debugMeshBoxesID); colorNdx++; @@ -1031,7 +1050,7 @@ Extents Model::getUnscaledMeshExtents() const { if (!isActive()) { return Extents(); } - + const Extents& extents = _geometry->getFBXGeometry().meshExtents; // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which @@ -1039,7 +1058,7 @@ Extents Model::getUnscaledMeshExtents() const { glm::vec3 minimum = glm::vec3(_geometry->getFBXGeometry().offset * glm::vec4(extents.minimum, 1.0f)); glm::vec3 maximum = glm::vec3(_geometry->getFBXGeometry().offset * glm::vec4(extents.maximum, 1.0f)); Extents scaledExtents = { minimum, maximum }; - + return scaledExtents; } @@ -1048,12 +1067,12 @@ Extents Model::calculateScaledOffsetExtents(const Extents& extents) const { glm::vec3 minimum = glm::vec3(_geometry->getFBXGeometry().offset * glm::vec4(extents.minimum, 1.0f)); glm::vec3 maximum = glm::vec3(_geometry->getFBXGeometry().offset * glm::vec4(extents.maximum, 1.0f)); - Extents scaledOffsetExtents = { ((minimum + _offset) * _scale), + Extents scaledOffsetExtents = { ((minimum + _offset) * _scale), ((maximum + _offset) * _scale) }; Extents rotatedExtents = scaledOffsetExtents.getRotated(_rotation); - Extents translatedExtents = { rotatedExtents.minimum + _translation, + Extents translatedExtents = { rotatedExtents.minimum + _translation, rotatedExtents.maximum + _translation }; return translatedExtents; @@ -1075,43 +1094,63 @@ glm::vec3 Model::calculateScaledOffsetPoint(const glm::vec3& point) const { bool Model::getJointState(int index, glm::quat& rotation) const { - if (index == -1 || index >= _jointStates.size()) { - return false; + if (_rig) { + return _rig->getJointState(index, rotation); + } else { + if (index == -1 || index >= _jointStates.size()) { + return false; + } + const JointState& state = _jointStates.at(index); + rotation = state.getRotationInConstrainedFrame(); + return !state.rotationIsDefault(rotation); } - const JointState& state = _jointStates.at(index); - rotation = state.getRotationInConstrainedFrame(); - return !state.rotationIsDefault(rotation); } bool Model::getVisibleJointState(int index, glm::quat& rotation) const { - if (index == -1 || index >= _jointStates.size()) { - return false; + if (_rig) { + return _rig->getVisibleJointState(index, rotation); + } else { + if (index == -1 || index >= _jointStates.size()) { + return false; + } + const JointState& state = _jointStates.at(index); + rotation = state.getVisibleRotationInConstrainedFrame(); + return !state.rotationIsDefault(rotation); } - const JointState& state = _jointStates.at(index); - rotation = state.getVisibleRotationInConstrainedFrame(); - return !state.rotationIsDefault(rotation); } void Model::clearJointState(int index) { - if (index != -1 && index < _jointStates.size()) { - JointState& state = _jointStates[index]; - state.setRotationInConstrainedFrame(glm::quat(), 0.0f); + if (_rig) { + _rig->clearJointState(index); + } else { + if (index != -1 && index < _jointStates.size()) { + JointState& state = _jointStates[index]; + state.setRotationInConstrainedFrame(glm::quat(), 0.0f); + } } } void Model::clearJointAnimationPriority(int index) { - if (index != -1 && index < _jointStates.size()) { - _jointStates[index]._animationPriority = 0.0f; + if (_rig) { + _rig->clearJointAnimationPriority(index); + } else { + if (index != -1 && index < _jointStates.size()) { + _jointStates[index]._animationPriority = 0.0f; + } } } void Model::setJointState(int index, bool valid, const glm::quat& rotation, float priority) { - if (index != -1 && index < _jointStates.size()) { - JointState& state = _jointStates[index]; - if (valid) { - state.setRotationInConstrainedFrame(rotation, priority); - } else { - state.restoreRotation(1.0f, priority); + if (_rig) { + _rig->setJointState(index, valid, rotation, priority); + } else { + if (index != -1 && index < _jointStates.size()) { + JointState& state = _jointStates[index]; + if (valid) { + state.setRotationInConstrainedFrame(rotation, priority); + } else { + state.restoreRotation(1.0f, priority); + } } } } @@ -1138,7 +1177,7 @@ void Model::setURL(const QUrl& url, const QUrl& fallback, bool retainCurrent, bo onInvalidate(); - // if so instructed, keep the current geometry until the new one is loaded + // if so instructed, keep the current geometry until the new one is loaded _nextGeometry = DependencyManager::get()->getGeometry(url, fallback, delayLoad); _nextLODHysteresis = NetworkGeometry::NO_HYSTERESIS; if (!retainCurrent || !isActive() || (_nextGeometry && _nextGeometry->isLoaded())) { @@ -1148,14 +1187,14 @@ void Model::setURL(const QUrl& url, const QUrl& fallback, bool retainCurrent, bo void Model::geometryRefreshed() { QObject* sender = QObject::sender(); - + if (sender == _geometry) { _readyWhenAdded = false; // reset out render items. _needsReload = true; invalidCalculatedMeshBoxes(); - + onInvalidate(); - + // if so instructed, keep the current geometry until the new one is loaded _nextGeometry = DependencyManager::get()->getGeometry(_url); _nextLODHysteresis = NetworkGeometry::NO_HYSTERESIS; @@ -1175,7 +1214,7 @@ const QSharedPointer Model::getCollisionGeometry(bool delayLoad if (_collisionGeometry && _collisionGeometry->isLoaded()) { return _collisionGeometry; } - + return QSharedPointer(); } @@ -1187,62 +1226,82 @@ void Model::setCollisionModelURL(const QUrl& url) { _collisionGeometry = DependencyManager::get()->getGeometry(url, QUrl(), true); } + +bool Model::getJointStateAtIndex(int jointIndex, JointState& jointState) const { + if (_rig) { + return _rig->getJointStateAtIndex(jointIndex, jointState); + } else { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + return false; + } + jointState = _jointStates[jointIndex]; + return true; + } +} + bool Model::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position) const { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + JointState jointState; + if (!getJointStateAtIndex(jointIndex, jointState)) { return false; } // position is in world-frame - position = _translation + _rotation * _jointStates[jointIndex].getPosition(); + position = _translation + _rotation * jointState.getPosition(); return true; } bool Model::getJointPosition(int jointIndex, glm::vec3& position) const { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + JointState jointState; + if (!getJointStateAtIndex(jointIndex, jointState)) { return false; } // position is in model-frame - position = extractTranslation(_jointStates[jointIndex].getTransform()); + position = extractTranslation(jointState.getTransform()); return true; } bool Model::getJointRotationInWorldFrame(int jointIndex, glm::quat& rotation) const { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + JointState jointState; + if (!getJointStateAtIndex(jointIndex, jointState)) { return false; } - rotation = _rotation * _jointStates[jointIndex].getRotation(); + rotation = _rotation * jointState.getRotation(); return true; } bool Model::getJointRotation(int jointIndex, glm::quat& rotation) const { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + JointState jointState; + if (!getJointStateAtIndex(jointIndex, jointState)) { return false; } - rotation = _jointStates[jointIndex].getRotation(); + rotation = jointState.getRotation(); return true; } bool Model::getJointCombinedRotation(int jointIndex, glm::quat& rotation) const { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + JointState jointState; + if (!getJointStateAtIndex(jointIndex, jointState)) { return false; } - rotation = _rotation * _jointStates[jointIndex].getRotation(); + rotation = _rotation * jointState.getRotation(); return true; } bool Model::getVisibleJointPositionInWorldFrame(int jointIndex, glm::vec3& position) const { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + JointState jointState; + if (!getJointStateAtIndex(jointIndex, jointState)) { return false; } // position is in world-frame - position = _translation + _rotation * _jointStates[jointIndex].getVisiblePosition(); + position = _translation + _rotation * jointState.getVisiblePosition(); return true; } bool Model::getVisibleJointRotationInWorldFrame(int jointIndex, glm::quat& rotation) const { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + JointState jointState; + if (!getJointStateAtIndex(jointIndex, jointState)) { return false; } - rotation = _rotation * _jointStates[jointIndex].getVisibleRotation(); + rotation = _rotation * jointState.getVisibleRotation(); return true; } @@ -1270,11 +1329,11 @@ public: Blender(Model* model, int blendNumber, const QWeakPointer& geometry, const QVector& meshes, const QVector& blendshapeCoefficients); - + virtual void run(); private: - + QPointer _model; int _blendNumber; QWeakPointer _geometry; @@ -1348,10 +1407,10 @@ void Model::setScaleToFit(bool scaleToFit, float largestDimension, bool forceRes } return; } - + if (forceRescale || _scaleToFit != scaleToFit || glm::length(_scaleToFitDimensions) != largestDimension) { _scaleToFit = scaleToFit; - + // we only need to do this work if we're "turning on" scale to fit. if (scaleToFit) { Extents modelMeshExtents = getUnscaledMeshExtents(); @@ -1372,7 +1431,7 @@ void Model::scaleToFit() { // we didn't yet have an active mesh. We can only enter this scaleToFit() in this state // if we now do have an active mesh, so we take this opportunity to actually determine // the correct scale. - if (_scaleToFit && _scaleToFitDimensions.y == FAKE_DIMENSION_PLACEHOLDER + if (_scaleToFit && _scaleToFitDimensions.y == FAKE_DIMENSION_PLACEHOLDER && _scaleToFitDimensions.z == FAKE_DIMENSION_PLACEHOLDER) { setScaleToFit(_scaleToFit, _scaleToFitDimensions.x); } @@ -1407,7 +1466,7 @@ void Model::simulate(float deltaTime, bool fullUpdate) { PROFILE_RANGE(__FUNCTION__); fullUpdate = updateGeometry() || fullUpdate || (_scaleToFit && !_scaledToFit) || (_snapModelToRegistrationPoint && !_snappedToRegistrationPoint); - + if (isActive() && fullUpdate) { // NOTE: This is overly aggressive and we are invalidating the MeshBoxes when in fact they may not be invalid // they really only become invalid if something about the transform to world space has changed. This is @@ -1437,12 +1496,20 @@ void Model::updateClusterMatrices() { if (_showTrueJointTransforms) { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - state.clusterMatrices[j] = modelToWorld * _jointStates[cluster.jointIndex].getTransform() * cluster.inverseBindMatrix; + JointState jointState; + if (!getJointStateAtIndex(cluster.jointIndex, jointState)) { + return; + } + state.clusterMatrices[j] = modelToWorld * jointState.getTransform() * cluster.inverseBindMatrix; } } else { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - state.clusterMatrices[j] = modelToWorld * _jointStates[cluster.jointIndex].getVisibleTransform() * cluster.inverseBindMatrix; + JointState jointState; + if (!getJointStateAtIndex(cluster.jointIndex, jointState)) { + return; + } + state.clusterMatrices[j] = modelToWorld * jointState.getVisibleTransform() * cluster.inverseBindMatrix; } } } @@ -1450,21 +1517,26 @@ void Model::updateClusterMatrices() { void Model::simulateInternal(float deltaTime) { // update the world space transforms for all joints - + // update animations foreach (const AnimationHandlePointer& handle, _runningAnimations) { handle->simulate(deltaTime); } - for (int i = 0; i < _jointStates.size(); i++) { - updateJointState(i); - } - for (int i = 0; i < _jointStates.size(); i++) { - _jointStates[i].resetTransformChanged(); + if (_rig) { + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; + _rig->updateJointStates(parentTransform); + _rig->resetAllTransformsChanged(); + } else { + updateJointStates(); + for (int i = 0; i < _jointStates.size(); i++) { + _jointStates[i].resetTransformChanged(); + } } _shapesAreDirty = !_shapes.isEmpty(); - + const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 modelToWorld = glm::mat4_cast(_rotation); for (int i = 0; i < _meshStates.size(); i++) { @@ -1473,16 +1545,24 @@ void Model::simulateInternal(float deltaTime) { if (_showTrueJointTransforms) { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - state.clusterMatrices[j] = modelToWorld * _jointStates[cluster.jointIndex].getTransform() * cluster.inverseBindMatrix; + JointState jointState; + if (!getJointStateAtIndex(cluster.jointIndex, jointState)) { + return; + } + state.clusterMatrices[j] = modelToWorld * jointState.getTransform() * cluster.inverseBindMatrix; } } else { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - state.clusterMatrices[j] = modelToWorld * _jointStates[cluster.jointIndex].getVisibleTransform() * cluster.inverseBindMatrix; + JointState jointState; + if (!getJointStateAtIndex(cluster.jointIndex, jointState)) { + return; + } + state.clusterMatrices[j] = modelToWorld * jointState.getVisibleTransform() * cluster.inverseBindMatrix; } } } - + // post the blender if we're not currently waiting for one to finish if (geometry.hasBlendedMeshes() && _blendshapeCoefficients != _blendedBlendshapeCoefficients) { _blendedBlendshapeCoefficients = _blendshapeCoefficients; @@ -1490,10 +1570,18 @@ void Model::simulateInternal(float deltaTime) { } } +void Model::updateJointStates() { + assert(!_rig); + for (int i = 0; i < _jointStates.size(); i++) { + updateJointState(i); + } +} + void Model::updateJointState(int index) { + assert(!_rig); JointState& state = _jointStates[index]; const FBXJoint& joint = state.getFBXJoint(); - + // compute model transforms int parentIndex = joint.parentIndex; if (parentIndex == -1) { @@ -1514,14 +1602,38 @@ void Model::updateVisibleJointStates() { // no need to update visible transforms return; } - for (int i = 0; i < _jointStates.size(); i++) { - _jointStates[i].slaveVisibleTransform(); + if (_rig) { + _rig->updateVisibleJointStates(); + } else { + for (int i = 0; i < _jointStates.size(); i++) { + _jointStates[i].slaveVisibleTransform(); + } } } -bool Model::setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, - bool useRotation, int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, - float priority) { +glm::quat Model::setJointRotationInBindFrame(int jointIndex, const glm::quat& rotation, float priority) { + glm::quat endRotation; + if (jointIndex == -1 || _jointStates.isEmpty()) { + return endRotation; + } + JointState& state = _jointStates[jointIndex]; + state.setRotationInBindFrame(rotation, priority); + endRotation = state.getRotationInBindFrame(); + return endRotation; +} + + +bool Model::setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, bool useRotation, + int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, float priority) { + if (_rig) { + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; + bool result = _rig->setJointPosition(jointIndex, position, rotation, useRotation, lastFreeIndex, allIntermediatesFree, + alignment, priority, parentTransform); + _shapesAreDirty = !_shapes.isEmpty(); + return result; + } + if (jointIndex == -1 || _jointStates.isEmpty()) { return false; } @@ -1609,6 +1721,13 @@ bool Model::setJointPosition(int jointIndex, const glm::vec3& position, const gl void Model::inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::quat& targetRotation, float priority) { // NOTE: targetRotation is from bind- to model-frame + if (_rig) { + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + glm::mat4 topParentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; + _rig->inverseKinematics( endIndex, targetPosition, targetRotation, priority, topParentTransform); + _shapesAreDirty = !_shapes.isEmpty(); + return; + } if (endIndex == -1 || _jointStates.isEmpty()) { return; @@ -1718,17 +1837,20 @@ void Model::inverseKinematics(int endIndex, glm::vec3 targetPosition, // set final rotation of the end joint endState.setRotationInBindFrame(targetRotation, priority, true); - + _shapesAreDirty = !_shapes.isEmpty(); } bool Model::restoreJointPosition(int jointIndex, float fraction, float priority) { + if (_rig) { + return _rig->restoreJointPosition(jointIndex, fraction, priority); + } if (jointIndex == -1 || _jointStates.isEmpty()) { return false; } const FBXGeometry& geometry = _geometry->getFBXGeometry(); const QVector& freeLineage = geometry.joints.at(jointIndex).freeLineage; - + foreach (int index, freeLineage) { JointState& state = _jointStates[index]; state.restoreRotation(fraction, priority); @@ -1737,6 +1859,9 @@ bool Model::restoreJointPosition(int jointIndex, float fraction, float priority) } float Model::getLimbLength(int jointIndex) const { + if (_rig) { + return _rig->getLimbLength(jointIndex, _scale); + } if (jointIndex == -1 || _jointStates.isEmpty()) { return 0.0f; } @@ -1770,7 +1895,7 @@ void Model::setBlendedVertices(int blendNumber, const QWeakPointergetFBXGeometry(); + const FBXGeometry& fbxGeometry = _geometry->getFBXGeometry(); int index = 0; for (int i = 0; i < fbxGeometry.meshes.size(); i++) { const FBXMesh& mesh = fbxGeometry.meshes.at(i); @@ -1791,7 +1916,7 @@ void Model::setGeometry(const QSharedPointer& newGeometry) { if (_geometry == newGeometry) { return; } - + if (_geometry) { _geometry->disconnect(_geometry.data(), &Resource::onRefresh, this, &Model::geometryRefreshed); } @@ -1804,10 +1929,10 @@ void Model::applyNextGeometry() { deleteGeometry(); _dilatedTextures.clear(); _lodHysteresis = _nextLODHysteresis; - + // we retain a reference to the base geometry so that its reference count doesn't fall to zero setGeometry(_nextGeometry); - + _meshGroupsKnown = false; _readyWhenAdded = false; // in case any of our users are using scenes _needsReload = false; // we are loaded now! @@ -1818,9 +1943,12 @@ void Model::applyNextGeometry() { void Model::deleteGeometry() { _blendedVertexBuffers.clear(); _jointStates.clear(); + if (_rig) { + _rig->clearJointStates(); + } _meshStates.clear(); clearShapes(); - + for (QSet::iterator it = _animationHandles.begin(); it != _animationHandles.end(); ) { AnimationHandlePointer handle = it->lock(); if (handle) { @@ -1830,11 +1958,11 @@ void Model::deleteGeometry() { it = _animationHandles.erase(it); } } - + if (_geometry) { _geometry->clearLoadPriority(this); } - + _blendedBlendshapeCoefficients.clear(); } @@ -1848,9 +1976,9 @@ AABox Model::getPartBounds(int meshIndex, int partIndex) { return calculateScaledOffsetAABox(_geometry->getFBXGeometry().meshExtents); } } - + if (_geometry->getFBXGeometry().meshes.size() > meshIndex) { - + // FIX ME! - This is currently a hack because for some mesh parts our efforts to calculate the bounding // box of the mesh part fails. It seems to create boxes that are not consistent with where the // geometry actually renders. If instead we make all the parts share the bounds of the entire subMesh @@ -1875,7 +2003,7 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran if (!_readyWhenAdded) { return; // bail asap } - + // We need to make sure we have valid offsets calculated before we can render if (!_calculatedMeshPartOffsetValid) { _mutex.lock(); @@ -1900,13 +2028,13 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran // guard against partially loaded meshes if (meshIndex >= networkMeshes.size() || meshIndex >= geometry.meshes.size() || meshIndex >= _meshStates.size() ) { - return; + return; } const NetworkMesh& networkMesh = networkMeshes.at(meshIndex); const FBXMesh& mesh = geometry.meshes.at(meshIndex); const MeshState& state = _meshStates.at(meshIndex); - + bool translucentMesh = translucent; // networkMesh.getTranslucentPartCount(mesh) == networkMesh.parts.size(); bool hasTangents = !mesh.tangents.isEmpty(); bool hasSpecular = mesh.hasSpecularTexture(); @@ -1936,7 +2064,7 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran DependencyManager::get()->renderWireCube(batch, 1.0f, cubeColor); } #endif //def DEBUG_BOUNDING_PARTS - + if (wireframe) { translucentMesh = hasTangents = hasSpecular = hasLightmap = isSkinned = false; } @@ -1951,14 +2079,14 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran // if our index is ever out of range for either meshes or networkMeshes, then skip it, and set our _meshGroupsKnown // to false to rebuild out mesh groups. - + if (meshIndex < 0 || meshIndex >= networkMeshes.size() || meshIndex > geometry.meshes.size()) { _meshGroupsKnown = false; // regenerate these lists next time around. _readyWhenAdded = false; // in case any of our users are using scenes invalidCalculatedMeshBoxes(); // if we have to reload, we need to assume our mesh boxes are all invalid return; // FIXME! } - + batch.setIndexBuffer(gpu::UINT32, (networkMesh._indexBuffer), 0); int vertexCount = mesh.vertices.size(); if (vertexCount == 0) { @@ -1970,7 +2098,7 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran if (_transforms.empty()) { _transforms.push_back(Transform()); } - + if (isSkinned) { batch._glUniformMatrix4fv(locations->clusterMatrices, state.clusterMatrices.size(), false, (const float*)state.clusterMatrices.constData()); @@ -2010,7 +2138,7 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran qCDebug(renderutils) << "WARNING: material == nullptr!!!"; } #endif - + if (material != nullptr) { // apply material properties @@ -2052,12 +2180,12 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran batch._glUniformMatrix4fv(locations->texcoordMatrices, 2, false, (const float*) &texcoordTransform); } - if (!mesh.tangents.isEmpty()) { + if (!mesh.tangents.isEmpty()) { NetworkTexture* normalMap = networkPart.normalTexture.data(); batch.setResourceTexture(1, (!normalMap || !normalMap->isLoaded()) ? textureCache->getBlueTexture() : normalMap->getGPUTexture()); } - + if (locations->specularTextureUnit >= 0) { NetworkTexture* specularMap = networkPart.specularTexture.data(); batch.setResourceTexture(locations->specularTextureUnit, (!specularMap || !specularMap->isLoaded()) ? @@ -2075,18 +2203,18 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran float emissiveOffset = part.emissiveParams.x; float emissiveScale = part.emissiveParams.y; batch._glUniform2f(locations->emissiveParams, emissiveOffset, emissiveScale); - + NetworkTexture* emissiveMap = networkPart.emissiveTexture.data(); batch.setResourceTexture(locations->emissiveTextureUnit, (!emissiveMap || !emissiveMap->isLoaded()) ? textureCache->getGrayTexture() : emissiveMap->getGPUTexture()); } - + if (translucent && locations->lightBufferUnit >= 0) { DependencyManager::get()->setupTransparent(args, locations->lightBufferUnit); } } } - + qint64 offset; { // FIXME_STUTTER: We should n't have any lock here @@ -2123,7 +2251,7 @@ void Model::segregateMeshGroups() { qDebug() << "WARNING!!!! Mesh Sizes don't match! We will not segregate mesh groups yet."; return; } - + _transparentRenderItems.clear(); _opaqueRenderItems.clear(); @@ -2132,7 +2260,7 @@ void Model::segregateMeshGroups() { const NetworkMesh& networkMesh = networkMeshes.at(i); const FBXMesh& mesh = geometry.meshes.at(i); const MeshState& state = _meshStates.at(i); - + bool translucentMesh = networkMesh.getTranslucentPartCount(mesh) == networkMesh.parts.size(); bool hasTangents = !mesh.tangents.isEmpty(); @@ -2140,7 +2268,7 @@ void Model::segregateMeshGroups() { bool hasLightmap = mesh.hasEmissiveTexture(); bool isSkinned = state.clusterMatrices.size() > 1; bool wireframe = isWireframe(); - + if (wireframe) { translucentMesh = hasTangents = hasSpecular = hasLightmap = isSkinned = false; } @@ -2157,7 +2285,7 @@ void Model::segregateMeshGroups() { } } _meshGroupsKnown = true; -} +} void Model::pickPrograms(gpu::Batch& batch, RenderMode mode, bool translucent, float alphaThreshold, bool hasLightmap, bool hasTangents, bool hasSpecular, bool isSkinned, bool isWireframe, RenderArgs* args, @@ -2177,7 +2305,7 @@ void Model::pickPrograms(gpu::Batch& batch, RenderMode mode, bool translucent, f gpu::ShaderPointer program = (*pipeline).second._pipeline->getProgram(); locations = (*pipeline).second._locations.get(); - + // Setup the One pipeline batch.setPipeline((*pipeline).second._pipeline); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index f34690e570..95c191732a 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -262,6 +262,7 @@ protected: bool _showTrueJointTransforms; + bool getJointStateAtIndex(int jointIndex, JointState& jointState) const; QVector _jointStates; class MeshState { @@ -283,10 +284,13 @@ protected: void simulateInternal(float deltaTime); /// Updates the state of the joint at the specified index. + void updateJointStates(); virtual void updateJointState(int index); virtual void updateVisibleJointStates(); - + + glm::quat setJointRotationInBindFrame(int jointIndex, const glm::quat& rotation, float priority); + /// \param jointIndex index of joint in model structure /// \param position position of joint in model-frame /// \param rotation rotation of joint in model-frame @@ -525,6 +529,7 @@ private: bool _readyWhenAdded = false; bool _needsReload = true; +protected: RigPointer _rig; }; From 0d0f12164a0c529ebd2cee6e42d6319722b28c27 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 21 Jul 2015 16:57:02 +0200 Subject: [PATCH 039/242] planky, enabling properties button --- examples/example/games/planky.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/example/games/planky.js b/examples/example/games/planky.js index 6733749317..4181eb541e 100644 --- a/examples/example/games/planky.js +++ b/examples/example/games/planky.js @@ -32,7 +32,7 @@ const DEFAULT_BLOCK_YAW_OFFSET = 45; var editMode = false; -const BUTTON_DIMENSIONS = {width: 50, height: 50}; +const BUTTON_DIMENSIONS = {width: 49, height: 49}; const MAXIMUM_PERCENTAGE = 100.0; const NO_ANGLE = 0; const RIGHT_ANGLE = 90; @@ -326,10 +326,11 @@ button = toolBar.addTool({ cogButton = toolBar.addTool({ width: BUTTON_DIMENSIONS.width, height: BUTTON_DIMENSIONS.height, - imageURL: HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/cog.svg', + imageURL: "https://dl.dropboxusercontent.com/u/14997455/hifi/planky/cog.svg", //HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/cog.svg', + subImage: { x: 0, y: BUTTON_DIMENSIONS.height, width: BUTTON_DIMENSIONS.width, height: BUTTON_DIMENSIONS.height }, alpha: 0.8, visible: true -}); +}, true, false); Controller.mousePressEvent.connect(function(event) { var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); @@ -341,6 +342,7 @@ Controller.mousePressEvent.connect(function(event) { editMode = !editMode; plankyStack.refresh(); } else if (toolBar.clicked(clickedOverlay) === cogButton) { + toolBar.selectTool(cogButton, true); settingsWindow.webWindow.setVisible(true); } }); From e0634de4032c7fce30606cf411b5f35ebe935b7f Mon Sep 17 00:00:00 2001 From: Niraj Venkat Date: Tue, 21 Jul 2015 12:14:09 -0700 Subject: [PATCH 040/242] Turn on/off debug AO from menu item --- interface/src/Application.cpp | 2 ++ interface/src/Menu.h | 2 +- .../render-utils/src/AmbientOcclusionEffect.cpp | 14 ++++++++------ libraries/render-utils/src/RenderDeferredTask.cpp | 8 +++++++- libraries/render-utils/src/RenderDeferredTask.h | 5 +++++ libraries/render-utils/src/ambient_occlusion.slf | 5 +++-- libraries/render/src/render/Engine.h | 2 ++ 7 files changed, 28 insertions(+), 10 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d5ac230f0c..891caa8418 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3336,6 +3336,8 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se renderContext._drawItemStatus = sceneInterface->doEngineDisplayItemStatus(); + renderContext._occlusionStatus = Menu::getInstance()->isOptionChecked(MenuOption::AmbientOcclusion); + renderArgs->_shouldRender = LODManager::shouldRender; renderContext.args = renderArgs; diff --git a/interface/src/Menu.h b/interface/src/Menu.h index b6c2e47329..c8685fe714 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -134,7 +134,7 @@ namespace MenuOption { const QString AddressBar = "Show Address Bar"; const QString AlignForearmsWithWrists = "Align Forearms with Wrists"; const QString AlternateIK = "Alternate IK"; - const QString AmbientOcclusion = "Ambient Occlusion"; + const QString AmbientOcclusion = "Debug Ambient Occlusion"; const QString Animations = "Animations..."; const QString Atmosphere = "Atmosphere"; const QString Attachments = "Attachments..."; diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index a6646d8e4a..2b1113d61b 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -193,8 +193,10 @@ const gpu::PipelinePointer& AmbientOcclusion::getOcclusionPipeline() { gpu::Shader::makeProgram(*program, slotBindings); - //_drawItemBoundPosLoc = program->getUniforms().findLocation("inBoundPos"); - //_drawItemBoundDimLoc = program->getUniforms().findLocation("inBoundDim"); + _gScaleLoc = program->getUniforms().findLocation("g_scale"); + _gBiasLoc = program->getUniforms().findLocation("g_bias"); + _gSampleRadiusLoc = program->getUniforms().findLocation("g_sample_rad"); + _gIntensityLoc = program->getUniforms().findLocation("g_intensity"); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); @@ -345,7 +347,7 @@ void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, cons // Occlusion step getOcclusionPipeline(); batch.setResourceTexture(0, DependencyManager::get()->getPrimaryDepthTexture()); - batch.setResourceTexture(1, DependencyManager::get()->getPrimaryFramebuffer()->getRenderBuffer(0)); + batch.setResourceTexture(1, DependencyManager::get()->getPrimaryNormalTexture()); _occlusionBuffer->setRenderBuffer(0, _occlusionTexture); batch.setFramebuffer(_occlusionBuffer); @@ -380,12 +382,12 @@ void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, cons batch.setPipeline(getHBlurPipeline()); DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); - + // "Blend" step - batch.setResourceTexture(0, _hBlurTexture); + batch.setResourceTexture(0, _occlusionTexture); batch.setFramebuffer(DependencyManager::get()->getPrimaryFramebuffer()); - // bind the fourth gpu::Pipeline we need - for + // bind the fourth gpu::Pipeline we need - for blending the primary framefuffer with blurred occlusion texture batch.setPipeline(getBlendPipeline()); glm::vec2 bottomLeftSmall(0.5f, -1.0f); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 1ab8a4ce01..eea343972e 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -61,6 +61,10 @@ RenderDeferredTask::RenderDeferredTask() : Task() { _jobs.push_back(Job(new RenderDeferred::JobModel("RenderDeferred"))); _jobs.push_back(Job(new ResolveDeferred::JobModel("ResolveDeferred"))); _jobs.push_back(Job(new AmbientOcclusion::JobModel("AmbientOcclusion"))); + + _jobs.back().setEnabled(false); + _occlusionJobIndex = _jobs.size() - 1; + _jobs.push_back(Job(new FetchItems::JobModel("FetchTransparent", FetchItems( ItemFilter::Builder::transparentShape().withoutLayered(), @@ -79,7 +83,6 @@ RenderDeferredTask::RenderDeferredTask() : Task() { _jobs.back().setEnabled(false); _drawStatusJobIndex = _jobs.size() - 1; - //_jobs.push_back(Job(new AmbientOcclusion::JobModel("AmbientOcclusion"))); _jobs.push_back(Job(new DrawOverlay3D::JobModel("DrawOverlay3D"))); _jobs.push_back(Job(new ResetGLState::JobModel())); @@ -110,6 +113,9 @@ void RenderDeferredTask::run(const SceneContextPointer& sceneContext, const Rend // Make sure we turn the displayItemStatus on/off setDrawItemStatus(renderContext->_drawItemStatus); + // TODO: turn on/off AO through menu item + setOcclusionStatus(renderContext->_occlusionStatus); + renderContext->args->_context->syncCache(); for (auto job : _jobs) { diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 4040606c62..5c9764eb52 100755 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -75,6 +75,11 @@ public: void setDrawItemStatus(bool draw) { if (_drawStatusJobIndex >= 0) { _jobs[_drawStatusJobIndex].setEnabled(draw); } } bool doDrawItemStatus() const { if (_drawStatusJobIndex >= 0) { return _jobs[_drawStatusJobIndex].isEnabled(); } else { return false; } } + int _occlusionJobIndex = -1; + + void setOcclusionStatus(bool draw) { if (_occlusionJobIndex >= 0) { _jobs[_occlusionJobIndex].setEnabled(draw); } } + bool doOcclusionStatus() const { if (_occlusionJobIndex >= 0) { return _jobs[_occlusionJobIndex].isEnabled(); } else { return false; } } + virtual void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); diff --git a/libraries/render-utils/src/ambient_occlusion.slf b/libraries/render-utils/src/ambient_occlusion.slf index 342f4148d2..10b687ad1f 100644 --- a/libraries/render-utils/src/ambient_occlusion.slf +++ b/libraries/render-utils/src/ambient_occlusion.slf @@ -39,8 +39,9 @@ void main(void) { float f = 30.0; // the far plane float c = (2.0 * n) / (f + n - z * (f - n)); // convert to linear values - //gl_FragColor = vec4(c, c, c, 1.0); - gl_FragColor = mix(depthColor, normalColor, normalColor.a); + vec4 linearizedDepthColor = vec4(c, c, c, 1.0); + gl_FragColor = mix(linearizedDepthColor, normalColor, 0.5); + //gl_FragColor = linearizedDepthColor; //vec3 p = getPosition(i.uv); //vec3 n = getNormal(i.uv); diff --git a/libraries/render/src/render/Engine.h b/libraries/render/src/render/Engine.h index 1c600b13d6..5bfece96cc 100644 --- a/libraries/render/src/render/Engine.h +++ b/libraries/render/src/render/Engine.h @@ -51,6 +51,8 @@ public: bool _drawItemStatus = false; + bool _occlusionStatus = false; + RenderContext() {} }; typedef std::shared_ptr RenderContextPointer; From cd58dc11ceb92bf3a4c0e959a41f2ddaeb3a0f7c Mon Sep 17 00:00:00 2001 From: "Kevin M. Thomas" Date: Tue, 21 Jul 2015 18:10:13 -0400 Subject: [PATCH 041/242] Initial file placement into job. --- .../jsstreamplayerdomain-zone-entity.js | 33 +++ examples/example/jsstreamplayerdomain-zone.js | 203 ++++++++++++++++++ examples/html/jsstreamplayerdomain-zone.html | 42 ++++ 3 files changed, 278 insertions(+) create mode 100644 examples/example/entities/jsstreamplayerdomain-zone-entity.js create mode 100644 examples/example/jsstreamplayerdomain-zone.js create mode 100644 examples/html/jsstreamplayerdomain-zone.html diff --git a/examples/example/entities/jsstreamplayerdomain-zone-entity.js b/examples/example/entities/jsstreamplayerdomain-zone-entity.js new file mode 100644 index 0000000000..9a8cb8b8b4 --- /dev/null +++ b/examples/example/entities/jsstreamplayerdomain-zone-entity.js @@ -0,0 +1,33 @@ +// +// #20628: JS Stream Player Domain-Zone-Entity +// ******************************************** +// +// Created by Kevin M. Thomas and Thoys 07/20/15. +// Copyright 2015 High Fidelity, Inc. +// kevintown.net +// +// JavaScript for the High Fidelity interface that is an entity script to be placed in a chosen entity inside a domain-zone. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +// Function which exists inside of an entity which triggers as a user approches it. +(function() { + const SCRIPT_NAME = "https://dl.dropboxusercontent.com/u/17344741/jsstreamplayer/jsstreamplayerdomain-zone.js"; + function isScriptRunning(script) { + script = script.toLowerCase().trim(); + var runningScripts = ScriptDiscoveryService.getRunning(); + for (i in runningScripts) { + if (runningScripts[i].url.toLowerCase().trim() == script) { + return true; + } + } + return false; + }; + + if (!isScriptRunning(SCRIPT_NAME)) { + Script.load(SCRIPT_NAME); + } +}) \ No newline at end of file diff --git a/examples/example/jsstreamplayerdomain-zone.js b/examples/example/jsstreamplayerdomain-zone.js new file mode 100644 index 0000000000..fab5792dd7 --- /dev/null +++ b/examples/example/jsstreamplayerdomain-zone.js @@ -0,0 +1,203 @@ +// +// #20628: JS Stream Player Domain-Zone +// ************************************* +// +// Created by Kevin M. Thomas and Thoys 07/20/15. +// Copyright 2015 High Fidelity, Inc. +// kevintown.net +// +// JavaScript for the High Fidelity interface that creates a stream player with a UI for playing a domain-zone specificed stream URL in addition to play, stop and volume functionality which is resident only in the domain-zone. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +// Declare variables and set up new WebWindow. +var stream = "http://listen.radionomy.com/80sMixTape"; +var volume; +var streamWindow = new WebWindow('Stream', "https://dl.dropboxusercontent.com/u/17344741/jsstreamplayer/jsstreamplayerdomain-zone.html", 0, 0, false); +var visible = false; + +// Set up toggleStreamPlayButton overlay. +var toggleStreamPlayButton = Overlays.addOverlay("text", { + x: 122, + y: 310, + width: 38, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + visible: false, + text: " Play" +}); + +// Set up toggleStreamStopButton overlay. +var toggleStreamStopButton = Overlays.addOverlay("text", { + x: 166, + y: 310, + width: 40, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + visible: false, + text: " Stop" +}); + +// Set up increaseVolumeButton overlay. +var toggleIncreaseVolumeButton = Overlays.addOverlay("text", { + x: 211, + y: 310, + width: 18, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + visible: false, + text: " +" +}); + +// Set up decreaseVolumeButton overlay. +var toggleDecreaseVolumeButton = Overlays.addOverlay("text", { + x: 234, + y: 310, + width: 15, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + visible: false, + text: " -" +}); + +// Function to change JSON object stream. +function changeStream(stream) { + var streamJSON = { + action: "changeStream", + stream: stream + } + streamWindow.eventBridge.emitScriptEvent(JSON.stringify(streamJSON)); +} + +// Function to change JSON object volume. +function changeVolume(volume) { + var volumeJSON = { + action: "changeVolume", + volume: volume + } + streamWindow.eventBridge.emitScriptEvent(JSON.stringify(volumeJSON)); +} + +// Function that adds mousePressEvent functionality to connect UI to enter stream URL, play and stop stream. +function mousePressEvent(event) { + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleStreamPlayButton) { + changeStream(stream); + volume = 0.25; + changeVolume(volume); + } + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleStreamStopButton) { + changeStream(""); + } + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleIncreaseVolumeButton) { + volume += 0.25; + changeVolume(volume); + } + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleDecreaseVolumeButton) { + volume -= 0.25; + changeVolume(volume); + } +} + +// Function checking bool if in proper zone. +function isOurZone(properties) { + return properties.type == "Zone" && properties.name == "Zone2"; +} + +// Function checking if avatar is in zone. +function isInZone() { + var entities = Entities.findEntities(MyAvatar.position, 10000); + + for (var i in entities) { + var properties = Entities.getEntityProperties(entities[i]); + + if (isOurZone(properties)) { + var minX = properties.position.x - (properties.dimensions.x / 2); + var maxX = properties.position.x + (properties.dimensions.x / 2); + var minY = properties.position.y - (properties.dimensions.y / 2); + var maxY = properties.position.y + (properties.dimensions.y / 2); + var minZ = properties.position.z - (properties.dimensions.z / 2); + var maxZ = properties.position.z + (properties.dimensions.z / 2); + + if (MyAvatar.position.x >= minX && MyAvatar.position.x <= maxX && MyAvatar.position.y >= minY && MyAvatar.position.y <= maxY && MyAvatar.position.z >= minZ && MyAvatar.position.z <= maxZ) { + return true; + } + } + } + return false; +} + +// Function to toggle visibile the overlay. +function toggleVisible(newVisibility) { + if (newVisibility != visible) { + visible = newVisibility; + Overlays.editOverlay(toggleStreamPlayButton, {visible: visible}); + Overlays.editOverlay(toggleStreamStopButton, {visible: visible}); + Overlays.editOverlay(toggleIncreaseVolumeButton, {visible: visible}); + Overlays.editOverlay(toggleDecreaseVolumeButton, {visible: visible}); + } +} + +// Function to check if avatar is in proper domain. +Window.domainChanged.connect(function() { + if (Window.location.hostname != "Music" && Window.location.hostname != "LiveMusic") { + Script.stop(); + } +}); + +// Function to check if avatar is within zone. +Entities.enterEntity.connect(function(entityID) { + print("Entered..." + JSON.stringify(entityID)); + var properties = Entities.getEntityProperties(entityID); + if (isOurZone(properties)) { + print("Entering Zone!"); + toggleVisible(true); + } +}) + +// Function to check if avatar is leaving zone. +Entities.leaveEntity.connect(function(entityID) { + print("Left..." + JSON.stringify(entityID)); + var properties = Entities.getEntityProperties(entityID); + if (isOurZone(properties)) { + print("Leaving Zone!"); + toggleVisible(false); + changeStream(""); + } +}) + +// Function to delete overlays upon exit. +function onScriptEnding() { + Overlays.deleteOverlay(toggleStreamPlayButton); + Overlays.deleteOverlay(toggleStreamStopButton); + Overlays.deleteOverlay(toggleIncreaseVolumeButton); + Overlays.deleteOverlay(toggleDecreaseVolumeButton); + changeStream(""); + streamWindow.deleteLater(); +} + +// Function call to ensure that if you log in to the zone visibility is true. +if (isInZone()) { + toggleVisible(true); +} + +// Connect mouse and hide WebWindow. +Controller.mousePressEvent.connect(mousePressEvent); +streamWindow.setVisible(false); + +// Call function upon ending script. +Script.scriptEnding.connect(onScriptEnding); \ No newline at end of file diff --git a/examples/html/jsstreamplayerdomain-zone.html b/examples/html/jsstreamplayerdomain-zone.html new file mode 100644 index 0000000000..28b2202591 --- /dev/null +++ b/examples/html/jsstreamplayerdomain-zone.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 75fa789b79591b25d6240479d74c671ba2eb0b2d Mon Sep 17 00:00:00 2001 From: "Kevin M. Thomas" Date: Tue, 21 Jul 2015 18:13:26 -0400 Subject: [PATCH 042/242] Changeable zone var declaration. --- examples/example/jsstreamplayerdomain-zone.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/example/jsstreamplayerdomain-zone.js b/examples/example/jsstreamplayerdomain-zone.js index fab5792dd7..543a95b839 100644 --- a/examples/example/jsstreamplayerdomain-zone.js +++ b/examples/example/jsstreamplayerdomain-zone.js @@ -14,6 +14,7 @@ // Declare variables and set up new WebWindow. +var zone = "Zone2"; var stream = "http://listen.radionomy.com/80sMixTape"; var volume; var streamWindow = new WebWindow('Stream', "https://dl.dropboxusercontent.com/u/17344741/jsstreamplayer/jsstreamplayerdomain-zone.html", 0, 0, false); @@ -115,7 +116,7 @@ function mousePressEvent(event) { // Function checking bool if in proper zone. function isOurZone(properties) { - return properties.type == "Zone" && properties.name == "Zone2"; + return properties.type == "Zone" && properties.name == zone; } // Function checking if avatar is in zone. From 9ac4e2f6877f9bd56112b40d1fc39064427c43a8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 21 Jul 2015 18:22:27 -0700 Subject: [PATCH 043/242] Render spheres over avatar eyes if they're looking at me --- interface/src/avatar/Avatar.cpp | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 3a55e73fa6..f9a1185e24 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -448,36 +448,35 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition, boo } } - // Stack indicator spheres - float indicatorOffset = 0.0f; - if (!_displayName.isEmpty() && _displayNameAlpha != 0.0f) { - const float DISPLAY_NAME_INDICATOR_OFFSET = 0.22f; - indicatorOffset = DISPLAY_NAME_INDICATOR_OFFSET; - } - const float INDICATOR_RADIUS = 0.03f; - const float INDICATOR_INDICATOR_OFFSET = 3.0f * INDICATOR_RADIUS; - // If this is the avatar being looked at, render a little ball above their head if (_isLookAtTarget && Menu::getInstance()->isOptionChecked(MenuOption::RenderFocusIndicator)) { + const float INDICATOR_OFFSET = 0.22f; + const float INDICATOR_RADIUS = 0.03f; const glm::vec4 LOOK_AT_INDICATOR_COLOR = { 0.8f, 0.0f, 0.0f, 0.75f }; - glm::vec3 position = glm::vec3(_position.x, getDisplayNamePosition().y + indicatorOffset, _position.z); + glm::vec3 position = glm::vec3(_position.x, getDisplayNamePosition().y + INDICATOR_OFFSET, _position.z); Transform transform; transform.setTranslation(position); batch.setModelTransform(transform); DependencyManager::get()->renderSolidSphere(batch, INDICATOR_RADIUS, 15, 15, LOOK_AT_INDICATOR_COLOR); - indicatorOffset += INDICATOR_INDICATOR_OFFSET; } - // If the avatar is looking at me, render an indication that they area + // If the avatar is looking at me, indicate that they are if (getHead()->getIsLookingAtMe() && Menu::getInstance()->isOptionChecked(MenuOption::ShowWhosLookingAtMe)) { - const glm::vec4 LOOKING_AT_ME_COLOR = { 0.8f, 0.65f, 0.0f, 0.1f }; - glm::vec3 position = glm::vec3(_position.x, getDisplayNamePosition().y + indicatorOffset, _position.z); + const glm::vec3 LOOKING_AT_ME_COLOR = { 1.0f, 1.0f, 1.0f }; + float alpha = 1.0f; + float radius = 0.035f; Transform transform; + glm::vec3 position = getHead()->getLeftEyePosition(); transform.setTranslation(position); batch.setModelTransform(transform); - DependencyManager::get()->renderSolidSphere(batch, INDICATOR_RADIUS, - 15, 15, LOOKING_AT_ME_COLOR); + DependencyManager::get()->renderSolidSphere(batch, radius, 15, 15, + glm::vec4(LOOKING_AT_ME_COLOR, alpha)); + position = getHead()->getRightEyePosition(); + transform.setTranslation(position); + batch.setModelTransform(transform); + DependencyManager::get()->renderSolidSphere(batch, radius, 15, 15, + glm::vec4(LOOKING_AT_ME_COLOR, alpha)); } // quick check before falling into the code below: From bed266dfe993469cb6252fccf90a6e48e00767a8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 21 Jul 2015 18:23:50 -0700 Subject: [PATCH 044/242] Fade looking-at-me eye spheres over half a second --- interface/src/avatar/Avatar.cpp | 31 ++++++++++++++++++------------- interface/src/avatar/Head.cpp | 4 ++++ interface/src/avatar/Head.h | 2 ++ 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index f9a1185e24..884540bf7c 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -464,19 +464,24 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition, boo // If the avatar is looking at me, indicate that they are if (getHead()->getIsLookingAtMe() && Menu::getInstance()->isOptionChecked(MenuOption::ShowWhosLookingAtMe)) { const glm::vec3 LOOKING_AT_ME_COLOR = { 1.0f, 1.0f, 1.0f }; - float alpha = 1.0f; - float radius = 0.035f; - Transform transform; - glm::vec3 position = getHead()->getLeftEyePosition(); - transform.setTranslation(position); - batch.setModelTransform(transform); - DependencyManager::get()->renderSolidSphere(batch, radius, 15, 15, - glm::vec4(LOOKING_AT_ME_COLOR, alpha)); - position = getHead()->getRightEyePosition(); - transform.setTranslation(position); - batch.setModelTransform(transform); - DependencyManager::get()->renderSolidSphere(batch, radius, 15, 15, - glm::vec4(LOOKING_AT_ME_COLOR, alpha)); + const float LOOKING_AT_ME_DURATION = 0.5f; // seconds + quint64 now = usecTimestampNow(); + float alpha = 1.0f - ((float)(usecTimestampNow() - getHead()->getIsLookingAtMeStarted())) + / (LOOKING_AT_ME_DURATION * (float)USECS_PER_SECOND); + if (alpha > 0.0f) { + float radius = 0.035f; + Transform transform; + glm::vec3 position = getHead()->getLeftEyePosition(); + transform.setTranslation(position); + batch.setModelTransform(transform); + DependencyManager::get()->renderSolidSphere(batch, radius, 15, 15, + glm::vec4(LOOKING_AT_ME_COLOR, alpha)); + position = getHead()->getRightEyePosition(); + transform.setTranslation(position); + batch.setModelTransform(transform); + DependencyManager::get()->renderSolidSphere(batch, radius, 15, 15, + glm::vec4(LOOKING_AT_ME_COLOR, alpha)); + } } // quick check before falling into the code below: diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index 43e68557ce..94a163e508 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -55,6 +55,7 @@ Head::Head(Avatar* owningAvatar) : _deltaLeanForward(0.0f), _isCameraMoving(false), _isLookingAtMe(false), + _isLookingAtMeStarted(0), _faceModel(this), _leftEyeLookAtID(DependencyManager::get()->allocateID()), _rightEyeLookAtID(DependencyManager::get()->allocateID()) @@ -323,6 +324,9 @@ glm::vec3 Head::getCorrectedLookAtPosition() { } void Head::setCorrectedLookAtPosition(glm::vec3 correctedLookAtPosition) { + if (!_isLookingAtMe) { + _isLookingAtMeStarted = usecTimestampNow(); + } _isLookingAtMe = true; _correctedLookAtPosition = correctedLookAtPosition; } diff --git a/interface/src/avatar/Head.h b/interface/src/avatar/Head.h index d7a8462693..58f6f14b0a 100644 --- a/interface/src/avatar/Head.h +++ b/interface/src/avatar/Head.h @@ -53,6 +53,7 @@ public: glm::vec3 getCorrectedLookAtPosition(); void clearCorrectedLookAtPosition() { _isLookingAtMe = false; } bool getIsLookingAtMe() { return _isLookingAtMe; } + quint64 getIsLookingAtMeStarted() { return _isLookingAtMeStarted; } float getScale() const { return _scale; } glm::vec3 getPosition() const { return _position; } @@ -139,6 +140,7 @@ private: bool _isCameraMoving; bool _isLookingAtMe; + quint64 _isLookingAtMeStarted; FaceModel _faceModel; glm::vec3 _correctedLookAtPosition; From 55683e0cd589d353b7367a0aaaace23203d42229 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 21 Jul 2015 18:24:47 -0700 Subject: [PATCH 045/242] Size looking-at-me eye spheres per avatar model dimensions --- interface/src/avatar/Avatar.cpp | 14 +++++++++----- libraries/fbx/src/FBXReader.cpp | 9 ++++++++- libraries/fbx/src/FBXReader.h | 5 ++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 884540bf7c..ba0e0126ba 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -469,18 +469,22 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition, boo float alpha = 1.0f - ((float)(usecTimestampNow() - getHead()->getIsLookingAtMeStarted())) / (LOOKING_AT_ME_DURATION * (float)USECS_PER_SECOND); if (alpha > 0.0f) { - float radius = 0.035f; + const float RADIUS_INCREMENT = 0.005f; Transform transform; + glm::vec3 position = getHead()->getLeftEyePosition(); transform.setTranslation(position); batch.setModelTransform(transform); - DependencyManager::get()->renderSolidSphere(batch, radius, 15, 15, - glm::vec4(LOOKING_AT_ME_COLOR, alpha)); + DependencyManager::get()->renderSolidSphere(batch, + getHead()->getFaceModel().getGeometry()->getFBXGeometry().leftEyeSize * _scale / 2.0f + RADIUS_INCREMENT, + 15, 15, glm::vec4(LOOKING_AT_ME_COLOR, alpha)); + position = getHead()->getRightEyePosition(); transform.setTranslation(position); batch.setModelTransform(transform); - DependencyManager::get()->renderSolidSphere(batch, radius, 15, 15, - glm::vec4(LOOKING_AT_ME_COLOR, alpha)); + DependencyManager::get()->renderSolidSphere(batch, + getHead()->getFaceModel().getGeometry()->getFBXGeometry().rightEyeSize * _scale / 2.0f + RADIUS_INCREMENT, + 15, 15, glm::vec4(LOOKING_AT_ME_COLOR, alpha)); } } diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 4d7bff4df0..466b3de3ee 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -2616,10 +2616,17 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, buildModelMesh(extracted); # endif + if (extracted.mesh.isEye) { + if (maxJointIndex == geometry.leftEyeJointIndex) { + geometry.leftEyeSize = extracted.mesh.meshExtents.largestDimension() * offsetScale; + } else { + geometry.rightEyeSize = extracted.mesh.meshExtents.largestDimension() * offsetScale; + } + } + geometry.meshes.append(extracted.mesh); int meshIndex = geometry.meshes.size() - 1; meshIDsToMeshIndices.insert(it.key(), meshIndex); - } // now that all joints have been scanned, compute a collision shape for each joint diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 200cd4a121..3b3d90eb05 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -232,7 +232,10 @@ public: int rightHandJointIndex = -1; int leftToeJointIndex = -1; int rightToeJointIndex = -1; - + + float leftEyeSize = 0.0f; // Maximum mesh extents dimension + float rightEyeSize = 0.0f; + QVector humanIKJointIndices; glm::vec3 palmDirection; From 87dbbdb2e8a4c740f9c37b76cdd6fcba2c4983c5 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 21 Jul 2015 18:25:35 -0700 Subject: [PATCH 046/242] Set an initial alpha in anticipation of sphere alpha working --- interface/src/avatar/Avatar.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index ba0e0126ba..6aa70196bd 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -464,10 +464,12 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition, boo // If the avatar is looking at me, indicate that they are if (getHead()->getIsLookingAtMe() && Menu::getInstance()->isOptionChecked(MenuOption::ShowWhosLookingAtMe)) { const glm::vec3 LOOKING_AT_ME_COLOR = { 1.0f, 1.0f, 1.0f }; + const float LOOKING_AT_ME_ALPHA_START = 0.8f; const float LOOKING_AT_ME_DURATION = 0.5f; // seconds quint64 now = usecTimestampNow(); - float alpha = 1.0f - ((float)(usecTimestampNow() - getHead()->getIsLookingAtMeStarted())) - / (LOOKING_AT_ME_DURATION * (float)USECS_PER_SECOND); + float alpha = LOOKING_AT_ME_ALPHA_START + * (1.0f - ((float)(usecTimestampNow() - getHead()->getIsLookingAtMeStarted())) + / (LOOKING_AT_ME_DURATION * (float)USECS_PER_SECOND)); if (alpha > 0.0f) { const float RADIUS_INCREMENT = 0.005f; Transform transform; From de116c4e3b35b26f8014641074d84d6f2796d431 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 22 Jul 2015 10:21:13 +0200 Subject: [PATCH 047/242] editMode on and off switch by clicking the COG, proper removal of planks when rows/columns removed --- examples/example/games/planky.js | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/examples/example/games/planky.js b/examples/example/games/planky.js index 4181eb541e..8abc697353 100644 --- a/examples/example/games/planky.js +++ b/examples/example/games/planky.js @@ -203,20 +203,29 @@ PlankyStack = function() { dimensions: {x: 5, y: 21, z: 5}, position: Vec3.sum(_this.basePosition, {y: -(_this.options.baseDimension.y / 2)}), lineWidth: 7, - color: {red: 214, green: 91, blue: 67}, - linePoints: [{x: 0, y: 0, z: 0}, {x: 0, y: 10, z: 0}] + color: {red: 20, green: 20, blue: 20}, + linePoints: [{x: 0, y: 0, z: 0}, {x: 0, y: 10, z: 0}], + visible: editMode })); return; } + _this.editLines.forEach(function(line) { + Entities.editEntity(line, {visible: editMode}); + }) }; var trimDimension = function(dimension, maxIndex) { + var removingPlanks = []; _this.planks.forEach(function(plank, index, object) { if (plank[dimension] > maxIndex) { - Entities.deleteEntity(plank.entity); - object.splice(index, 1); + removingPlanks.push(index); } }); + removingPlanks.reverse(); + for (var i = 0; i < removingPlanks.length; i++) { + Entities.deleteEntity(_this.planks[removingPlanks[i]].entity); + _this.planks.splice(removingPlanks[i], 1); + } }; var createOrUpdate = function(layer, row) { @@ -237,7 +246,6 @@ PlankyStack = function() { dimensions: Vec3.sum(_this.options.blockSize, {x: 0, y: -((_this.options.blockSize.y * (_this.options.blockHeightVariation / MAXIMUM_PERCENTAGE)) * Math.random()), z: 0}), position: Vec3.sum(_this.basePosition, localTransform), rotation: Quat.multiply(layerRotation, _this.offsetRot), - //collisionsWillMove: false,//!editMode,//false,//!editMode, damping: _this.options.dampingFactor, restitution: _this.options.restitution, friction: _this.options.friction, @@ -282,7 +290,6 @@ PlankyStack = function() { if (!editMode) { _this.planks.forEach(function(plank, index, object) { Entities.editEntity(plank.entity, {ignoreForCollisions: false, collisionsWillMove: true}); - // Entities.editEntity(plank.entity, {collisionsWillMove: true}); }); } }; @@ -326,7 +333,7 @@ button = toolBar.addTool({ cogButton = toolBar.addTool({ width: BUTTON_DIMENSIONS.width, height: BUTTON_DIMENSIONS.height, - imageURL: "https://dl.dropboxusercontent.com/u/14997455/hifi/planky/cog.svg", //HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/cog.svg', + imageURL: HIFI_PUBLIC_BUCKET + 'marketplace/hificontent/Games/blocks/cog.svg', subImage: { x: 0, y: BUTTON_DIMENSIONS.height, width: BUTTON_DIMENSIONS.width, height: BUTTON_DIMENSIONS.height }, alpha: 0.8, visible: true @@ -339,11 +346,14 @@ Controller.mousePressEvent.connect(function(event) { plankyStack.rez(); return; } - editMode = !editMode; plankyStack.refresh(); } else if (toolBar.clicked(clickedOverlay) === cogButton) { - toolBar.selectTool(cogButton, true); - settingsWindow.webWindow.setVisible(true); + editMode = !editMode; + toolBar.selectTool(cogButton, editMode); + settingsWindow.webWindow.setVisible(editMode); + if(plankyStack.planks.length) { + plankyStack.refresh(); + } } }); From e44cba500dc90ec00af72269be3a6df90dc06502 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 22 Jul 2015 09:52:49 -0700 Subject: [PATCH 048/242] Guard against head model not being available yet --- interface/src/avatar/Avatar.cpp | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 6aa70196bd..e30c2ac24d 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -471,22 +471,26 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition, boo * (1.0f - ((float)(usecTimestampNow() - getHead()->getIsLookingAtMeStarted())) / (LOOKING_AT_ME_DURATION * (float)USECS_PER_SECOND)); if (alpha > 0.0f) { - const float RADIUS_INCREMENT = 0.005f; - Transform transform; + QSharedPointer geometry = getHead()->getFaceModel().getGeometry(); + if (geometry) { + const float RADIUS_INCREMENT = 0.005f; + Transform transform; - glm::vec3 position = getHead()->getLeftEyePosition(); - transform.setTranslation(position); - batch.setModelTransform(transform); - DependencyManager::get()->renderSolidSphere(batch, - getHead()->getFaceModel().getGeometry()->getFBXGeometry().leftEyeSize * _scale / 2.0f + RADIUS_INCREMENT, - 15, 15, glm::vec4(LOOKING_AT_ME_COLOR, alpha)); + glm::vec3 position = getHead()->getLeftEyePosition(); + transform.setTranslation(position); + batch.setModelTransform(transform); + DependencyManager::get()->renderSolidSphere(batch, + geometry->getFBXGeometry().leftEyeSize * _scale / 2.0f + RADIUS_INCREMENT, 15, 15, + glm::vec4(LOOKING_AT_ME_COLOR, alpha)); - position = getHead()->getRightEyePosition(); - transform.setTranslation(position); - batch.setModelTransform(transform); - DependencyManager::get()->renderSolidSphere(batch, - getHead()->getFaceModel().getGeometry()->getFBXGeometry().rightEyeSize * _scale / 2.0f + RADIUS_INCREMENT, - 15, 15, glm::vec4(LOOKING_AT_ME_COLOR, alpha)); + position = getHead()->getRightEyePosition(); + transform.setTranslation(position); + batch.setModelTransform(transform); + DependencyManager::get()->renderSolidSphere(batch, + geometry->getFBXGeometry().rightEyeSize * _scale / 2.0f + RADIUS_INCREMENT, 15, 15, + glm::vec4(LOOKING_AT_ME_COLOR, alpha)); + + } } } From 9d33cfa9650eabbe07e54bcf027a8145ac083f7b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 22 Jul 2015 09:53:37 -0700 Subject: [PATCH 049/242] Allow for look-at positions being not quite up to date as avatars move So that looking-at-me indicators don't flicker on when they shouldn't. --- interface/src/avatar/Avatar.cpp | 4 ++-- interface/src/avatar/Head.cpp | 17 +++++++++++++---- interface/src/avatar/Head.h | 9 +++++---- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index e30c2ac24d..1fee720ad4 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -462,13 +462,13 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition, boo } // If the avatar is looking at me, indicate that they are - if (getHead()->getIsLookingAtMe() && Menu::getInstance()->isOptionChecked(MenuOption::ShowWhosLookingAtMe)) { + if (getHead()->isLookingAtMe() && Menu::getInstance()->isOptionChecked(MenuOption::ShowWhosLookingAtMe)) { const glm::vec3 LOOKING_AT_ME_COLOR = { 1.0f, 1.0f, 1.0f }; const float LOOKING_AT_ME_ALPHA_START = 0.8f; const float LOOKING_AT_ME_DURATION = 0.5f; // seconds quint64 now = usecTimestampNow(); float alpha = LOOKING_AT_ME_ALPHA_START - * (1.0f - ((float)(usecTimestampNow() - getHead()->getIsLookingAtMeStarted())) + * (1.0f - ((float)(usecTimestampNow() - getHead()->getLookingAtMeStarted())) / (LOOKING_AT_ME_DURATION * (float)USECS_PER_SECOND)); if (alpha > 0.0f) { QSharedPointer geometry = getHead()->getFaceModel().getGeometry(); diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index 94a163e508..d4ef793ab3 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -55,7 +55,8 @@ Head::Head(Avatar* owningAvatar) : _deltaLeanForward(0.0f), _isCameraMoving(false), _isLookingAtMe(false), - _isLookingAtMeStarted(0), + _lookingAtMeStarted(0), + _wasLastLookingAtMe(0), _faceModel(this), _leftEyeLookAtID(DependencyManager::get()->allocateID()), _rightEyeLookAtID(DependencyManager::get()->allocateID()) @@ -316,7 +317,7 @@ glm::quat Head::getFinalOrientationInLocalFrame() const { } glm::vec3 Head::getCorrectedLookAtPosition() { - if (_isLookingAtMe) { + if (isLookingAtMe()) { return _correctedLookAtPosition; } else { return getLookAtPosition(); @@ -324,13 +325,21 @@ glm::vec3 Head::getCorrectedLookAtPosition() { } void Head::setCorrectedLookAtPosition(glm::vec3 correctedLookAtPosition) { - if (!_isLookingAtMe) { - _isLookingAtMeStarted = usecTimestampNow(); + if (!isLookingAtMe()) { + _lookingAtMeStarted = usecTimestampNow(); } _isLookingAtMe = true; + _wasLastLookingAtMe = usecTimestampNow(); _correctedLookAtPosition = correctedLookAtPosition; } +bool Head::isLookingAtMe() { + // Allow for outages such as may be encountered during avatar movement + quint64 now = usecTimestampNow(); + const quint64 LOOKING_AT_ME_GAP_ALLOWED = 1000000; // microseconds + return _isLookingAtMe || (now - _wasLastLookingAtMe) < LOOKING_AT_ME_GAP_ALLOWED; +} + glm::quat Head::getCameraOrientation() const { // NOTE: Head::getCameraOrientation() is not used for orienting the camera "view" while in Oculus mode, so // you may wonder why this code is here. This method will be called while in Oculus mode to determine how diff --git a/interface/src/avatar/Head.h b/interface/src/avatar/Head.h index 58f6f14b0a..a8161d1c7b 100644 --- a/interface/src/avatar/Head.h +++ b/interface/src/avatar/Head.h @@ -52,9 +52,9 @@ public: void setCorrectedLookAtPosition(glm::vec3 correctedLookAtPosition); glm::vec3 getCorrectedLookAtPosition(); void clearCorrectedLookAtPosition() { _isLookingAtMe = false; } - bool getIsLookingAtMe() { return _isLookingAtMe; } - quint64 getIsLookingAtMeStarted() { return _isLookingAtMeStarted; } - + bool isLookingAtMe(); + quint64 getLookingAtMeStarted() { return _lookingAtMeStarted; } + float getScale() const { return _scale; } glm::vec3 getPosition() const { return _position; } const glm::vec3& getEyePosition() const { return _eyePosition; } @@ -140,7 +140,8 @@ private: bool _isCameraMoving; bool _isLookingAtMe; - quint64 _isLookingAtMeStarted; + quint64 _lookingAtMeStarted; + quint64 _wasLastLookingAtMe; FaceModel _faceModel; glm::vec3 _correctedLookAtPosition; From 3cac781cdca985a1a81891f0af40bab61debba4f Mon Sep 17 00:00:00 2001 From: Bing Shearer Date: Wed, 22 Jul 2015 10:47:27 -0700 Subject: [PATCH 050/242] Fixed indentation and number formats, as well as streamlining calculation of leaf velocity to make it more readable. --- examples/leaves.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/examples/leaves.js b/examples/leaves.js index 347371ff5b..e611eb7de6 100755 --- a/examples/leaves.js +++ b/examples/leaves.js @@ -175,7 +175,7 @@ var leafSquall = function (properties) { } Script.setInterval(function () { nearbyEntities = Entities.findEntities(squallOrigin, squallRadius); - newLeafMovement() + newLeafMovement() }, 100); function newLeafMovement() { //new additions to leaf code. Operates at 10 Hz or every 100 ms @@ -193,20 +193,18 @@ var leafSquall = function (properties) { currentLeaf = nearbyEntities[i]; var leafHeight = entityProperties.position.y; if (complexMovement && leafHeight > floorHeight || complexMovement && floorHeight == null) { //actual new movement code; - var currentLeafVelocity = entityProperties.velocity, - currentLeafRot = entityProperties.rotation, + var leafCurrentVel = entityProperties.velocity, + leafCurrentRot = entityProperties.rotation, yVec = { x: 0, y: 1, z: 0 }, - leafYinWFVec = Vec3.multiplyQbyV(currentLeafRot, yVec), + leafYinWFVec = Vec3.multiplyQbyV(leafCurrentRot, yVec), leafLocalHorVec = Vec3.cross(leafYinWFVec, yVec), leafMostDownVec = Vec3.cross(leafYinWFVec, leafLocalHorVec), - leafDesiredVel = Vec3.multiply(leafMostDownVec, windFactor); + leafDesiredVel = Vec3.multiply(leafMostDownVec, windFactor), + leafVelDelt = Vec3.subtract(leafDesiredVel, leafCurrentVel), + leafNewVel = Vec3.sum(leafCurrentVel, Vec3.multiply(leafVelDelt, windFactor)); Entities.editEntity(currentLeaf, { angularVelocity: randomRotationSpeed, - velocity: { - x: (leafDesiredVel.x - currentLeafVelocity.x) * windFactor + currentLeafVelocity.x, - y: (leafDesiredVel.y - currentLeafVelocity.y) * windFactor + currentLeafVelocity.y, - z: (leafDesiredVel.z - currentLeafVelocity.z) * windFactor + currentLeafVelocity.z - } + velocity: leafNewVel }) } else if (leafHeight <= floorHeight) { @@ -274,8 +272,8 @@ var leafSquall1 = new leafSquall({ origin: { x: 3071.5, y: 2170, z: 6765.3 }, radius: 100, leavesPerMinute: 30, - leafSize: { x: 0.3, y: 0.00, z: .3}, - leafFallSpeed: .4, + leafSize: { x: 0.3, y: 0.00, z: 0.3}, + leafFallSpeed: 0.4, leafLifetime: 100, leafSpinMax: 30, debug: false, @@ -283,7 +281,7 @@ var leafSquall1 = new leafSquall({ leafDeleteOnTearDown: true, complexMovement: true, floorHeight: 2143.5, - windFactor: .5, + windFactor: 0.5, leafDeleteOnGround: false }); From a161f527c4071e2cd3bbfda23d0bf8ae57a05ea8 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 22 Jul 2015 11:41:24 -0700 Subject: [PATCH 051/242] Comments documenting direction and questions. --- libraries/animation/src/Rig.h | 8 ++++++++ tests/rig/src/RigTests.cpp | 30 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index c3e1edd415..99a83a2a8d 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -10,6 +10,14 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* TBD: + - What is responsibilities of Animation/AnimationPointer/AnimationCache/AnimationDetails? Is there common/copied code (e.g., ScriptableAvatar::update)? + - How do attachments interact with the physics of the attached entity? E.g., do hand joints need to reflect held object physics? + - Is there any current need (i.e., for initial campatability) to have multiple animations per role (e.g., idle) with the system choosing randomly? + + - Distribute some doc from here to the right files if it turns out to be correct: + - AnimationDetails is a script-useable copy of animation state, analogous to EntityItemProperties, but without anything equivalent to editEntity. + */ #ifndef __hifi__Rig__ #define __hifi__Rig__ diff --git a/tests/rig/src/RigTests.cpp b/tests/rig/src/RigTests.cpp index c32e136fd1..ca07cda844 100644 --- a/tests/rig/src/RigTests.cpp +++ b/tests/rig/src/RigTests.cpp @@ -8,6 +8,36 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* FIXME/TBD: + + The following lower level functionality might be separated out into a separate class, covered by a separate test case class: + - With no input, initial pose is standing, arms at side + - Some single animation produces correct results at a given keyframe time. + - Some single animation produces correct results at a given interpolated time. + - Blend between two animations, started at separate times, produces correct result at a given interpolated time. + - Head orientation can be overridden to produce change that doesn't come from the playing animation. + - Hand position/orientation can be overridden to produce change that doesn't come from the playing animation. + - Hand position/orientation can be overrridden to produce elbow change that doesn't come from the playing animation. + - Respect scaling? (e.g., so that MyAvatar.increase/decreaseSize can alter rig, such that anti-scating and footfalls-on-stairs works) + + Higher level functionality: + - start/stopAnimation adds the animation to that which is playing, blending/fading as needed. + - thrust causes walk role animation to be used. + - turning causes turn role animation to be used. (two tests, correctly symmetric left & right) + - walk/turn do not skate (footfalls match over-ground velocity) + - (Later?) walk up stairs / hills have proper footfall for terrain + - absence of above causes return to idle role animation to be used + - (later?) The lower-level head/hand placements respect previous state. E.g., actual hand movement can be slower than requested. + - (later) The lower-level head/hand placements can move whole skeleton. E.g., turning head past a limit may turn whole body. Reaching up can move shoulders and hips. + + Backward-compatability operations. We should think of this behavior as deprecated: + - clearJointData return to standing. TBD: presumably with idle and all other animations NOT playing, until explicitly reenabled with a new TBD method? + - setJointData applies the given data. Same TBD. + These can be defined true or false, but the tests document the behavior and tells us if something's changed: + - An external change to the original skeleton IS/ISN'T seen by the rig. + - An external change to the original skeleton's head orientation IS/ISN'T seen by the rig. + - An external change to the original skeleton's hand orientiation IS/ISN'T seen by the rig. + */ #include #include "RigTests.h" From 8f0893ba2153b5a10f22aa0cff4cd573de3063cf Mon Sep 17 00:00:00 2001 From: Marcel Verhagen Date: Wed, 22 Jul 2015 22:34:45 +0200 Subject: [PATCH 052/242] Added fileOnUrl to check if a texture exist at the location. It return the correct filename of where the texture lives. Added the url of the fix file to extractFBXGeometry and readFBX and updated the calls to readFBX to include the url of the fix file. So it now does not break existing content. Found a second place in the FBXReader.cpp where the RelativeFileName stripped out the dir location. --- interface/src/ModelPackager.cpp | 2 +- libraries/animation/src/AnimationCache.cpp | 2 +- libraries/fbx/src/FBXReader.cpp | 31 +++++++++++++++----- libraries/fbx/src/FBXReader.h | 4 +-- libraries/render-utils/src/GeometryCache.cpp | 2 +- tools/vhacd-util/src/VHACDUtil.cpp | 2 +- 6 files changed, 29 insertions(+), 14 deletions(-) diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index 2864738f9c..09d572c31d 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -106,7 +106,7 @@ bool ModelPackager::loadModel() { } qCDebug(interfaceapp) << "Reading FBX file : " << _fbxInfo.filePath(); QByteArray fbxContents = fbx.readAll(); - _geometry = readFBX(fbxContents, QVariantHash()); + _geometry = readFBX(fbxContents, QVariantHash(), _fbxInfo.filePath()); // make sure we have some basic mappings populateBasicMapping(_mapping, _fbxInfo.filePath(), _geometry); diff --git a/libraries/animation/src/AnimationCache.cpp b/libraries/animation/src/AnimationCache.cpp index 6c02ccbd2b..0aa5c34544 100644 --- a/libraries/animation/src/AnimationCache.cpp +++ b/libraries/animation/src/AnimationCache.cpp @@ -65,7 +65,7 @@ void AnimationReader::run() { QSharedPointer animation = _animation.toStrongRef(); if (!animation.isNull()) { QMetaObject::invokeMethod(animation.data(), "setGeometry", - Q_ARG(const FBXGeometry&, readFBX(_reply->readAll(), QVariantHash()))); + Q_ARG(const FBXGeometry&, readFBX(_reply->readAll(), QVariantHash(), _reply->property("url").toString()))); } _reply->deleteLater(); } diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 24cbc983ed..554d56ee5a 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -1454,9 +1455,23 @@ void buildModelMesh(ExtractedMesh& extracted) { } #endif // USE_MODEL_MESH +QByteArray fileOnUrl(const QByteArray filenameString,const QString url) { + QString path = QFileInfo(url).path(); + QByteArray filename = filenameString; + QFileInfo checkFile(path + "/" + filename.replace('\\', '/')); + //check if the file exists at the RelativeFileName + if (checkFile.exists() && checkFile.isFile()) { + filename = filename.replace('\\', '/'); + } else { + // there is not texture at the filename. Assume it is in the root dir. + filename = filename.mid(qMax(filename.lastIndexOf('\\'), filename.lastIndexOf('/')) + 1); + } + filename = filename.mid(qMax(filename.lastIndexOf('\\'), filename.lastIndexOf('/')) + 1); + filename = filename.replace('\\', '/'); + return filename; +} - -FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, bool loadLightmaps, float lightmapLevel) { +FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, QString url, bool loadLightmaps, float lightmapLevel) { QHash meshes; QHash modelIDsToNames; QHash meshIDsToMeshIndices; @@ -1782,7 +1797,7 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, foreach (const FBXNode& subobject, object.children) { if (subobject.name == "RelativeFilename") { QByteArray filename = subobject.properties.at(0).toByteArray(); - filename = filename.replace('\\', '/'); + filename = fileOnUrl(filename, url); textureFilenames.insert(getID(object.properties), filename); } else if (subobject.name == "TextureName") { // trim the name from the timestamp @@ -1856,7 +1871,7 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, foreach (const FBXNode& subobject, object.children) { if (subobject.name == "RelativeFilename") { filename = subobject.properties.at(0).toByteArray(); - filename = filename.mid(qMax(filename.lastIndexOf('\\'), filename.lastIndexOf('/')) + 1); + filename = fileOnUrl(filename, url); } else if (subobject.name == "Content" && !subobject.properties.isEmpty()) { content = subobject.properties.at(0).toByteArray(); @@ -2709,12 +2724,12 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, return geometry; } -FBXGeometry readFBX(const QByteArray& model, const QVariantHash& mapping, bool loadLightmaps, float lightmapLevel) { +FBXGeometry readFBX(const QByteArray& model, const QVariantHash& mapping, const QString url, bool loadLightmaps, float lightmapLevel) { QBuffer buffer(const_cast(&model)); buffer.open(QIODevice::ReadOnly); - return readFBX(&buffer, mapping, loadLightmaps, lightmapLevel); + return readFBX(&buffer, mapping, url, loadLightmaps, lightmapLevel); } -FBXGeometry readFBX(QIODevice* device, const QVariantHash& mapping, bool loadLightmaps, float lightmapLevel) { - return extractFBXGeometry(parseFBX(device), mapping, loadLightmaps, lightmapLevel); +FBXGeometry readFBX(QIODevice* device, const QVariantHash& mapping, const QString url, bool loadLightmaps, float lightmapLevel) { + return extractFBXGeometry(parseFBX(device), mapping, url, loadLightmaps, lightmapLevel); } diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 200cd4a121..cf86c304ac 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -268,10 +268,10 @@ Q_DECLARE_METATYPE(FBXGeometry) /// Reads FBX geometry from the supplied model and mapping data. /// \exception QString if an error occurs in parsing -FBXGeometry readFBX(const QByteArray& model, const QVariantHash& mapping, bool loadLightmaps = true, float lightmapLevel = 1.0f); +FBXGeometry readFBX(const QByteArray& model, const QVariantHash& mapping, const QString url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); /// Reads FBX geometry from the supplied model and mapping data. /// \exception QString if an error occurs in parsing -FBXGeometry readFBX(QIODevice* device, const QVariantHash& mapping, bool loadLightmaps = true, float lightmapLevel = 1.0f); +FBXGeometry readFBX(QIODevice* device, const QVariantHash& mapping, const QString url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); #endif // hifi_FBXReader_h diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 02e59bfa3a..3d32ba74ad 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -2185,7 +2185,7 @@ void GeometryReader::run() { } else if (_url.path().toLower().endsWith("palaceoforinthilian4.fbx")) { lightmapLevel = 3.5f; } - fbxgeo = readFBX(_reply, _mapping, grabLightmaps, lightmapLevel); + fbxgeo = readFBX(_reply, _mapping, _url.path(), grabLightmaps, lightmapLevel); } else if (_url.path().toLower().endsWith(".obj")) { fbxgeo = OBJReader().readOBJ(_reply, _mapping, &_url); } diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index f1ff0e9e4f..d775244b9c 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -38,7 +38,7 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { if (filename.toLower().endsWith(".obj")) { result = OBJReader().readOBJ(fbxContents, QVariantHash()); } else if (filename.toLower().endsWith(".fbx")) { - result = readFBX(fbxContents, QVariantHash()); + result = readFBX(fbxContents, QVariantHash(), filename); } else { qDebug() << "unknown file extension"; return false; From 7c8d52cbd15f3f5b2f052703aeeac1c8e9108466 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 22 Jul 2015 13:41:31 -0700 Subject: [PATCH 053/242] back out some changes to Model.cpp, change how rig pointer is delivered to model initilizer --- interface/src/avatar/Avatar.cpp | 4 +- interface/src/avatar/Avatar.h | 2 +- interface/src/avatar/AvatarManager.cpp | 4 +- interface/src/avatar/MyAvatar.cpp | 8 +- interface/src/avatar/MyAvatar.h | 2 +- interface/src/avatar/SkeletonModel.cpp | 9 +- interface/src/avatar/SkeletonModel.h | 2 +- libraries/animation/src/Rig.cpp | 15 +- libraries/animation/src/Rig.h | 4 +- libraries/render-utils/src/Model.cpp | 521 +++++++++---------------- libraries/render-utils/src/Model.h | 13 +- tools/vhacd-util/src/VHACDUtil.cpp | 2 + 12 files changed, 228 insertions(+), 358 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 3a55e73fa6..1d14a0f464 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -75,9 +75,9 @@ namespace render { } } -Avatar::Avatar() : +Avatar::Avatar(RigPointer rig) : AvatarData(), - _skeletonModel(this), + _skeletonModel(this, nullptr, rig), _skeletonOffset(0.0f), _bodyYawDelta(0.0f), _positionDeltaAccumulator(0.0f), diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index ae99317541..096af5c229 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -72,7 +72,7 @@ class Avatar : public AvatarData { Q_PROPERTY(glm::vec3 skeletonOffset READ getSkeletonOffset WRITE setSkeletonOffset) public: - Avatar(); + Avatar(RigPointer rig = nullptr); ~Avatar(); typedef render::Payload Payload; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index d5922366e9..730c77e9c3 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -65,7 +65,7 @@ AvatarManager::AvatarManager(QObject* parent) : { // register a meta type for the weak pointer we'll use for the owning avatar mixer for each avatar qRegisterMetaType >("NodeWeakPointer"); - _myAvatar = std::make_shared(); + _myAvatar = std::make_shared(std::make_shared()); auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket"); @@ -160,7 +160,7 @@ void AvatarManager::simulateAvatarFades(float deltaTime) { } AvatarSharedPointer AvatarManager::newSharedAvatar() { - return AvatarSharedPointer(std::make_shared()); + return AvatarSharedPointer(std::make_shared(std::make_shared())); } // virtual diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index c59f92f33f..b59d605752 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -78,8 +78,8 @@ const float MyAvatar::ZOOM_MIN = 0.5f; const float MyAvatar::ZOOM_MAX = 25.0f; const float MyAvatar::ZOOM_DEFAULT = 1.5f; -MyAvatar::MyAvatar() : - Avatar(), +MyAvatar::MyAvatar(RigPointer rig) : + Avatar(rig), _gravity(0.0f, 0.0f, 0.0f), _wasPushing(false), _isPushing(false), @@ -102,8 +102,8 @@ MyAvatar::MyAvatar() : _eyeContactTarget(LEFT_EYE), _realWorldFieldOfView("realWorldFieldOfView", DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), - _rig(new Rig()), - _firstPersonSkeletonModel(this, nullptr, _rig), + _rig(rig), + _firstPersonSkeletonModel(this, nullptr, rig), _prevShouldDrawHead(true) { _firstPersonSkeletonModel.setIsFirstPerson(true); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index d2c3ee9ab0..ba84ab0e96 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -36,7 +36,7 @@ class MyAvatar : public Avatar { //TODO: make gravity feature work Q_PROPERTY(glm::vec3 gravity READ getGravity WRITE setGravity) public: - MyAvatar(); + MyAvatar(RigPointer rig); ~MyAvatar(); QByteArray toByteArray(); diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 551d616732..c8e6f03d69 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -42,6 +42,7 @@ SkeletonModel::SkeletonModel(Avatar* owningAvatar, QObject* parent, RigPointer r _headClipDistance(DEFAULT_NEAR_CLIP), _isFirstPerson(false) { + assert(_rig); assert(_owningAvatar); _enableShapes = true; } @@ -269,7 +270,6 @@ void SkeletonModel::applyPalmData(int jointIndex, PalmData& palm) { } } - void SkeletonModel::updateJointState(int index) { if (index < 0 && index >= _jointStates.size()) { return; // bail @@ -281,10 +281,10 @@ void SkeletonModel::updateJointState(int index) { const FBXGeometry& geometry = _geometry->getFBXGeometry(); if (index == geometry.leanJointIndex) { maybeUpdateLeanRotation(parentState, state); - + } else if (index == geometry.neckJointIndex) { - maybeUpdateNeckRotation(parentState, joint, state); - + maybeUpdateNeckRotation(parentState, joint, state); + } else if (index == geometry.leftEyeJointIndex || index == geometry.rightEyeJointIndex) { maybeUpdateEyeRotation(parentState, joint, state); } @@ -297,7 +297,6 @@ void SkeletonModel::updateJointState(int index) { } } - void SkeletonModel::maybeUpdateLeanRotation(const JointState& parentState, JointState& state) { if (!_owningAvatar->isMyAvatar()) { return; diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index ae8ffe66a9..6d33b3da7b 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -161,7 +161,7 @@ private: void setHandPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation); bool getEyeModelPositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const; - + Avatar* _owningAvatar; CapsuleShape _boundingShape; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index a76c866140..5e322e420f 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -184,17 +184,28 @@ void Rig::resetAllTransformsChanged() { } } -glm::quat Rig::setJointRotationInBindFrame(int jointIndex, const glm::quat& rotation, float priority) { +glm::quat Rig::setJointRotationInBindFrame(int jointIndex, const glm::quat& rotation, float priority, bool constrain) { glm::quat endRotation; if (jointIndex == -1 || _jointStates.isEmpty()) { return endRotation; } JointState& state = _jointStates[jointIndex]; - state.setRotationInBindFrame(rotation, priority); + state.setRotationInBindFrame(rotation, priority, constrain); endRotation = state.getRotationInBindFrame(); return endRotation; } +glm::quat Rig::setJointRotationInConstrainedFrame(int jointIndex, glm::quat targetRotation, float priority, bool constrain) { + glm::quat endRotation; + if (jointIndex == -1 || _jointStates.isEmpty()) { + return endRotation; + } + JointState& state = _jointStates[jointIndex]; + state.setRotationInConstrainedFrame(targetRotation, priority, constrain); + endRotation = state.getRotationInConstrainedFrame(); + return endRotation; +} + void Rig::applyJointRotationDelta(int jointIndex, const glm::quat& delta, bool constrain, float priority) { if (jointIndex == -1 || _jointStates.isEmpty()) { return; diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 552601f44f..b1155cbb9d 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -54,7 +54,9 @@ public: void clearJointStates(); void setJointState(int index, bool valid, const glm::quat& rotation, float priority); void clearJointAnimationPriority(int index); - glm::quat setJointRotationInBindFrame(int jointIndex, const glm::quat& rotation, float priority); + glm::quat setJointRotationInBindFrame(int jointIndex, const glm::quat& rotation, float priority, bool constrain = false); + glm::quat setJointRotationInConstrainedFrame(int jointIndex, glm::quat targetRotation, + float priority, bool constrain = false); void applyJointRotationDelta(int jointIndex, const glm::quat& delta, bool constrain, float priority); QVector getJointStates() { return _jointStates; } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index df805e4d29..5a6908fc4a 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -83,12 +83,11 @@ Model::Model(QObject* parent, RigPointer rig) : _isWireframe(false), _renderCollisionHull(false), _rig(rig) { - // we may have been created in the network thread, but we live in the main thread if (_viewState) { moveToThread(_viewState->getMainThread()); } - + setSnapModelToRegistrationPoint(true, glm::vec3(0.5f)); } @@ -113,12 +112,14 @@ void Model::RenderPipelineLib::addRenderPipeline(Model::RenderKey key, gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vertexShader, pixelShader)); gpu::Shader::makeProgram(*program, slotBindings); - + + auto locations = std::make_shared(); initLocations(program, *locations); + auto state = std::make_shared(); - + // Backface on shadow if (key.isShadow()) { state->setCullMode(gpu::State::CULL_FRONT); @@ -133,35 +134,29 @@ void Model::RenderPipelineLib::addRenderPipeline(Model::RenderKey key, // Blend on transparent state->setBlendFunction(key.isTranslucent(), - gpu::State::ONE, gpu::State::BLEND_OP_ADD, - gpu::State::INV_SRC_ALPHA, // For transparent only, this keep the highlight intensity - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + gpu::State::ONE, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, // For transparent only, this keep the highlight intensity + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); // Good to go add the brand new pipeline auto pipeline = gpu::PipelinePointer(gpu::Pipeline::create(program, state)); insert(value_type(key.getRaw(), RenderPipeline(pipeline, locations))); - - + + if (!key.isWireFrame()) { - + RenderKey wireframeKey(key.getRaw() | RenderKey::IS_WIREFRAME); -<<<<<<< HEAD - gpu::StatePointer wireframeState = gpu::StatePointer(new gpu::State(state->getValues())); - -======= auto wireframeState = std::make_shared(state->getValues()); ->>>>>>> cf88bce0820df76a55473370db0fcb8c5fe6763d wireframeState->setFillMode(gpu::State::FILL_LINE); - + // create a new RenderPipeline with the same shader side and the mirrorState auto wireframePipeline = gpu::PipelinePointer(gpu::Pipeline::create(program, wireframeState)); insert(value_type(wireframeKey.getRaw(), RenderPipeline(wireframePipeline, locations))); } - + // If not a shadow pass, create the mirror version from the same state, just change the FrontFace if (!key.isShadow()) { - + RenderKey mirrorKey(key.getRaw() | RenderKey::IS_MIRROR); auto mirrorState = std::make_shared(state->getValues()); @@ -170,18 +165,13 @@ void Model::RenderPipelineLib::addRenderPipeline(Model::RenderKey key, // create a new RenderPipeline with the same shader side and the mirrorState auto mirrorPipeline = gpu::PipelinePointer(gpu::Pipeline::create(program, mirrorState)); insert(value_type(mirrorKey.getRaw(), RenderPipeline(mirrorPipeline, locations))); - + if (!key.isWireFrame()) { RenderKey wireframeKey(key.getRaw() | RenderKey::IS_MIRROR | RenderKey::IS_WIREFRAME); -<<<<<<< HEAD - gpu::StatePointer wireframeState = gpu::StatePointer(new gpu::State(state->getValues()));; - -======= auto wireframeState = std::make_shared(state->getValues()); ->>>>>>> cf88bce0820df76a55473370db0fcb8c5fe6763d wireframeState->setFillMode(gpu::State::FILL_LINE); - + // create a new RenderPipeline with the same shader side and the mirrorState auto wireframePipeline = gpu::PipelinePointer(gpu::Pipeline::create(program, wireframeState)); insert(value_type(wireframeKey.getRaw(), RenderPipeline(wireframePipeline, locations))); @@ -210,7 +200,7 @@ void Model::RenderPipelineLib::initLocations(gpu::ShaderPointer& program, Model: locations.clusterIndices = program->getInputs().findLocation("clusterIndices");; locations.clusterWeights = program->getInputs().findLocation("clusterWeights");; - + } @@ -240,12 +230,12 @@ void Model::setScaleInternal(const glm::vec3& scale) { } } -void Model::setOffset(const glm::vec3& offset) { - _offset = offset; - +void Model::setOffset(const glm::vec3& offset) { + _offset = offset; + // if someone manually sets our offset, then we are no longer snapped to center - _snapModelToRegistrationPoint = false; - _snappedToRegistrationPoint = false; + _snapModelToRegistrationPoint = false; + _snappedToRegistrationPoint = false; } QVector Model::createJointStates(const FBXGeometry& geometry) { @@ -262,24 +252,20 @@ QVector Model::createJointStates(const FBXGeometry& geometry) { }; void Model::initJointTransforms() { - if (_rig) { - _rig->initJointTransforms(_scale, _offset); - } else { - // compute model transforms - int numStates = _jointStates.size(); - for (int i = 0; i < numStates; ++i) { - JointState& state = _jointStates[i]; - const FBXJoint& joint = state.getFBXJoint(); - int parentIndex = joint.parentIndex; - if (parentIndex == -1) { - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - // NOTE: in practice geometry.offset has a non-unity scale (rather than a translation) - glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - state.initTransform(parentTransform); - } else { - const JointState& parentState = _jointStates.at(parentIndex); - state.initTransform(parentState.getTransform()); - } + // compute model transforms + int numStates = _jointStates.size(); + for (int i = 0; i < numStates; ++i) { + JointState& state = _jointStates[i]; + const FBXJoint& joint = state.getFBXJoint(); + int parentIndex = joint.parentIndex; + if (parentIndex == -1) { + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + // NOTE: in practice geometry.offset has a non-unity scale (rather than a translation) + glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; + state.initTransform(parentTransform); + } else { + const JointState& parentState = _jointStates.at(parentIndex); + state.initTransform(parentState.getTransform()); } } } @@ -309,7 +295,7 @@ void Model::init() { auto modelLightmapNormalSpecularMapPixel = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(model_lightmap_normal_specular_map_frag))); // Fill the renderPipelineLib - + _renderPipelineLib.addRenderPipeline( RenderKey(0), modelVertex, modelPixel); @@ -326,7 +312,7 @@ void Model::init() { RenderKey(RenderKey::HAS_TANGENTS | RenderKey::HAS_SPECULAR), modelNormalMapVertex, modelNormalSpecularMapPixel); - + _renderPipelineLib.addRenderPipeline( RenderKey(RenderKey::IS_TRANSLUCENT), modelVertex, modelTranslucentPixel); @@ -334,7 +320,7 @@ void Model::init() { _renderPipelineLib.addRenderPipeline( RenderKey(RenderKey::IS_TRANSLUCENT | RenderKey::HAS_LIGHTMAP), modelVertex, modelTranslucentPixel); - + _renderPipelineLib.addRenderPipeline( RenderKey(RenderKey::HAS_TANGENTS | RenderKey::IS_TRANSLUCENT), modelNormalMapVertex, modelTranslucentPixel); @@ -410,18 +396,14 @@ void Model::init() { } void Model::reset() { - if (_rig) { - _rig->resetJoints(); - } else { - if (_jointStates.isEmpty()) { - return; - } - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - for (int i = 0; i < _jointStates.size(); i++) { - _jointStates[i].setRotationInConstrainedFrame(geometry.joints.at(i).rotation, 0.0f); - } + if (_jointStates.isEmpty()) { + return; } - + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + for (int i = 0; i < _jointStates.size(); i++) { + _jointStates[i].setRotationInConstrainedFrame(geometry.joints.at(i).rotation, 0.0f); + } + _meshGroupsKnown = false; _readyWhenAdded = false; // in case any of our users are using scenes invalidCalculatedMeshBoxes(); // if we have to reload, we need to assume our mesh boxes are all invalid @@ -446,8 +428,6 @@ bool Model::updateGeometry() { return false; } - bool jointStatesEmpty = _rig ? _rig->jointStatesEmpty() : _jointStates.isEmpty(); - QSharedPointer geometry = _geometry->getLODOrFallback(_lodDistance, _lodHysteresis); if (_geometry != geometry) { @@ -456,7 +436,7 @@ bool Model::updateGeometry() { const FBXGeometry& newGeometry = geometry->getFBXGeometry(); QVector newJointStates = createJointStates(newGeometry); - if (! jointStatesEmpty) { + if (! _jointStates.isEmpty()) { // copy the existing joint states const FBXGeometry& oldGeometry = _geometry->getFBXGeometry(); for (QHash::const_iterator it = oldGeometry.jointIndices.constBegin(); @@ -464,24 +444,20 @@ bool Model::updateGeometry() { int oldIndex = it.value() - 1; int newIndex = newGeometry.getJointIndex(it.key()); if (newIndex != -1) { - JointState jointState; - if (!getJointStateAtIndex(oldIndex, jointState)) { - return false; - } - newJointStates[newIndex].copyState(jointState); + newJointStates[newIndex].copyState(_jointStates[oldIndex]); } } - } + } deleteGeometry(); _dilatedTextures.clear(); setGeometry(geometry); - + _meshGroupsKnown = false; _readyWhenAdded = false; // in case any of our users are using scenes invalidCalculatedMeshBoxes(); // if we have to reload, we need to assume our mesh boxes are all invalid initJointStates(newJointStates); needToRebuild = true; - } else if (jointStatesEmpty) { + } else if (_jointStates.isEmpty()) { const FBXGeometry& fbxGeometry = geometry->getFBXGeometry(); if (fbxGeometry.joints.size() > 0) { initJointStates(createJointStates(fbxGeometry)); @@ -493,21 +469,15 @@ bool Model::updateGeometry() { } _geometry->setLoadPriority(this, -_lodDistance); _geometry->ensureLoading(); - + if (needToRebuild) { const FBXGeometry& fbxGeometry = geometry->getFBXGeometry(); foreach (const FBXMesh& mesh, fbxGeometry.meshes) { MeshState state; state.clusterMatrices.resize(mesh.clusters.size()); -<<<<<<< HEAD - _meshStates.append(state); - - gpu::BufferPointer buffer(new gpu::Buffer()); -======= _meshStates.append(state); auto buffer = std::make_shared(); ->>>>>>> cf88bce0820df76a55473370db0fcb8c5fe6763d if (!mesh.blendshapes.isEmpty()) { buffer->resize((mesh.vertices.size() + mesh.normals.size()) * sizeof(glm::vec3)); buffer->setSubData(0, mesh.vertices.size() * sizeof(glm::vec3), (gpu::Byte*) mesh.vertices.constData()); @@ -523,29 +493,25 @@ bool Model::updateGeometry() { // virtual void Model::initJointStates(QVector states) { - if (_rig) { - _boundingRadius = _rig->initJointStates(_scale, _offset, states); - } else { - _jointStates = states; - initJointTransforms(); + _jointStates = states; + initJointTransforms(); - int numStates = _jointStates.size(); - float radius = 0.0f; - for (int i = 0; i < numStates; ++i) { - float distance = glm::length(_jointStates[i].getPosition()); - if (distance > radius) { - radius = distance; - } - _jointStates[i].buildConstraint(); + int numStates = _jointStates.size(); + float radius = 0.0f; + for (int i = 0; i < numStates; ++i) { + float distance = glm::length(_jointStates[i].getPosition()); + if (distance > radius) { + radius = distance; } - for (int i = 0; i < _jointStates.size(); i++) { - _jointStates[i].slaveVisibleTransform(); - } - _boundingRadius = radius; + _jointStates[i].buildConstraint(); } + for (int i = 0; i < _jointStates.size(); i++) { + _jointStates[i].slaveVisibleTransform(); + } + _boundingRadius = radius; } -bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, +bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, QString& extraInfo, bool pickAgainstTriangles) { bool intersectedSomething = false; @@ -554,7 +520,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g if (!isActive()) { return intersectedSomething; } - + // extents is the entity relative, scaled, centered extents of the entity glm::vec3 position = _translation; glm::mat4 rotation = glm::mat4_cast(_rotation); @@ -563,7 +529,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g glm::mat4 worldToModelMatrix = glm::inverse(modelToWorldMatrix); Extents modelExtents = getMeshExtents(); // NOTE: unrotated - + glm::vec3 dimensions = modelExtents.maximum - modelExtents.minimum; glm::vec3 corner = -(dimensions * _registrationPoint); // since we're going to do the ray picking in the model frame of reference AABox modelFrameBox(corner, dimensions); @@ -602,7 +568,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g int t = 0; foreach (const Triangle& triangle, meshTriangles) { t++; - + float thisTriangleDistance; if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance)) { if (thisTriangleDistance < bestDistance) { @@ -621,7 +587,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g extraInfo = geometry.getModelNameOfMesh(subMeshIndex); } } - } + } subMeshIndex++; } _mutex.unlock(); @@ -629,7 +595,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g if (intersectedSomething) { distance = bestDistance; } - + return intersectedSomething; } @@ -641,22 +607,22 @@ bool Model::convexHullContains(glm::vec3 point) { if (!isActive()) { return false; } - + // extents is the entity relative, scaled, centered extents of the entity glm::vec3 position = _translation; glm::mat4 rotation = glm::mat4_cast(_rotation); glm::mat4 translation = glm::translate(position); glm::mat4 modelToWorldMatrix = translation * rotation; glm::mat4 worldToModelMatrix = glm::inverse(modelToWorldMatrix); - + Extents modelExtents = getMeshExtents(); // NOTE: unrotated - + glm::vec3 dimensions = modelExtents.maximum - modelExtents.minimum; glm::vec3 corner = -(dimensions * _registrationPoint); AABox modelFrameBox(corner, dimensions); - + glm::vec3 modelFramePoint = glm::vec3(worldToModelMatrix * glm::vec4(point, 1.0f)); - + // we can use the AABox's contains() by mapping our point into the model frame // and testing there. if (modelFrameBox.contains(modelFramePoint)){ @@ -664,7 +630,7 @@ bool Model::convexHullContains(glm::vec3 point) { if (!_calculatedMeshTrianglesValid) { recalculateMeshBoxes(true); } - + // If we are inside the models box, then consider the submeshes... int subMeshIndex = 0; foreach(const AABox& subMeshBox, _calculatedMeshBoxes) { @@ -678,7 +644,7 @@ bool Model::convexHullContains(glm::vec3 point) { insideMesh = false; break; } - + } if (insideMesh) { // It's inside this mesh, return true. @@ -717,7 +683,7 @@ void Model::recalculateMeshPartOffsets() { // Any script might trigger findRayIntersectionAgainstSubMeshes (and maybe convexHullContains), so these // can occur multiple times. In addition, rendering does it's own ray picking in order to decide which // entity-scripts to call. I think it would be best to do the picking once-per-frame (in cpu, or gpu if possible) -// and then the calls use the most recent such result. +// and then the calls use the most recent such result. void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { PROFILE_RANGE(__FUNCTION__); bool calculatedMeshTrianglesNeeded = pickAgainstTriangles && !_calculatedMeshTrianglesValid; @@ -762,7 +728,7 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); glm::vec3 mv3 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i3], 1.0f)); - + // track the mesh parts in model space if (!atLeastOnePointInBounds) { thisPartBounds.setBox(mv0, 0.0f); @@ -778,18 +744,18 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { glm::vec3 v1 = calculateScaledOffsetPoint(mv1); glm::vec3 v2 = calculateScaledOffsetPoint(mv2); glm::vec3 v3 = calculateScaledOffsetPoint(mv3); - + // Sam's recommended triangle slices Triangle tri1 = { v0, v1, v3 }; Triangle tri2 = { v1, v2, v3 }; - + // NOTE: Random guy on the internet's recommended triangle slices //Triangle tri1 = { v0, v1, v2 }; //Triangle tri2 = { v2, v3, v0 }; - + thisMeshTriangles.push_back(tri1); thisMeshTriangles.push_back(tri2); - + } } @@ -851,7 +817,7 @@ void Model::renderSetup(RenderArgs* args) { _dilatedTextures.append(dilated); } } - + if (!_meshGroupsKnown && isLoaded()) { segregateMeshGroups(); } @@ -864,7 +830,7 @@ public: transparent(transparent), model(model), url(model->getURL()), meshIndex(meshIndex), partIndex(partIndex) { } typedef render::Payload Payload; typedef Payload::DataPointer Pointer; - + bool transparent; Model* model; QUrl url; @@ -873,14 +839,14 @@ public: }; namespace render { - template <> const ItemKey payloadGetKey(const MeshPartPayload::Pointer& payload) { + template <> const ItemKey payloadGetKey(const MeshPartPayload::Pointer& payload) { if (!payload->model->isVisible()) { return ItemKey::Builder().withInvisible().build(); } return payload->transparent ? ItemKey::Builder::transparentShape() : ItemKey::Builder::opaqueShape(); } - - template <> const Item::Bound payloadGetBound(const MeshPartPayload::Pointer& payload) { + + template <> const Item::Bound payloadGetBound(const MeshPartPayload::Pointer& payload) { if (payload) { return payload->model->getPartBounds(payload->meshIndex, payload->partIndex); } @@ -934,7 +900,7 @@ bool Model::addToScene(std::shared_ptr scene, render::PendingChan _renderItems.insert(item, renderPayload); somethingAdded = true; } - + _readyWhenAdded = readyToAddToScene(); return somethingAdded; @@ -966,7 +932,7 @@ bool Model::addToScene(std::shared_ptr scene, render::PendingChan _renderItems.insert(item, renderPayload); somethingAdded = true; } - + _readyWhenAdded = readyToAddToScene(); return somethingAdded; @@ -988,7 +954,7 @@ void Model::renderDebugMeshBoxes() { _debugMeshBoxesID = DependencyManager::get()->allocateID(); } QVector points; - + glm::vec3 brn = box.getCorner(); glm::vec3 bln = brn + glm::vec3(box.getDimensions().x, 0, 0); glm::vec3 brf = brn + glm::vec3(0, 0, box.getDimensions().z); @@ -1022,12 +988,12 @@ void Model::renderDebugMeshBoxes() { { 1.0f, 1.0f, 0.0f, 1.0f }, // yellow { 0.0f, 1.0f, 1.0f, 1.0f }, // cyan { 1.0f, 1.0f, 1.0f, 1.0f }, // white - { 0.0f, 0.5f, 0.0f, 1.0f }, - { 0.0f, 0.0f, 0.5f, 1.0f }, - { 0.5f, 0.0f, 0.5f, 1.0f }, - { 0.5f, 0.5f, 0.0f, 1.0f }, + { 0.0f, 0.5f, 0.0f, 1.0f }, + { 0.0f, 0.0f, 0.5f, 1.0f }, + { 0.5f, 0.0f, 0.5f, 1.0f }, + { 0.5f, 0.5f, 0.0f, 1.0f }, { 0.0f, 0.5f, 0.5f, 1.0f } }; - + DependencyManager::get()->updateVertices(_debugMeshBoxesID, points, color[colorNdx]); DependencyManager::get()->renderVertices(gpu::LINES, _debugMeshBoxesID); colorNdx++; @@ -1062,7 +1028,7 @@ Extents Model::getUnscaledMeshExtents() const { if (!isActive()) { return Extents(); } - + const Extents& extents = _geometry->getFBXGeometry().meshExtents; // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which @@ -1070,7 +1036,7 @@ Extents Model::getUnscaledMeshExtents() const { glm::vec3 minimum = glm::vec3(_geometry->getFBXGeometry().offset * glm::vec4(extents.minimum, 1.0f)); glm::vec3 maximum = glm::vec3(_geometry->getFBXGeometry().offset * glm::vec4(extents.maximum, 1.0f)); Extents scaledExtents = { minimum, maximum }; - + return scaledExtents; } @@ -1079,12 +1045,12 @@ Extents Model::calculateScaledOffsetExtents(const Extents& extents) const { glm::vec3 minimum = glm::vec3(_geometry->getFBXGeometry().offset * glm::vec4(extents.minimum, 1.0f)); glm::vec3 maximum = glm::vec3(_geometry->getFBXGeometry().offset * glm::vec4(extents.maximum, 1.0f)); - Extents scaledOffsetExtents = { ((minimum + _offset) * _scale), + Extents scaledOffsetExtents = { ((minimum + _offset) * _scale), ((maximum + _offset) * _scale) }; Extents rotatedExtents = scaledOffsetExtents.getRotated(_rotation); - Extents translatedExtents = { rotatedExtents.minimum + _translation, + Extents translatedExtents = { rotatedExtents.minimum + _translation, rotatedExtents.maximum + _translation }; return translatedExtents; @@ -1106,39 +1072,27 @@ glm::vec3 Model::calculateScaledOffsetPoint(const glm::vec3& point) const { bool Model::getJointState(int index, glm::quat& rotation) const { - if (_rig) { - return _rig->getJointState(index, rotation); - } else { - if (index == -1 || index >= _jointStates.size()) { - return false; - } - const JointState& state = _jointStates.at(index); - rotation = state.getRotationInConstrainedFrame(); - return !state.rotationIsDefault(rotation); + if (index == -1 || index >= _jointStates.size()) { + return false; } + const JointState& state = _jointStates.at(index); + rotation = state.getRotationInConstrainedFrame(); + return !state.rotationIsDefault(rotation); } bool Model::getVisibleJointState(int index, glm::quat& rotation) const { - if (_rig) { - return _rig->getVisibleJointState(index, rotation); - } else { - if (index == -1 || index >= _jointStates.size()) { - return false; - } - const JointState& state = _jointStates.at(index); - rotation = state.getVisibleRotationInConstrainedFrame(); - return !state.rotationIsDefault(rotation); + if (index == -1 || index >= _jointStates.size()) { + return false; } + const JointState& state = _jointStates.at(index); + rotation = state.getVisibleRotationInConstrainedFrame(); + return !state.rotationIsDefault(rotation); } void Model::clearJointState(int index) { - if (_rig) { - _rig->clearJointState(index); - } else { - if (index != -1 && index < _jointStates.size()) { - JointState& state = _jointStates[index]; - state.setRotationInConstrainedFrame(glm::quat(), 0.0f); - } + if (index != -1 && index < _jointStates.size()) { + JointState& state = _jointStates[index]; + state.setRotationInConstrainedFrame(glm::quat(), 0.0f); } } @@ -1153,16 +1107,12 @@ void Model::clearJointAnimationPriority(int index) { } void Model::setJointState(int index, bool valid, const glm::quat& rotation, float priority) { - if (_rig) { - _rig->setJointState(index, valid, rotation, priority); - } else { - if (index != -1 && index < _jointStates.size()) { - JointState& state = _jointStates[index]; - if (valid) { - state.setRotationInConstrainedFrame(rotation, priority); - } else { - state.restoreRotation(1.0f, priority); - } + if (index != -1 && index < _jointStates.size()) { + JointState& state = _jointStates[index]; + if (valid) { + state.setRotationInConstrainedFrame(rotation, priority); + } else { + state.restoreRotation(1.0f, priority); } } } @@ -1189,7 +1139,7 @@ void Model::setURL(const QUrl& url, const QUrl& fallback, bool retainCurrent, bo onInvalidate(); - // if so instructed, keep the current geometry until the new one is loaded + // if so instructed, keep the current geometry until the new one is loaded _nextGeometry = DependencyManager::get()->getGeometry(url, fallback, delayLoad); _nextLODHysteresis = NetworkGeometry::NO_HYSTERESIS; if (!retainCurrent || !isActive() || (_nextGeometry && _nextGeometry->isLoaded())) { @@ -1199,14 +1149,14 @@ void Model::setURL(const QUrl& url, const QUrl& fallback, bool retainCurrent, bo void Model::geometryRefreshed() { QObject* sender = QObject::sender(); - + if (sender == _geometry) { _readyWhenAdded = false; // reset out render items. _needsReload = true; invalidCalculatedMeshBoxes(); - + onInvalidate(); - + // if so instructed, keep the current geometry until the new one is loaded _nextGeometry = DependencyManager::get()->getGeometry(_url); _nextLODHysteresis = NetworkGeometry::NO_HYSTERESIS; @@ -1226,7 +1176,7 @@ const QSharedPointer Model::getCollisionGeometry(bool delayLoad if (_collisionGeometry && _collisionGeometry->isLoaded()) { return _collisionGeometry; } - + return QSharedPointer(); } @@ -1238,82 +1188,62 @@ void Model::setCollisionModelURL(const QUrl& url) { _collisionGeometry = DependencyManager::get()->getGeometry(url, QUrl(), true); } - -bool Model::getJointStateAtIndex(int jointIndex, JointState& jointState) const { - if (_rig) { - return _rig->getJointStateAtIndex(jointIndex, jointState); - } else { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { - return false; - } - jointState = _jointStates[jointIndex]; - return true; - } -} - bool Model::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position) const { - JointState jointState; - if (!getJointStateAtIndex(jointIndex, jointState)) { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return false; } // position is in world-frame - position = _translation + _rotation * jointState.getPosition(); + position = _translation + _rotation * _jointStates[jointIndex].getPosition(); return true; } bool Model::getJointPosition(int jointIndex, glm::vec3& position) const { - JointState jointState; - if (!getJointStateAtIndex(jointIndex, jointState)) { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return false; } // position is in model-frame - position = extractTranslation(jointState.getTransform()); + position = extractTranslation(_jointStates[jointIndex].getTransform()); return true; } bool Model::getJointRotationInWorldFrame(int jointIndex, glm::quat& rotation) const { - JointState jointState; - if (!getJointStateAtIndex(jointIndex, jointState)) { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return false; } - rotation = _rotation * jointState.getRotation(); + rotation = _rotation * _jointStates[jointIndex].getRotation(); return true; } bool Model::getJointRotation(int jointIndex, glm::quat& rotation) const { - JointState jointState; - if (!getJointStateAtIndex(jointIndex, jointState)) { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return false; } - rotation = jointState.getRotation(); + rotation = _jointStates[jointIndex].getRotation(); return true; } bool Model::getJointCombinedRotation(int jointIndex, glm::quat& rotation) const { - JointState jointState; - if (!getJointStateAtIndex(jointIndex, jointState)) { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return false; } - rotation = _rotation * jointState.getRotation(); + rotation = _rotation * _jointStates[jointIndex].getRotation(); return true; } bool Model::getVisibleJointPositionInWorldFrame(int jointIndex, glm::vec3& position) const { - JointState jointState; - if (!getJointStateAtIndex(jointIndex, jointState)) { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return false; } // position is in world-frame - position = _translation + _rotation * jointState.getVisiblePosition(); + position = _translation + _rotation * _jointStates[jointIndex].getVisiblePosition(); return true; } bool Model::getVisibleJointRotationInWorldFrame(int jointIndex, glm::quat& rotation) const { - JointState jointState; - if (!getJointStateAtIndex(jointIndex, jointState)) { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return false; } - rotation = _rotation * jointState.getVisibleRotation(); + rotation = _rotation * _jointStates[jointIndex].getVisibleRotation(); return true; } @@ -1341,11 +1271,11 @@ public: Blender(Model* model, int blendNumber, const QWeakPointer& geometry, const QVector& meshes, const QVector& blendshapeCoefficients); - + virtual void run(); private: - + QPointer _model; int _blendNumber; QWeakPointer _geometry; @@ -1419,10 +1349,10 @@ void Model::setScaleToFit(bool scaleToFit, float largestDimension, bool forceRes } return; } - + if (forceRescale || _scaleToFit != scaleToFit || glm::length(_scaleToFitDimensions) != largestDimension) { _scaleToFit = scaleToFit; - + // we only need to do this work if we're "turning on" scale to fit. if (scaleToFit) { Extents modelMeshExtents = getUnscaledMeshExtents(); @@ -1443,7 +1373,7 @@ void Model::scaleToFit() { // we didn't yet have an active mesh. We can only enter this scaleToFit() in this state // if we now do have an active mesh, so we take this opportunity to actually determine // the correct scale. - if (_scaleToFit && _scaleToFitDimensions.y == FAKE_DIMENSION_PLACEHOLDER + if (_scaleToFit && _scaleToFitDimensions.y == FAKE_DIMENSION_PLACEHOLDER && _scaleToFitDimensions.z == FAKE_DIMENSION_PLACEHOLDER) { setScaleToFit(_scaleToFit, _scaleToFitDimensions.x); } @@ -1478,7 +1408,7 @@ void Model::simulate(float deltaTime, bool fullUpdate) { PROFILE_RANGE(__FUNCTION__); fullUpdate = updateGeometry() || fullUpdate || (_scaleToFit && !_scaledToFit) || (_snapModelToRegistrationPoint && !_snappedToRegistrationPoint); - + if (isActive() && fullUpdate) { // NOTE: This is overly aggressive and we are invalidating the MeshBoxes when in fact they may not be invalid // they really only become invalid if something about the transform to world space has changed. This is @@ -1508,20 +1438,12 @@ void Model::updateClusterMatrices() { if (_showTrueJointTransforms) { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - JointState jointState; - if (!getJointStateAtIndex(cluster.jointIndex, jointState)) { - return; - } - state.clusterMatrices[j] = modelToWorld * jointState.getTransform() * cluster.inverseBindMatrix; + state.clusterMatrices[j] = modelToWorld * _jointStates[cluster.jointIndex].getTransform() * cluster.inverseBindMatrix; } } else { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - JointState jointState; - if (!getJointStateAtIndex(cluster.jointIndex, jointState)) { - return; - } - state.clusterMatrices[j] = modelToWorld * jointState.getVisibleTransform() * cluster.inverseBindMatrix; + state.clusterMatrices[j] = modelToWorld * _jointStates[cluster.jointIndex].getVisibleTransform() * cluster.inverseBindMatrix; } } } @@ -1529,26 +1451,21 @@ void Model::updateClusterMatrices() { void Model::simulateInternal(float deltaTime) { // update the world space transforms for all joints - + // update animations foreach (const AnimationHandlePointer& handle, _runningAnimations) { handle->simulate(deltaTime); } - if (_rig) { - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - _rig->updateJointStates(parentTransform); - _rig->resetAllTransformsChanged(); - } else { - updateJointStates(); - for (int i = 0; i < _jointStates.size(); i++) { - _jointStates[i].resetTransformChanged(); - } + for (int i = 0; i < _jointStates.size(); i++) { + updateJointState(i); + } + for (int i = 0; i < _jointStates.size(); i++) { + _jointStates[i].resetTransformChanged(); } _shapesAreDirty = !_shapes.isEmpty(); - + const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 modelToWorld = glm::mat4_cast(_rotation); for (int i = 0; i < _meshStates.size(); i++) { @@ -1557,24 +1474,16 @@ void Model::simulateInternal(float deltaTime) { if (_showTrueJointTransforms) { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - JointState jointState; - if (!getJointStateAtIndex(cluster.jointIndex, jointState)) { - return; - } - state.clusterMatrices[j] = modelToWorld * jointState.getTransform() * cluster.inverseBindMatrix; + state.clusterMatrices[j] = modelToWorld * _jointStates[cluster.jointIndex].getTransform() * cluster.inverseBindMatrix; } } else { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - JointState jointState; - if (!getJointStateAtIndex(cluster.jointIndex, jointState)) { - return; - } - state.clusterMatrices[j] = modelToWorld * jointState.getVisibleTransform() * cluster.inverseBindMatrix; + state.clusterMatrices[j] = modelToWorld * _jointStates[cluster.jointIndex].getVisibleTransform() * cluster.inverseBindMatrix; } } } - + // post the blender if we're not currently waiting for one to finish if (geometry.hasBlendedMeshes() && _blendshapeCoefficients != _blendedBlendshapeCoefficients) { _blendedBlendshapeCoefficients = _blendshapeCoefficients; @@ -1582,18 +1491,10 @@ void Model::simulateInternal(float deltaTime) { } } -void Model::updateJointStates() { - assert(!_rig); - for (int i = 0; i < _jointStates.size(); i++) { - updateJointState(i); - } -} - void Model::updateJointState(int index) { - assert(!_rig); JointState& state = _jointStates[index]; const FBXJoint& joint = state.getFBXJoint(); - + // compute model transforms int parentIndex = joint.parentIndex; if (parentIndex == -1) { @@ -1614,38 +1515,13 @@ void Model::updateVisibleJointStates() { // no need to update visible transforms return; } - if (_rig) { - _rig->updateVisibleJointStates(); - } else { - for (int i = 0; i < _jointStates.size(); i++) { - _jointStates[i].slaveVisibleTransform(); - } + for (int i = 0; i < _jointStates.size(); i++) { + _jointStates[i].slaveVisibleTransform(); } } -glm::quat Model::setJointRotationInBindFrame(int jointIndex, const glm::quat& rotation, float priority) { - glm::quat endRotation; - if (jointIndex == -1 || _jointStates.isEmpty()) { - return endRotation; - } - JointState& state = _jointStates[jointIndex]; - state.setRotationInBindFrame(rotation, priority); - endRotation = state.getRotationInBindFrame(); - return endRotation; -} - - bool Model::setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, bool useRotation, - int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, float priority) { - if (_rig) { - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - bool result = _rig->setJointPosition(jointIndex, position, rotation, useRotation, lastFreeIndex, allIntermediatesFree, - alignment, priority, parentTransform); - _shapesAreDirty = !_shapes.isEmpty(); - return result; - } - + int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, float priority) { if (jointIndex == -1 || _jointStates.isEmpty()) { return false; } @@ -1730,16 +1606,8 @@ bool Model::setJointPosition(int jointIndex, const glm::vec3& position, const gl return true; } -void Model::inverseKinematics(int endIndex, glm::vec3 targetPosition, - const glm::quat& targetRotation, float priority) { +void Model::inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::quat& targetRotation, float priority) { // NOTE: targetRotation is from bind- to model-frame - if (_rig) { - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - glm::mat4 topParentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - _rig->inverseKinematics( endIndex, targetPosition, targetRotation, priority, topParentTransform); - _shapesAreDirty = !_shapes.isEmpty(); - return; - } if (endIndex == -1 || _jointStates.isEmpty()) { return; @@ -1849,20 +1717,17 @@ void Model::inverseKinematics(int endIndex, glm::vec3 targetPosition, // set final rotation of the end joint endState.setRotationInBindFrame(targetRotation, priority, true); - + _shapesAreDirty = !_shapes.isEmpty(); } bool Model::restoreJointPosition(int jointIndex, float fraction, float priority) { - if (_rig) { - return _rig->restoreJointPosition(jointIndex, fraction, priority); - } if (jointIndex == -1 || _jointStates.isEmpty()) { return false; } const FBXGeometry& geometry = _geometry->getFBXGeometry(); const QVector& freeLineage = geometry.joints.at(jointIndex).freeLineage; - + foreach (int index, freeLineage) { JointState& state = _jointStates[index]; state.restoreRotation(fraction, priority); @@ -1871,9 +1736,6 @@ bool Model::restoreJointPosition(int jointIndex, float fraction, float priority) } float Model::getLimbLength(int jointIndex) const { - if (_rig) { - return _rig->getLimbLength(jointIndex, _scale); - } if (jointIndex == -1 || _jointStates.isEmpty()) { return 0.0f; } @@ -1907,7 +1769,7 @@ void Model::setBlendedVertices(int blendNumber, const QWeakPointergetFBXGeometry(); + const FBXGeometry& fbxGeometry = _geometry->getFBXGeometry(); int index = 0; for (int i = 0; i < fbxGeometry.meshes.size(); i++) { const FBXMesh& mesh = fbxGeometry.meshes.at(i); @@ -1928,7 +1790,7 @@ void Model::setGeometry(const QSharedPointer& newGeometry) { if (_geometry == newGeometry) { return; } - + if (_geometry) { _geometry->disconnect(_geometry.data(), &Resource::onRefresh, this, &Model::geometryRefreshed); } @@ -1941,10 +1803,10 @@ void Model::applyNextGeometry() { deleteGeometry(); _dilatedTextures.clear(); _lodHysteresis = _nextLODHysteresis; - + // we retain a reference to the base geometry so that its reference count doesn't fall to zero setGeometry(_nextGeometry); - + _meshGroupsKnown = false; _readyWhenAdded = false; // in case any of our users are using scenes _needsReload = false; // we are loaded now! @@ -1955,12 +1817,9 @@ void Model::applyNextGeometry() { void Model::deleteGeometry() { _blendedVertexBuffers.clear(); _jointStates.clear(); - if (_rig) { - _rig->clearJointStates(); - } _meshStates.clear(); clearShapes(); - + for (QSet::iterator it = _animationHandles.begin(); it != _animationHandles.end(); ) { AnimationHandlePointer handle = it->lock(); if (handle) { @@ -1970,11 +1829,11 @@ void Model::deleteGeometry() { it = _animationHandles.erase(it); } } - + if (_geometry) { _geometry->clearLoadPriority(this); } - + _blendedBlendshapeCoefficients.clear(); } @@ -1988,9 +1847,9 @@ AABox Model::getPartBounds(int meshIndex, int partIndex) { return calculateScaledOffsetAABox(_geometry->getFBXGeometry().meshExtents); } } - + if (_geometry->getFBXGeometry().meshes.size() > meshIndex) { - + // FIX ME! - This is currently a hack because for some mesh parts our efforts to calculate the bounding // box of the mesh part fails. It seems to create boxes that are not consistent with where the // geometry actually renders. If instead we make all the parts share the bounds of the entire subMesh @@ -2015,7 +1874,7 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran if (!_readyWhenAdded) { return; // bail asap } - + // We need to make sure we have valid offsets calculated before we can render if (!_calculatedMeshPartOffsetValid) { _mutex.lock(); @@ -2040,13 +1899,13 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran // guard against partially loaded meshes if (meshIndex >= networkMeshes.size() || meshIndex >= geometry.meshes.size() || meshIndex >= _meshStates.size() ) { - return; + return; } const NetworkMesh& networkMesh = networkMeshes.at(meshIndex); const FBXMesh& mesh = geometry.meshes.at(meshIndex); const MeshState& state = _meshStates.at(meshIndex); - + bool translucentMesh = translucent; // networkMesh.getTranslucentPartCount(mesh) == networkMesh.parts.size(); bool hasTangents = !mesh.tangents.isEmpty(); bool hasSpecular = mesh.hasSpecularTexture(); @@ -2076,7 +1935,7 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran DependencyManager::get()->renderWireCube(batch, 1.0f, cubeColor); } #endif //def DEBUG_BOUNDING_PARTS - + if (wireframe) { translucentMesh = hasTangents = hasSpecular = hasLightmap = isSkinned = false; } @@ -2091,14 +1950,14 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran // if our index is ever out of range for either meshes or networkMeshes, then skip it, and set our _meshGroupsKnown // to false to rebuild out mesh groups. - + if (meshIndex < 0 || meshIndex >= networkMeshes.size() || meshIndex > geometry.meshes.size()) { _meshGroupsKnown = false; // regenerate these lists next time around. _readyWhenAdded = false; // in case any of our users are using scenes invalidCalculatedMeshBoxes(); // if we have to reload, we need to assume our mesh boxes are all invalid return; // FIXME! } - + batch.setIndexBuffer(gpu::UINT32, (networkMesh._indexBuffer), 0); int vertexCount = mesh.vertices.size(); if (vertexCount == 0) { @@ -2110,7 +1969,7 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran if (_transforms.empty()) { _transforms.push_back(Transform()); } - + if (isSkinned) { batch._glUniformMatrix4fv(locations->clusterMatrices, state.clusterMatrices.size(), false, (const float*)state.clusterMatrices.constData()); @@ -2150,7 +2009,7 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran qCDebug(renderutils) << "WARNING: material == nullptr!!!"; } #endif - + if (material != nullptr) { // apply material properties @@ -2192,12 +2051,12 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran batch._glUniformMatrix4fv(locations->texcoordMatrices, 2, false, (const float*) &texcoordTransform); } - if (!mesh.tangents.isEmpty()) { + if (!mesh.tangents.isEmpty()) { NetworkTexture* normalMap = networkPart.normalTexture.data(); batch.setResourceTexture(1, (!normalMap || !normalMap->isLoaded()) ? textureCache->getBlueTexture() : normalMap->getGPUTexture()); } - + if (locations->specularTextureUnit >= 0) { NetworkTexture* specularMap = networkPart.specularTexture.data(); batch.setResourceTexture(locations->specularTextureUnit, (!specularMap || !specularMap->isLoaded()) ? @@ -2215,18 +2074,18 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran float emissiveOffset = part.emissiveParams.x; float emissiveScale = part.emissiveParams.y; batch._glUniform2f(locations->emissiveParams, emissiveOffset, emissiveScale); - + NetworkTexture* emissiveMap = networkPart.emissiveTexture.data(); batch.setResourceTexture(locations->emissiveTextureUnit, (!emissiveMap || !emissiveMap->isLoaded()) ? textureCache->getGrayTexture() : emissiveMap->getGPUTexture()); } - + if (translucent && locations->lightBufferUnit >= 0) { DependencyManager::get()->setupTransparent(args, locations->lightBufferUnit); } } } - + qint64 offset; { // FIXME_STUTTER: We should n't have any lock here @@ -2263,7 +2122,7 @@ void Model::segregateMeshGroups() { qDebug() << "WARNING!!!! Mesh Sizes don't match! We will not segregate mesh groups yet."; return; } - + _transparentRenderItems.clear(); _opaqueRenderItems.clear(); @@ -2272,7 +2131,7 @@ void Model::segregateMeshGroups() { const NetworkMesh& networkMesh = networkMeshes.at(i); const FBXMesh& mesh = geometry.meshes.at(i); const MeshState& state = _meshStates.at(i); - + bool translucentMesh = networkMesh.getTranslucentPartCount(mesh) == networkMesh.parts.size(); bool hasTangents = !mesh.tangents.isEmpty(); @@ -2280,7 +2139,7 @@ void Model::segregateMeshGroups() { bool hasLightmap = mesh.hasEmissiveTexture(); bool isSkinned = state.clusterMatrices.size() > 1; bool wireframe = isWireframe(); - + if (wireframe) { translucentMesh = hasTangents = hasSpecular = hasLightmap = isSkinned = false; } @@ -2297,7 +2156,7 @@ void Model::segregateMeshGroups() { } } _meshGroupsKnown = true; -} +} void Model::pickPrograms(gpu::Batch& batch, RenderMode mode, bool translucent, float alphaThreshold, bool hasLightmap, bool hasTangents, bool hasSpecular, bool isSkinned, bool isWireframe, RenderArgs* args, @@ -2317,7 +2176,7 @@ void Model::pickPrograms(gpu::Batch& batch, RenderMode mode, bool translucent, f gpu::ShaderPointer program = (*pipeline).second._pipeline->getProgram(); locations = (*pipeline).second._locations.get(); - + // Setup the One pipeline batch.setPipeline((*pipeline).second._pipeline); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 5289302390..1b4df6821c 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -64,7 +64,7 @@ public: static void setAbstractViewStateInterface(AbstractViewStateInterface* viewState) { _viewState = viewState; } - Model(QObject* parent = NULL, RigPointer rig = nullptr); + Model(QObject* parent = nullptr, RigPointer rig = nullptr); virtual ~Model(); /// enables/disables scale to fit behavior, the model will be automatically scaled to the specified largest dimension @@ -206,6 +206,7 @@ public: QStringList getJointNames() const; + AnimationHandlePointer createAnimationHandle(); const QList& getRunningAnimations() const { return _runningAnimations; } @@ -259,7 +260,6 @@ protected: bool _showTrueJointTransforms; - bool getJointStateAtIndex(int jointIndex, JointState& jointState) const; QVector _jointStates; class MeshState { @@ -281,13 +281,10 @@ protected: void simulateInternal(float deltaTime); /// Updates the state of the joint at the specified index. - void updateJointStates(); virtual void updateJointState(int index); virtual void updateVisibleJointStates(); - - glm::quat setJointRotationInBindFrame(int jointIndex, const glm::quat& rotation, float priority); - + /// \param jointIndex index of joint in model structure /// \param position position of joint in model-frame /// \param rotation rotation of joint in model-frame @@ -297,8 +294,8 @@ protected: /// \param alignment /// \return true if joint exists bool setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation = glm::quat(), - bool useRotation = false, int lastFreeIndex = -1, bool allIntermediatesFree = false, - const glm::vec3& alignment = glm::vec3(0.0f, -1.0f, 0.0f), float priority = 1.0f); + bool useRotation = false, int lastFreeIndex = -1, bool allIntermediatesFree = false, + const glm::vec3& alignment = glm::vec3(0.0f, -1.0f, 0.0f), float priority = 1.0f); /// Restores the indexed joint to its default position. /// \param fraction the fraction of the default position to apply (i.e., 0.25f to slerp one fourth of the way to diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index f1ff0e9e4f..4e7e413770 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -113,6 +113,8 @@ void vhacd::VHACDUtil::fattenMeshes(const FBXMesh& mesh, FBXMesh& result, int index1 = triangles[i * 3 + 1] + indexStartOffset; int index2 = triangles[i * 3 + 2] + indexStartOffset; + // TODO: skip triangles with a normal that points more negative-y than positive-y + glm::vec3 p0 = result.vertices[index0]; glm::vec3 p1 = result.vertices[index1]; glm::vec3 p2 = result.vertices[index2]; From 8173dc2c9a12cc28911b63010b91d1d10b42883a Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 22 Jul 2015 14:21:42 -0700 Subject: [PATCH 054/242] Remove text extensions reference that isn't needed and which has changed underneath us. --- tests/rig/src/RigTests.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rig/src/RigTests.h b/tests/rig/src/RigTests.h index ddb8de9c2d..9f8ba22eb8 100644 --- a/tests/rig/src/RigTests.h +++ b/tests/rig/src/RigTests.h @@ -15,7 +15,7 @@ #include #include -#include "../QTestExtensions.h" +//#include "../QTestExtensions.h" // The QTest terminology is not consistent with itself or with industry: From ee334ff8266e1fbcaac8a275d28cd40b4b61d748 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 22 Jul 2015 18:43:10 -0700 Subject: [PATCH 055/242] JointStates are owned by Rig objects. Model, FaceModel, SkeletonModel call into their Rig pointer to access JointStates. --- interface/src/avatar/Avatar.cpp | 3 +- interface/src/avatar/AvatarManager.cpp | 5 +- interface/src/avatar/FaceModel.cpp | 48 +- interface/src/avatar/FaceModel.h | 6 +- interface/src/avatar/Head.cpp | 4 +- interface/src/avatar/SkeletonModel.cpp | 131 +++--- interface/src/avatar/SkeletonModel.h | 6 +- interface/src/ui/overlays/ModelOverlay.cpp | 4 +- libraries/animation/src/AnimationHandle.cpp | 22 +- libraries/animation/src/AvatarRig.cpp | 51 +++ libraries/animation/src/AvatarRig.h | 28 ++ libraries/animation/src/EntityRig.cpp | 30 ++ libraries/animation/src/EntityRig.h | 28 ++ libraries/animation/src/Rig.cpp | 315 ++++++++----- libraries/animation/src/Rig.h | 74 +-- .../src/EntityTreeRenderer.cpp | 5 +- .../src/RenderableZoneEntityItem.cpp | 2 +- libraries/render-utils/src/Model.cpp | 421 +++--------------- libraries/render-utils/src/Model.h | 14 +- 19 files changed, 562 insertions(+), 635 deletions(-) create mode 100644 libraries/animation/src/AvatarRig.cpp create mode 100644 libraries/animation/src/AvatarRig.h create mode 100644 libraries/animation/src/EntityRig.cpp create mode 100644 libraries/animation/src/EntityRig.h diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 1d14a0f464..26e218d05b 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -45,6 +45,7 @@ #include "Util.h" #include "world.h" #include "InterfaceLogging.h" +#include "EntityRig.h" using namespace std; @@ -965,7 +966,7 @@ void Avatar::setAttachmentData(const QVector& attachmentData) { if (_unusedAttachments.size() > 0) { model = _unusedAttachments.takeFirst(); } else { - model = new Model(this); + model = new Model(std::make_shared(), this); } model->init(); _attachmentModels.append(model); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 730c77e9c3..ead330a41f 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -35,6 +35,7 @@ #include "Menu.h" #include "MyAvatar.h" #include "SceneScriptingInterface.h" +#include "AvatarRig.h" // 70 times per second - target is 60hz, but this helps account for any small deviations // in the update loop @@ -65,7 +66,7 @@ AvatarManager::AvatarManager(QObject* parent) : { // register a meta type for the weak pointer we'll use for the owning avatar mixer for each avatar qRegisterMetaType >("NodeWeakPointer"); - _myAvatar = std::make_shared(std::make_shared()); + _myAvatar = std::make_shared(std::make_shared()); auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket"); @@ -160,7 +161,7 @@ void AvatarManager::simulateAvatarFades(float deltaTime) { } AvatarSharedPointer AvatarManager::newSharedAvatar() { - return AvatarSharedPointer(std::make_shared(std::make_shared())); + return AvatarSharedPointer(std::make_shared(std::make_shared())); } // virtual diff --git a/interface/src/avatar/FaceModel.cpp b/interface/src/avatar/FaceModel.cpp index 16b370d459..4a5eadccd3 100644 --- a/interface/src/avatar/FaceModel.cpp +++ b/interface/src/avatar/FaceModel.cpp @@ -16,9 +16,11 @@ #include "Head.h" #include "Menu.h" -FaceModel::FaceModel(Head* owningHead) : +FaceModel::FaceModel(Head* owningHead, RigPointer rig) : + Model(rig, nullptr), _owningHead(owningHead) { + assert(_rig); } void FaceModel::simulate(float deltaTime, bool fullUpdate) { @@ -48,54 +50,58 @@ void FaceModel::simulate(float deltaTime, bool fullUpdate) { } } -void FaceModel::maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, JointState& state) { +void FaceModel::maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, int index) { // get the rotation axes in joint space and use them to adjust the rotation glm::mat3 axes = glm::mat3_cast(glm::quat()); glm::mat3 inverse = glm::mat3(glm::inverse(parentState.getTransform() * - glm::translate(state.getDefaultTranslationInConstrainedFrame()) * + glm::translate(_rig->getJointDefaultTranslationInConstrainedFrame(index)) * joint.preTransform * glm::mat4_cast(joint.preRotation))); glm::vec3 pitchYawRoll = safeEulerAngles(_owningHead->getFinalOrientationInLocalFrame()); glm::vec3 lean = glm::radians(glm::vec3(_owningHead->getFinalLeanForward(), _owningHead->getTorsoTwist(), _owningHead->getFinalLeanSideways())); pitchYawRoll -= lean; - state.setRotationInConstrainedFrame(glm::angleAxis(-pitchYawRoll.z, glm::normalize(inverse * axes[2])) - * glm::angleAxis(pitchYawRoll.y, glm::normalize(inverse * axes[1])) - * glm::angleAxis(-pitchYawRoll.x, glm::normalize(inverse * axes[0])) - * joint.rotation, DEFAULT_PRIORITY); + _rig->setJointRotationInConstrainedFrame(index, + glm::angleAxis(-pitchYawRoll.z, glm::normalize(inverse * axes[2])) + * glm::angleAxis(pitchYawRoll.y, glm::normalize(inverse * axes[1])) + * glm::angleAxis(-pitchYawRoll.x, glm::normalize(inverse * axes[0])) + * joint.rotation, DEFAULT_PRIORITY); } -void FaceModel::maybeUpdateEyeRotation(Model* model, const JointState& parentState, const FBXJoint& joint, JointState& state) { +void FaceModel::maybeUpdateEyeRotation(Model* model, const JointState& parentState, const FBXJoint& joint, int index) { // likewise with the eye joints // NOTE: at the moment we do the math in the world-frame, hence the inverse transform is more complex than usual. - glm::mat4 inverse = glm::inverse(glm::mat4_cast(model->getRotation()) * parentState.getTransform() * - glm::translate(state.getDefaultTranslationInConstrainedFrame()) * - joint.preTransform * glm::mat4_cast(joint.preRotation * joint.rotation)); + glm::mat4 inverse = glm::inverse(glm::mat4_cast(model->getRotation()) * parentState.getTransform() * + glm::translate(_rig->getJointDefaultTranslationInConstrainedFrame(index)) * + joint.preTransform * glm::mat4_cast(joint.preRotation * joint.rotation)); glm::vec3 front = glm::vec3(inverse * glm::vec4(_owningHead->getFinalOrientationInWorldFrame() * IDENTITY_FRONT, 0.0f)); glm::vec3 lookAt = glm::vec3(inverse * glm::vec4(_owningHead->getCorrectedLookAtPosition() + _owningHead->getSaccade() - model->getTranslation(), 1.0f)); glm::quat between = rotationBetween(front, lookAt); const float MAX_ANGLE = 30.0f * RADIANS_PER_DEGREE; - state.setRotationInConstrainedFrame(glm::angleAxis(glm::clamp(glm::angle(between), -MAX_ANGLE, MAX_ANGLE), glm::axis(between)) * - joint.rotation, DEFAULT_PRIORITY); + _rig->setJointRotationInConstrainedFrame(index, glm::angleAxis(glm::clamp(glm::angle(between), + -MAX_ANGLE, MAX_ANGLE), glm::axis(between)) * + joint.rotation, DEFAULT_PRIORITY); } void FaceModel::updateJointState(int index) { - JointState& state = _jointStates[index]; + const JointState& state = _rig->getJointState(index); const FBXJoint& joint = state.getFBXJoint(); + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + // guard against out-of-bounds access to _jointStates - if (joint.parentIndex != -1 && joint.parentIndex >= 0 && joint.parentIndex < _jointStates.size()) { - const JointState& parentState = _jointStates.at(joint.parentIndex); - const FBXGeometry& geometry = _geometry->getFBXGeometry(); + if (joint.parentIndex != -1 && joint.parentIndex >= 0 && joint.parentIndex < _rig->getJointStateCount()) { + const JointState& parentState = _rig->getJointState(joint.parentIndex); if (index == geometry.neckJointIndex) { - maybeUpdateNeckRotation(parentState, joint, state); - + maybeUpdateNeckRotation(parentState, joint, index); + } else if (index == geometry.leftEyeJointIndex || index == geometry.rightEyeJointIndex) { - maybeUpdateEyeRotation(this, parentState, joint, state); + maybeUpdateEyeRotation(this, parentState, joint, index); } } - Model::updateJointState(index); + glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; + _rig->updateFaceJointState(index, parentTransform); } bool FaceModel::getEyePositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const { diff --git a/interface/src/avatar/FaceModel.h b/interface/src/avatar/FaceModel.h index 6c14beb587..ce78c51e70 100644 --- a/interface/src/avatar/FaceModel.h +++ b/interface/src/avatar/FaceModel.h @@ -22,12 +22,12 @@ class FaceModel : public Model { public: - FaceModel(Head* owningHead); + FaceModel(Head* owningHead, RigPointer rig); virtual void simulate(float deltaTime, bool fullUpdate = true); - virtual void maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, JointState& state); - virtual void maybeUpdateEyeRotation(Model* model, const JointState& parentState, const FBXJoint& joint, JointState& state); + virtual void maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, int index); + virtual void maybeUpdateEyeRotation(Model* model, const JointState& parentState, const FBXJoint& joint, int index); virtual void updateJointState(int index); /// Retrieve the positions of up to two eye meshes. diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index 43e68557ce..0d07f171c0 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -23,6 +23,7 @@ #include "Util.h" #include "devices/DdeFaceTracker.h" #include "devices/Faceshift.h" +#include "AvatarRig.h" using namespace std; @@ -55,11 +56,10 @@ Head::Head(Avatar* owningAvatar) : _deltaLeanForward(0.0f), _isCameraMoving(false), _isLookingAtMe(false), - _faceModel(this), + _faceModel(this, std::make_shared()), _leftEyeLookAtID(DependencyManager::get()->allocateID()), _rightEyeLookAtID(DependencyManager::get()->allocateID()) { - } void Head::init() { diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index c8e6f03d69..765dc9160e 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -30,7 +30,7 @@ enum StandingFootState { }; SkeletonModel::SkeletonModel(Avatar* owningAvatar, QObject* parent, RigPointer rig) : - Model(parent, rig), + Model(rig, parent), _triangleFanID(DependencyManager::get()->allocateID()), _owningAvatar(owningAvatar), _boundingShape(), @@ -51,11 +51,13 @@ SkeletonModel::~SkeletonModel() { } void SkeletonModel::initJointStates(QVector states) { - Model::initJointStates(states); + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; + _boundingRadius = _rig->initJointStates(states, parentTransform); // Determine the default eye position for avatar scale = 1.0 int headJointIndex = _geometry->getFBXGeometry().headJointIndex; - if (0 <= headJointIndex && headJointIndex < _jointStates.size()) { + if (0 <= headJointIndex && headJointIndex < _rig->getJointStateCount()) { glm::vec3 leftEyePosition, rightEyePosition; getEyeModelPositions(leftEyePosition, rightEyePosition); @@ -75,8 +77,8 @@ void SkeletonModel::initJointStates(QVector states) { // the SkeletonModel override of updateJointState() will clear the translation part // of its root joint and we need that done before we try to build shapes hence we // recompute all joint transforms at this time. - for (int i = 0; i < _jointStates.size(); i++) { - updateJointState(i); + for (int i = 0; i < _rig->getJointStateCount(); i++) { + _rig->updateJointState(i, parentTransform); } clearShapes(); @@ -168,7 +170,7 @@ void SkeletonModel::getHandShapes(int jointIndex, QVector& shapes) || jointIndex == getRightHandJointIndex()) { // get all shapes that have this hand as an ancestor in the skeleton heirarchy const FBXGeometry& geometry = _geometry->getFBXGeometry(); - for (int i = 0; i < _jointStates.size(); i++) { + for (int i = 0; i < _rig->getJointStateCount(); i++) { const FBXJoint& joint = geometry.joints[i]; int parentIndex = joint.parentIndex; Shape* shape = _shapes[i]; @@ -211,7 +213,7 @@ bool operator<(const IndexValue& firstIndex, const IndexValue& secondIndex) { } void SkeletonModel::applyHandPosition(int jointIndex, const glm::vec3& position) { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + if (jointIndex == -1 || jointIndex >= _rig->getJointStateCount()) { return; } // NOTE: 'position' is in model-frame @@ -226,16 +228,20 @@ void SkeletonModel::applyHandPosition(int jointIndex, const glm::vec3& position) if (forearmLength < EPSILON) { return; } - JointState& state = _jointStates[jointIndex]; - glm::quat handRotation = state.getRotation(); + glm::quat handRotation; + if (!_rig->getJointStateRotation(jointIndex, handRotation)) { + return; + } // align hand with forearm float sign = (jointIndex == geometry.rightHandJointIndex) ? 1.0f : -1.0f; - state.applyRotationDelta(rotationBetween(handRotation * glm::vec3(-sign, 0.0f, 0.0f), forearmVector), true, PALM_PRIORITY); + _rig->applyJointRotationDelta(jointIndex, + rotationBetween(handRotation * glm::vec3(-sign, 0.0f, 0.0f), forearmVector), + true, PALM_PRIORITY); } void SkeletonModel::applyPalmData(int jointIndex, PalmData& palm) { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + if (jointIndex == -1 || jointIndex >= _rig->getJointStateCount()) { return; } const FBXGeometry& geometry = _geometry->getFBXGeometry(); @@ -261,43 +267,40 @@ void SkeletonModel::applyPalmData(int jointIndex, PalmData& palm) { glm::vec3 forearm = palmRotation * glm::vec3(sign * forearmLength, 0.0f, 0.0f); setJointPosition(parentJointIndex, palmPosition + forearm, glm::quat(), false, -1, false, glm::vec3(0.0f, -1.0f, 0.0f), PALM_PRIORITY); - JointState& parentState = _jointStates[parentJointIndex]; - parentState.setRotationInBindFrame(palmRotation, PALM_PRIORITY); + _rig->setJointRotationInBindFrame(parentJointIndex, palmRotation, PALM_PRIORITY); // lock hand to forearm by slamming its rotation (in parent-frame) to identity - _jointStates[jointIndex].setRotationInConstrainedFrame(glm::quat(), PALM_PRIORITY); + _rig->setJointRotationInConstrainedFrame(jointIndex, glm::quat(), PALM_PRIORITY); } else { inverseKinematics(jointIndex, palmPosition, palmRotation, PALM_PRIORITY); } } void SkeletonModel::updateJointState(int index) { - if (index < 0 && index >= _jointStates.size()) { - return; // bail - } - JointState& state = _jointStates[index]; - const FBXJoint& joint = state.getFBXJoint(); - if (joint.parentIndex >= 0 && joint.parentIndex < _jointStates.size()) { - const JointState& parentState = _jointStates.at(joint.parentIndex); - const FBXGeometry& geometry = _geometry->getFBXGeometry(); + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; + + const JointState joint = _rig->getJointState(index); + if (joint.getParentIndex() >= 0 && joint.getParentIndex() < _rig->getJointStateCount()) { + const JointState parentState = _rig->getJointState(joint.getParentIndex()); if (index == geometry.leanJointIndex) { - maybeUpdateLeanRotation(parentState, state); - + maybeUpdateLeanRotation(parentState, index); + } else if (index == geometry.neckJointIndex) { - maybeUpdateNeckRotation(parentState, joint, state); - + maybeUpdateNeckRotation(parentState, joint.getFBXJoint(), index); + } else if (index == geometry.leftEyeJointIndex || index == geometry.rightEyeJointIndex) { - maybeUpdateEyeRotation(parentState, joint, state); + maybeUpdateEyeRotation(parentState, joint.getFBXJoint(), index); } } - Model::updateJointState(index); + _rig->updateJointState(index, parentTransform); if (index == _geometry->getFBXGeometry().rootJointIndex) { - state.clearTransformTranslation(); + _rig->clearJointTransformTranslation(index); } } -void SkeletonModel::maybeUpdateLeanRotation(const JointState& parentState, JointState& state) { +void SkeletonModel::maybeUpdateLeanRotation(const JointState& parentState, int index) { if (!_owningAvatar->isMyAvatar()) { return; } @@ -305,24 +308,24 @@ void SkeletonModel::maybeUpdateLeanRotation(const JointState& parentState, Joint glm::vec3 xAxis(1.0f, 0.0f, 0.0f); glm::vec3 yAxis(0.0f, 1.0f, 0.0f); glm::vec3 zAxis(0.0f, 0.0f, 1.0f); - glm::quat inverse = glm::inverse(parentState.getRotation() * state.getDefaultRotationInParentFrame()); - state.setRotationInConstrainedFrame( - glm::angleAxis(- RADIANS_PER_DEGREE * _owningAvatar->getHead()->getFinalLeanSideways(), inverse * zAxis) - * glm::angleAxis(- RADIANS_PER_DEGREE * _owningAvatar->getHead()->getFinalLeanForward(), inverse * xAxis) - * glm::angleAxis(RADIANS_PER_DEGREE * _owningAvatar->getHead()->getTorsoTwist(), inverse * yAxis) - * state.getFBXJoint().rotation, LEAN_PRIORITY); + glm::quat inverse = glm::inverse(parentState.getRotation() * _rig->getJointDefaultRotationInParentFrame(index)); + _rig->setJointRotationInConstrainedFrame(index, + glm::angleAxis(- RADIANS_PER_DEGREE * _owningAvatar->getHead()->getFinalLeanSideways(), inverse * zAxis) + * glm::angleAxis(- RADIANS_PER_DEGREE * _owningAvatar->getHead()->getFinalLeanForward(), inverse * xAxis) + * glm::angleAxis(RADIANS_PER_DEGREE * _owningAvatar->getHead()->getTorsoTwist(), inverse * yAxis) + * _rig->getJointState(index).getFBXJoint().rotation, LEAN_PRIORITY); } -void SkeletonModel::maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, JointState& state) { - _owningAvatar->getHead()->getFaceModel().maybeUpdateNeckRotation(parentState, joint, state); +void SkeletonModel::maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, int index) { + _owningAvatar->getHead()->getFaceModel().maybeUpdateNeckRotation(parentState, joint, index); } -void SkeletonModel::maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, JointState& state) { - _owningAvatar->getHead()->getFaceModel().maybeUpdateEyeRotation(this, parentState, joint, state); +void SkeletonModel::maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, int index) { + _owningAvatar->getHead()->getFaceModel().maybeUpdateEyeRotation(this, parentState, joint, index); } void SkeletonModel::renderJointConstraints(gpu::Batch& batch, int jointIndex) { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + if (jointIndex == -1 || jointIndex >= _rig->getJointStateCount()) { return; } const FBXGeometry& geometry = _geometry->getFBXGeometry(); @@ -331,9 +334,11 @@ void SkeletonModel::renderJointConstraints(gpu::Batch& batch, int jointIndex) { batch._glLineWidth(3.0f); do { const FBXJoint& joint = geometry.joints.at(jointIndex); - const JointState& jointState = _jointStates.at(jointIndex); + const JointState& jointState = _rig->getJointState(jointIndex); glm::vec3 position = _rotation * jointState.getPosition() + _translation; - glm::quat parentRotation = (joint.parentIndex == -1) ? _rotation : _rotation * _jointStates.at(joint.parentIndex).getRotation(); + glm::quat parentRotation = (joint.parentIndex == -1) ? + _rotation : + _rotation * _rig->getJointState(joint.parentIndex).getRotation(); float fanScale = directionSize * 0.75f; Transform transform = Transform(); @@ -464,14 +469,11 @@ void SkeletonModel::setHandPosition(int jointIndex, const glm::vec3& position, c glm::vec3 forwardVector(rightHand ? -1.0f : 1.0f, 0.0f, 0.0f); glm::quat shoulderRotation = rotationBetween(forwardVector, elbowPosition - shoulderPosition); - JointState& shoulderState = _jointStates[shoulderJointIndex]; - shoulderState.setRotationInBindFrame(shoulderRotation, PALM_PRIORITY); - - JointState& elbowState = _jointStates[elbowJointIndex]; - elbowState.setRotationInBindFrame(rotationBetween(shoulderRotation * forwardVector, wristPosition - elbowPosition) * shoulderRotation, PALM_PRIORITY); - - JointState& handState = _jointStates[jointIndex]; - handState.setRotationInBindFrame(rotation, PALM_PRIORITY); + _rig->setJointRotationInBindFrame(shoulderJointIndex, shoulderRotation, PALM_PRIORITY); + _rig->setJointRotationInBindFrame(elbowJointIndex, + rotationBetween(shoulderRotation * forwardVector, wristPosition - elbowPosition) * + shoulderRotation, PALM_PRIORITY); + _rig->setJointRotationInBindFrame(jointIndex, rotation, PALM_PRIORITY); } bool SkeletonModel::getLeftHandPosition(glm::vec3& position) const { @@ -526,7 +528,7 @@ bool SkeletonModel::getNeckParentRotationFromDefaultOrientation(glm::quat& neckP glm::quat worldFrameRotation; bool success = getJointRotationInWorldFrame(parentIndex, worldFrameRotation); if (success) { - neckParentRotation = worldFrameRotation * _jointStates[parentIndex].getFBXJoint().inverseDefaultRotation; + neckParentRotation = worldFrameRotation * _rig->getJointState(parentIndex).getFBXJoint().inverseDefaultRotation; } return success; } @@ -636,7 +638,7 @@ float VERY_BIG_MASS = 1.0e6f; // virtual void SkeletonModel::buildShapes() { - if (_geometry == NULL || _jointStates.isEmpty()) { + if (_geometry == NULL || _rig->jointStatesEmpty()) { return; } @@ -647,9 +649,8 @@ void SkeletonModel::buildShapes() { } float uniformScale = extractUniformScale(_scale); - const int numStates = _jointStates.size(); - for (int i = 0; i < numStates; i++) { - JointState& state = _jointStates[i]; + for (int i = 0; i < _rig->getJointStateCount(); i++) { + const JointState& state = _rig->getJointState(i); const FBXJoint& joint = state.getFBXJoint(); float radius = uniformScale * joint.boneRadius; float halfHeight = 0.5f * uniformScale * joint.distanceToParent; @@ -683,7 +684,7 @@ void SkeletonModel::buildShapes() { void SkeletonModel::computeBoundingShape(const FBXGeometry& geometry) { // compute default joint transforms - int numStates = _jointStates.size(); + int numStates = _rig->getJointStateCount(); assert(numStates == _shapes.size()); QVector transforms; transforms.fill(glm::mat4(), numStates); @@ -694,11 +695,11 @@ void SkeletonModel::computeBoundingShape(const FBXGeometry& geometry) { totalExtents.addPoint(glm::vec3(0.0f)); for (int i = 0; i < numStates; i++) { // compute the default transform of this joint - JointState& state = _jointStates[i]; + const JointState& state = _rig->getJointState(i); const FBXJoint& joint = state.getFBXJoint(); int parentIndex = joint.parentIndex; if (parentIndex == -1) { - transforms[i] = _jointStates[i].getTransform(); + transforms[i] = _rig->getJointTransform(i); } else { glm::quat modifiedRotation = joint.preRotation * joint.rotation * joint.postRotation; transforms[i] = transforms[parentIndex] * glm::translate(joint.translation) @@ -748,7 +749,7 @@ void SkeletonModel::computeBoundingShape(const FBXGeometry& geometry) { _boundingShape.setRadius(capsuleRadius); _boundingShape.setHalfHeight(0.5f * diagonal.y - capsuleRadius); - glm::vec3 rootPosition = _jointStates[geometry.rootJointIndex].getPosition(); + glm::vec3 rootPosition = _rig->getJointState(geometry.rootJointIndex).getPosition(); _boundingShapeLocalOffset = 0.5f * (totalExtents.maximum + totalExtents.minimum) - rootPosition; _boundingRadius = 0.5f * glm::length(diagonal); } @@ -853,7 +854,7 @@ void SkeletonModel::cauterizeHead() { if (isActive()) { const FBXGeometry& geometry = _geometry->getFBXGeometry(); const int neckJointIndex = geometry.neckJointIndex; - if (neckJointIndex > 0 && neckJointIndex < _jointStates.size()) { + if (neckJointIndex > 0 && neckJointIndex < _rig->getJointStateCount()) { // lazy init of headBones if (_headBones.size() == 0) { @@ -861,13 +862,13 @@ void SkeletonModel::cauterizeHead() { } // preserve the translation for the neck - glm::vec4 trans = _jointStates[neckJointIndex].getTransform()[3]; + // glm::vec4 trans = _jointStates[neckJointIndex].getTransform()[3]; + glm::vec4 trans = _rig->getJointTransform(neckJointIndex)[3]; glm::vec4 zero(0, 0, 0, 0); for (const int &i : _headBones) { - JointState& joint = _jointStates[i]; glm::mat4 newXform(zero, zero, zero, trans); - joint.setTransform(newXform); - joint.setVisibleTransform(newXform); + _rig->setJointTransform(i, newXform); + _rig->setJointVisibleTransform(i, newXform); } } } diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 6d33b3da7b..c4cd43b4df 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -134,9 +134,9 @@ protected: /// Updates the state of the joint at the specified index. virtual void updateJointState(int index); - void maybeUpdateLeanRotation(const JointState& parentState, JointState& state); - void maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, JointState& state); - void maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, JointState& state); + void maybeUpdateLeanRotation(const JointState& parentState, int index); + void maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, int index); + void maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, int index); void cauterizeHead(); void initHeadBones(); diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index ed15e57d43..78091cd1a6 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -14,7 +14,7 @@ #include "Application.h" ModelOverlay::ModelOverlay() - : _model(), + : _model(nullptr), _modelTextures(QVariantMap()), _updateModel(false) { @@ -24,7 +24,7 @@ ModelOverlay::ModelOverlay() ModelOverlay::ModelOverlay(const ModelOverlay* modelOverlay) : Volume3DOverlay(modelOverlay), - _model(), + _model(nullptr), _modelTextures(QVariantMap()), _url(modelOverlay->_url), _updateModel(false) diff --git a/libraries/animation/src/AnimationHandle.cpp b/libraries/animation/src/AnimationHandle.cpp index 7f45fb600e..d11dcacfc6 100644 --- a/libraries/animation/src/AnimationHandle.cpp +++ b/libraries/animation/src/AnimationHandle.cpp @@ -159,15 +159,15 @@ void AnimationHandle::applyFrame(float frameIndex) { const FBXAnimationFrame& floorFrame = animationGeometry.animationFrames.at((int)glm::floor(frameIndex) % frameCount); const FBXAnimationFrame& ceilFrame = animationGeometry.animationFrames.at((int)glm::ceil(frameIndex) % frameCount); float frameFraction = glm::fract(frameIndex); - QVector jointStates = _rig->getJointStates(); + assert(_rig->getJointStateCount() >= _jointMappings.size()); for (int i = 0; i < _jointMappings.size(); i++) { int mapping = _jointMappings.at(i); if (mapping != -1) { - JointState& state = jointStates[mapping]; - state.setRotationInConstrainedFrame(safeMix(floorFrame.rotations.at(i), - ceilFrame.rotations.at(i), - frameFraction), - _priority); + _rig->setJointRotationInConstrainedFrame(mapping, + safeMix(floorFrame.rotations.at(i), + ceilFrame.rotations.at(i), + frameFraction), + _priority); } } } @@ -176,10 +176,9 @@ void AnimationHandle::replaceMatchingPriorities(float newPriority) { for (int i = 0; i < _jointMappings.size(); i++) { int mapping = _jointMappings.at(i); if (mapping != -1) { - QVector jointStates = _rig->getJointStates(); - JointState& state = jointStates[mapping]; + JointState state = _rig->getJointState(mapping); if (_priority == state._animationPriority) { - state._animationPriority = newPriority; + _rig->setJointAnimatinoPriority(mapping, newPriority); } } } @@ -189,9 +188,8 @@ void AnimationHandle::restoreJoints() { for (int i = 0; i < _jointMappings.size(); i++) { int mapping = _jointMappings.at(i); if (mapping != -1) { - QVector jointStates = _rig->getJointStates(); - JointState& state = jointStates[mapping]; - state.restoreRotation(1.0f, state._animationPriority); + JointState state = _rig->getJointState(mapping); + _rig->restoreJointRotation(mapping, 1.0f, state._animationPriority); } } } diff --git a/libraries/animation/src/AvatarRig.cpp b/libraries/animation/src/AvatarRig.cpp new file mode 100644 index 0000000000..cf05e61cdb --- /dev/null +++ b/libraries/animation/src/AvatarRig.cpp @@ -0,0 +1,51 @@ +// +// AvatarRig.cpp +// libraries/animation/src/ +// +// Created by SethAlves on 2015-7-22. +// 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 "AvatarRig.h" + +/// Updates the state of the joint at the specified index. +void AvatarRig::updateJointState(int index, glm::mat4 parentTransform) { + if (index < 0 && index >= _jointStates.size()) { + return; // bail + } + JointState& state = _jointStates[index]; + const FBXJoint& joint = state.getFBXJoint(); + + // compute model transforms + int parentIndex = joint.parentIndex; + if (parentIndex == -1) { + state.computeTransform(parentTransform); + } else { + // guard against out-of-bounds access to _jointStates + if (joint.parentIndex >= 0 && joint.parentIndex < _jointStates.size()) { + const JointState& parentState = _jointStates.at(parentIndex); + state.computeTransform(parentState.getTransform(), parentState.getTransformChanged()); + } + } +} + + +void AvatarRig::updateFaceJointState(int index, glm::mat4 parentTransform) { + JointState& state = _jointStates[index]; + const FBXJoint& joint = state.getFBXJoint(); + + // compute model transforms + int parentIndex = joint.parentIndex; + if (parentIndex == -1) { + state.computeTransform(parentTransform); + } else { + // guard against out-of-bounds access to _jointStates + if (joint.parentIndex >= 0 && joint.parentIndex < _jointStates.size()) { + const JointState& parentState = _jointStates.at(parentIndex); + state.computeTransform(parentState.getTransform(), parentState.getTransformChanged()); + } + } +} diff --git a/libraries/animation/src/AvatarRig.h b/libraries/animation/src/AvatarRig.h new file mode 100644 index 0000000000..dbffd8aa45 --- /dev/null +++ b/libraries/animation/src/AvatarRig.h @@ -0,0 +1,28 @@ +// +// AvatarRig.h +// libraries/animation/src/ +// +// Created by SethAlves on 2015-7-22. +// 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_AvatarRig_h +#define hifi_AvatarRig_h + +#include + +#include "Rig.h" + +class AvatarRig : public Rig { + Q_OBJECT + + public: + ~AvatarRig() {} + virtual void updateJointState(int index, glm::mat4 parentTransform); + virtual void updateFaceJointState(int index, glm::mat4 parentTransform); +}; + +#endif // hifi_AvatarRig_h diff --git a/libraries/animation/src/EntityRig.cpp b/libraries/animation/src/EntityRig.cpp new file mode 100644 index 0000000000..5ed1799671 --- /dev/null +++ b/libraries/animation/src/EntityRig.cpp @@ -0,0 +1,30 @@ +// +// EntityRig.cpp +// libraries/animation/src/ +// +// Created by SethAlves on 2015-7-22. +// 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 "EntityRig.h" + +/// Updates the state of the joint at the specified index. +void EntityRig::updateJointState(int index, glm::mat4 parentTransform) { + JointState& state = _jointStates[index]; + const FBXJoint& joint = state.getFBXJoint(); + + // compute model transforms + int parentIndex = joint.parentIndex; + if (parentIndex == -1) { + state.computeTransform(parentTransform); + } else { + // guard against out-of-bounds access to _jointStates + if (joint.parentIndex >= 0 && joint.parentIndex < _jointStates.size()) { + const JointState& parentState = _jointStates.at(parentIndex); + state.computeTransform(parentState.getTransform(), parentState.getTransformChanged()); + } + } +} diff --git a/libraries/animation/src/EntityRig.h b/libraries/animation/src/EntityRig.h new file mode 100644 index 0000000000..aa6a5fbd2b --- /dev/null +++ b/libraries/animation/src/EntityRig.h @@ -0,0 +1,28 @@ +// +// EntityRig.h +// libraries/animation/src/ +// +// Created by SethAlves on 2015-7-22. +// 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_EntityRig_h +#define hifi_EntityRig_h + +#include + +#include "Rig.h" + +class EntityRig : public Rig { + Q_OBJECT + + public: + ~EntityRig() {} + virtual void updateJointState(int index, glm::mat4 parentTransform); + virtual void updateFaceJointState(int index, glm::mat4 parentTransform) { } +}; + +#endif // hifi_EntityRig_h diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 5e322e420f..12bfd606da 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -13,11 +13,7 @@ #include "Rig.h" -bool Rig::removeRunningAnimation(AnimationHandlePointer animationHandle) { - return _runningAnimations.removeOne(animationHandle); -} - -static void insertSorted(QList& handles, const AnimationHandlePointer& handle) { +void insertSorted(QList& handles, const AnimationHandlePointer& handle) { for (QList::iterator it = handles.begin(); it != handles.end(); it++) { if (handle->getPriority() > (*it)->getPriority()) { handles.insert(it, handle); @@ -27,17 +23,27 @@ static void insertSorted(QList& handles, const Animation handles.append(handle); } +AnimationHandlePointer Rig::createAnimationHandle() { + AnimationHandlePointer handle(new AnimationHandle(getRigPointer())); + _animationHandles.insert(handle); + return handle; +} + +bool Rig::removeRunningAnimation(AnimationHandlePointer animationHandle) { + return _runningAnimations.removeOne(animationHandle); +} + void Rig::addRunningAnimation(AnimationHandlePointer animationHandle) { - insertSorted(_runningAnimations, animationHandle); + insertSorted(_runningAnimations, animationHandle); } bool Rig::isRunningAnimation(AnimationHandlePointer animationHandle) { return _runningAnimations.contains(animationHandle); } -float Rig::initJointStates(glm::vec3 scale, glm::vec3 offset, QVector states) { +float Rig::initJointStates(QVector states, glm::mat4 parentTransform) { _jointStates = states; - initJointTransforms(scale, offset); + initJointTransforms(parentTransform); int numStates = _jointStates.size(); float radius = 0.0f; @@ -52,11 +58,11 @@ float Rig::initJointStates(glm::vec3 scale, glm::vec3 offset, QVectorgetFBXGeometry(); - // NOTE: in practice geometry.offset has a non-unity scale (rather than a translation) - glm::mat4 parentTransform = glm::scale(scale) * glm::translate(offset); // * geometry.offset; XXX state.initTransform(parentTransform); } else { const JointState& parentState = _jointStates.at(parentIndex); @@ -75,19 +78,30 @@ void Rig::initJointTransforms(glm::vec3 scale, glm::vec3 offset) { } } -void Rig::resetJoints() { +void Rig::clearJointTransformTranslation(int jointIndex) { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + return; + } + _jointStates[jointIndex].clearTransformTranslation(); +} + +void Rig::reset(const QVector& fbxJoints) { if (_jointStates.isEmpty()) { return; } - - // const FBXGeometry& geometry = _geometry->getFBXGeometry(); for (int i = 0; i < _jointStates.size(); i++) { - const FBXJoint& fbxJoint = _jointStates[i].getFBXJoint(); - _jointStates[i].setRotationInConstrainedFrame(fbxJoint.rotation, 0.0f); + _jointStates[i].setRotationInConstrainedFrame(fbxJoints.at(i).rotation, 0.0f); } } -bool Rig::getJointState(int index, glm::quat& rotation) const { +JointState Rig::getJointState(int jointIndex) const { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + return JointState(); + } + return _jointStates[jointIndex]; +} + +bool Rig::getJointStateRotation(int index, glm::quat& rotation) const { if (index == -1 || index >= _jointStates.size()) { return false; } @@ -105,12 +119,6 @@ bool Rig::getVisibleJointState(int index, glm::quat& rotation) const { return !state.rotationIsDefault(rotation); } -void Rig::updateVisibleJointStates() { - for (int i = 0; i < _jointStates.size(); i++) { - _jointStates[i].slaveVisibleTransform(); - } -} - void Rig::clearJointState(int index) { if (index != -1 && index < _jointStates.size()) { JointState& state = _jointStates[index]; @@ -122,6 +130,18 @@ void Rig::clearJointStates() { _jointStates.clear(); } +void Rig::clearJointAnimationPriority(int index) { + if (index != -1 && index < _jointStates.size()) { + _jointStates[index]._animationPriority = 0.0f; + } +} + +void Rig::setJointAnimatinoPriority(int index, float newPriority) { + if (index != -1 && index < _jointStates.size()) { + _jointStates[index]._animationPriority = newPriority; + } +} + void Rig::setJointState(int index, bool valid, const glm::quat& rotation, float priority) { if (index != -1 && index < _jointStates.size()) { JointState& state = _jointStates[index]; @@ -133,99 +153,103 @@ void Rig::setJointState(int index, bool valid, const glm::quat& rotation, float } } - -void Rig::clearJointAnimationPriority(int index) { +void Rig::restoreJointRotation(int index, float fraction, float priority) { if (index != -1 && index < _jointStates.size()) { - _jointStates[index]._animationPriority = 0.0f; + _jointStates[index].restoreRotation(fraction, priority); } } -AnimationHandlePointer Rig::createAnimationHandle() { - AnimationHandlePointer handle(new AnimationHandle(getRigPointer())); - _animationHandles.insert(handle); - return handle; -} - -bool Rig::getJointStateAtIndex(int jointIndex, JointState& jointState) const { +bool Rig::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, + glm::vec3 translation, glm::quat rotation) const { if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return false; } - jointState = _jointStates[jointIndex]; + // position is in world-frame + position = translation + rotation * _jointStates[jointIndex].getPosition(); return true; } -void Rig::updateJointStates(glm::mat4 parentTransform) { +bool Rig::getJointPosition(int jointIndex, glm::vec3& position) const { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + return false; + } + // position is in model-frame + position = extractTranslation(_jointStates[jointIndex].getTransform()); + return true; +} + +bool Rig::getJointRotationInWorldFrame(int jointIndex, glm::quat& result, const glm::quat& rotation) const { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + return false; + } + result = rotation * _jointStates[jointIndex].getRotation(); + return true; +} + +bool Rig::getJointRotation(int jointIndex, glm::quat& rotation) const { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + return false; + } + rotation = _jointStates[jointIndex].getRotation(); + return true; +} + +bool Rig::getJointCombinedRotation(int jointIndex, glm::quat& result, const glm::quat& rotation) const { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + return false; + } + result = rotation * _jointStates[jointIndex].getRotation(); + return true; +} + + +bool Rig::getVisibleJointPositionInWorldFrame(int jointIndex, glm::vec3& position, + glm::vec3 translation, glm::quat rotation) const { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + return false; + } + // position is in world-frame + position = translation + rotation * _jointStates[jointIndex].getVisiblePosition(); + return true; +} + +bool Rig::getVisibleJointRotationInWorldFrame(int jointIndex, glm::quat& result, glm::quat rotation) const { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + return false; + } + result = rotation * _jointStates[jointIndex].getVisibleRotation(); + return true; +} + +glm::mat4 Rig::getJointTransform(int jointIndex) const { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + return glm::mat4(); + } + return _jointStates[jointIndex].getTransform(); +} + +glm::mat4 Rig::getJointVisibleTransform(int jointIndex) const { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + return glm::mat4(); + } + return _jointStates[jointIndex].getVisibleTransform(); +} + +void Rig::simulateInternal(glm::mat4 parentTransform) { for (int i = 0; i < _jointStates.size(); i++) { updateJointState(i, parentTransform); } -} - -void Rig::updateJointState(int index, glm::mat4 parentTransform) { - JointState& state = _jointStates[index]; - const FBXJoint& joint = state.getFBXJoint(); - - // compute model transforms - int parentIndex = joint.parentIndex; - if (parentIndex == -1) { - // glm::mat4 parentTransform = glm::scale(scale) * glm::translate(offset) * geometryOffset; - state.computeTransform(parentTransform); - } else { - // guard against out-of-bounds access to _jointStates - if (joint.parentIndex >= 0 && joint.parentIndex < _jointStates.size()) { - const JointState& parentState = _jointStates.at(parentIndex); - state.computeTransform(parentState.getTransform(), parentState.getTransformChanged()); - } - } -} - -void Rig::resetAllTransformsChanged() { for (int i = 0; i < _jointStates.size(); i++) { _jointStates[i].resetTransformChanged(); } } -glm::quat Rig::setJointRotationInBindFrame(int jointIndex, const glm::quat& rotation, float priority, bool constrain) { - glm::quat endRotation; - if (jointIndex == -1 || _jointStates.isEmpty()) { - return endRotation; - } - JointState& state = _jointStates[jointIndex]; - state.setRotationInBindFrame(rotation, priority, constrain); - endRotation = state.getRotationInBindFrame(); - return endRotation; -} - -glm::quat Rig::setJointRotationInConstrainedFrame(int jointIndex, glm::quat targetRotation, float priority, bool constrain) { - glm::quat endRotation; - if (jointIndex == -1 || _jointStates.isEmpty()) { - return endRotation; - } - JointState& state = _jointStates[jointIndex]; - state.setRotationInConstrainedFrame(targetRotation, priority, constrain); - endRotation = state.getRotationInConstrainedFrame(); - return endRotation; -} - -void Rig::applyJointRotationDelta(int jointIndex, const glm::quat& delta, bool constrain, float priority) { - if (jointIndex == -1 || _jointStates.isEmpty()) { - return; - } - _jointStates[jointIndex].applyRotationDelta(delta, constrain, priority); -} - -bool Rig::setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, - bool useRotation, int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, - float priority, glm::mat4 parentTransform) { +bool Rig::setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, bool useRotation, + int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, float priority, + const QVector& freeLineage, glm::mat4 parentTransform) { if (jointIndex == -1 || _jointStates.isEmpty()) { return false; } - - // const FBXGeometry& geometry = _geometry->getFBXGeometry(); - // const QVector& freeLineage = geometry.joints.at(jointIndex).freeLineage; - const FBXJoint& fbxJoint = _jointStates[jointIndex].getFBXJoint(); - const QVector& freeLineage = fbxJoint.freeLineage; - - if (freeLineage.isEmpty()) { return false; } @@ -304,19 +328,14 @@ bool Rig::setJointPosition(int jointIndex, const glm::vec3& position, const glm: return true; } -void Rig::inverseKinematics(int endIndex, glm::vec3 targetPosition, - const glm::quat& targetRotation, float priority, glm::mat4 parentTransform) { +void Rig::inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::quat& targetRotation, float priority, + const QVector& freeLineage, glm::mat4 parentTransform) { // NOTE: targetRotation is from bind- to model-frame if (endIndex == -1 || _jointStates.isEmpty()) { return; } - // const FBXGeometry& geometry = _geometry->getFBXGeometry(); - // const QVector& freeLineage = geometry.joints.at(endIndex).freeLineage; - const FBXJoint& fbxJoint = _jointStates[endIndex].getFBXJoint(); - const QVector& freeLineage = fbxJoint.freeLineage; - if (freeLineage.isEmpty()) { return; } @@ -404,11 +423,11 @@ void Rig::inverseKinematics(int endIndex, glm::vec3 targetPosition, } // recompute transforms from the top down - glm::mat4 parentTransform = topParentTransform; + glm::mat4 currentParentTransform = topParentTransform; for (int j = numFree - 1; j >= 0; --j) { JointState& freeState = _jointStates[freeLineage.at(j)]; - freeState.computeTransform(parentTransform); - parentTransform = freeState.getTransform(); + freeState.computeTransform(currentParentTransform); + currentParentTransform = freeState.getTransform(); } // measure our success @@ -420,14 +439,10 @@ void Rig::inverseKinematics(int endIndex, glm::vec3 targetPosition, endState.setRotationInBindFrame(targetRotation, priority, true); } -bool Rig::restoreJointPosition(int jointIndex, float fraction, float priority) { +bool Rig::restoreJointPosition(int jointIndex, float fraction, float priority, const QVector& freeLineage) { if (jointIndex == -1 || _jointStates.isEmpty()) { return false; } - // const FBXGeometry& geometry = _geometry->getFBXGeometry(); - // const QVector& freeLineage = geometry.joints.at(jointIndex).freeLineage; - const FBXJoint& fbxJoint = _jointStates[jointIndex].getFBXJoint(); - const QVector& freeLineage = fbxJoint.freeLineage; foreach (int index, freeLineage) { JointState& state = _jointStates[index]; @@ -436,22 +451,78 @@ bool Rig::restoreJointPosition(int jointIndex, float fraction, float priority) { return true; } -float Rig::getLimbLength(int jointIndex, glm::vec3 scale) const { +float Rig::getLimbLength(int jointIndex, const QVector& freeLineage, + const glm::vec3 scale, const QVector& fbxJoints) const { if (jointIndex == -1 || _jointStates.isEmpty()) { return 0.0f; } - - // const FBXGeometry& geometry = _geometry->getFBXGeometry(); - // const QVector& freeLineage = geometry.joints.at(jointIndex).freeLineage; - const FBXJoint& fbxJoint = _jointStates[jointIndex].getFBXJoint(); - const QVector& freeLineage = fbxJoint.freeLineage; - float length = 0.0f; float lengthScale = (scale.x + scale.y + scale.z) / 3.0f; for (int i = freeLineage.size() - 2; i >= 0; i--) { - int something = freeLineage.at(i); - const FBXJoint& fbxJointI = _jointStates[something].getFBXJoint(); - length += fbxJointI.distanceToParent * lengthScale; + length += fbxJoints.at(freeLineage.at(i)).distanceToParent * lengthScale; } return length; } + +glm::quat Rig::setJointRotationInBindFrame(int jointIndex, const glm::quat& rotation, float priority, bool constrain) { + glm::quat endRotation; + if (jointIndex == -1 || _jointStates.isEmpty()) { + return endRotation; + } + JointState& state = _jointStates[jointIndex]; + state.setRotationInBindFrame(rotation, priority, constrain); + endRotation = state.getRotationInBindFrame(); + return endRotation; +} + +glm::vec3 Rig::getJointDefaultTranslationInConstrainedFrame(int jointIndex) { + if (jointIndex == -1 || _jointStates.isEmpty()) { + return glm::vec3(); + } + return _jointStates[jointIndex].getDefaultTranslationInConstrainedFrame(); +} + +glm::quat Rig::setJointRotationInConstrainedFrame(int jointIndex, glm::quat targetRotation, float priority, bool constrain) { + glm::quat endRotation; + if (jointIndex == -1 || _jointStates.isEmpty()) { + return endRotation; + } + JointState& state = _jointStates[jointIndex]; + state.setRotationInConstrainedFrame(targetRotation, priority, constrain); + endRotation = state.getRotationInConstrainedFrame(); + return endRotation; +} + +void Rig::updateVisibleJointStates() { + for (int i = 0; i < _jointStates.size(); i++) { + _jointStates[i].slaveVisibleTransform(); + } +} + +void Rig::setJointTransform(int jointIndex, glm::mat4 newTransform) { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + return; + } + _jointStates[jointIndex].setTransform(newTransform); +} + +void Rig::setJointVisibleTransform(int jointIndex, glm::mat4 newTransform) { + if (jointIndex == -1 || jointIndex >= _jointStates.size()) { + return; + } + _jointStates[jointIndex].setVisibleTransform(newTransform); +} + +void Rig::applyJointRotationDelta(int jointIndex, const glm::quat& delta, bool constrain, float priority) { + if (jointIndex == -1 || _jointStates.isEmpty()) { + return; + } + _jointStates[jointIndex].applyRotationDelta(delta, constrain, priority); +} + +glm::quat Rig::getJointDefaultRotationInParentFrame(int jointIndex) { + if (jointIndex == -1 || _jointStates.isEmpty()) { + return glm::quat(); + } + return _jointStates[jointIndex].getDefaultRotationInParentFrame(); +} diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 89ea99f162..87b0c7504e 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -11,10 +11,10 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // /* TBD: - - What is responsibilities of Animation/AnimationPointer/AnimationCache/AnimationDetails? Is there common/copied code (e.g., ScriptableAvatar::update)? + - What is responsibilities of Animation/AnimationPointer/AnimationCache/AnimationDetails? Is there common/copied code (e.g., ScriptableAvatar::update)? - How do attachments interact with the physics of the attached entity? E.g., do hand joints need to reflect held object physics? - Is there any current need (i.e., for initial campatability) to have multiple animations per role (e.g., idle) with the system choosing randomly? - + - Distribute some doc from here to the right files if it turns out to be correct: - AnimationDetails is a script-useable copy of animation state, analogous to EntityItemProperties, but without anything equivalent to editEntity. */ @@ -37,49 +37,69 @@ typedef std::shared_ptr RigPointer; class Rig : public QObject, public std::enable_shared_from_this { public: + + virtual ~Rig() {} + RigPointer getRigPointer() { return shared_from_this(); } + AnimationHandlePointer createAnimationHandle(); bool removeRunningAnimation(AnimationHandlePointer animationHandle); void addRunningAnimation(AnimationHandlePointer animationHandle); bool isRunningAnimation(AnimationHandlePointer animationHandle); const QList& getRunningAnimations() const { return _runningAnimations; } - float initJointStates(glm::vec3 scale, glm::vec3 offset, QVector states); - void initJointTransforms(glm::vec3 scale, glm::vec3 offset); - void resetJoints(); + float initJointStates(QVector states, glm::mat4 parentTransform); bool jointStatesEmpty() { return _jointStates.isEmpty(); }; - int jointStateCount() const { return _jointStates.size(); } - bool getJointStateAtIndex(int jointIndex, JointState& jointState) const; + int getJointStateCount() const { return _jointStates.size(); } - void updateJointStates(glm::mat4 parentTransform); - void updateJointState(int index, glm::mat4 parentTransform); - void resetAllTransformsChanged(); - - bool getJointState(int index, glm::quat& rotation) const; + void initJointTransforms(glm::mat4 parentTransform); + void clearJointTransformTranslation(int jointIndex); + void reset(const QVector& fbxJoints); + bool getJointStateRotation(int index, glm::quat& rotation) const; + void applyJointRotationDelta(int jointIndex, const glm::quat& delta, bool constrain, float priority); + JointState getJointState(int jointIndex) const; bool getVisibleJointState(int index, glm::quat& rotation) const; - void updateVisibleJointStates(); void clearJointState(int index); void clearJointStates(); - void setJointState(int index, bool valid, const glm::quat& rotation, float priority); void clearJointAnimationPriority(int index); + void setJointAnimatinoPriority(int index, float newPriority); + void setJointState(int index, bool valid, const glm::quat& rotation, float priority); + void restoreJointRotation(int index, float fraction, float priority); + bool getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, + glm::vec3 translation, glm::quat rotation) const; + + bool getJointPosition(int jointIndex, glm::vec3& position) const; + bool getJointRotationInWorldFrame(int jointIndex, glm::quat& result, const glm::quat& rotation) const; + bool getJointRotation(int jointIndex, glm::quat& rotation) const; + bool getJointCombinedRotation(int jointIndex, glm::quat& result, const glm::quat& rotation) const; + bool getVisibleJointPositionInWorldFrame(int jointIndex, glm::vec3& position, + glm::vec3 translation, glm::quat rotation) const; + bool getVisibleJointRotationInWorldFrame(int jointIndex, glm::quat& result, glm::quat rotation) const; + glm::mat4 getJointTransform(int jointIndex) const; + void setJointTransform(int jointIndex, glm::mat4 newTransform); + glm::mat4 getJointVisibleTransform(int jointIndex) const; + void setJointVisibleTransform(int jointIndex, glm::mat4 newTransform); + void simulateInternal(glm::mat4 parentTransform); + bool setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, bool useRotation, + int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, float priority, + const QVector& freeLineage, glm::mat4 parentTransform); + void inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::quat& targetRotation, float priority, + const QVector& freeLineage, glm::mat4 parentTransform); + bool restoreJointPosition(int jointIndex, float fraction, float priority, const QVector& freeLineage); + float getLimbLength(int jointIndex, const QVector& freeLineage, + const glm::vec3 scale, const QVector& fbxJoints) const; + glm::quat setJointRotationInBindFrame(int jointIndex, const glm::quat& rotation, float priority, bool constrain = false); + glm::vec3 getJointDefaultTranslationInConstrainedFrame(int jointIndex); glm::quat setJointRotationInConstrainedFrame(int jointIndex, glm::quat targetRotation, float priority, bool constrain = false); - void applyJointRotationDelta(int jointIndex, const glm::quat& delta, bool constrain, float priority); + glm::quat getJointDefaultRotationInParentFrame(int jointIndex); + void updateVisibleJointStates(); - QVector getJointStates() { return _jointStates; } + virtual void updateJointState(int index, glm::mat4 parentTransform) = 0; + virtual void updateFaceJointState(int index, glm::mat4 parentTransform) = 0; - AnimationHandlePointer createAnimationHandle(); - - bool setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, - bool useRotation, int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, - float priority, glm::mat4 parentTransform); - void inverseKinematics(int endIndex, glm::vec3 targetPosition, - const glm::quat& targetRotation, float priority, glm::mat4 parentTransform); - bool restoreJointPosition(int jointIndex, float fraction, float priority); - float getLimbLength(int jointIndex, glm::vec3 scale) const; - -protected: + protected: QVector _jointStates; QSet _animationHandles; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index adc6ae9de2..2fcdd7f664 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -40,6 +40,7 @@ #include "RenderablePolyVoxEntityItem.h" #include "EntitiesRendererLogging.h" #include "AddressManager.h" +#include "EntityRig.h" EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterface* viewState, AbstractScriptingServicesInterface* scriptingServices) : @@ -695,7 +696,7 @@ Model* EntityTreeRenderer::allocateModel(const QString& url, const QString& coll return model; } - model = new Model(); + model = new Model(std::make_shared()); model->init(); model->setURL(QUrl(url)); model->setCollisionModelURL(QUrl(collisionUrl)); @@ -728,7 +729,7 @@ Model* EntityTreeRenderer::updateModel(Model* original, const QString& newUrl, c } // create the model and correctly initialize it with the new url - model = new Model(); + model = new Model(std::make_shared()); model->init(); model->setURL(QUrl(newUrl)); model->setCollisionModelURL(QUrl(collisionUrl)); diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 2e03266253..bb0a35f7b0 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -67,7 +67,7 @@ int RenderableZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned ch } Model* RenderableZoneEntityItem::getModel() { - Model* model = new Model(); + Model* model = new Model(nullptr); model->setIsWireframe(true); model->init(); return model; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 5a6908fc4a..af60c91664 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -60,7 +60,7 @@ static int weakNetworkGeometryPointerTypeId = qRegisterMetaType >(); float Model::FAKE_DIMENSION_PLACEHOLDER = -1.0f; -Model::Model(QObject* parent, RigPointer rig) : +Model::Model(RigPointer rig, QObject* parent) : QObject(parent), _scale(1.0f, 1.0f, 1.0f), _scaleToFit(false), @@ -87,7 +87,7 @@ Model::Model(QObject* parent, RigPointer rig) : if (_viewState) { moveToThread(_viewState->getMainThread()); } - + setSnapModelToRegistrationPoint(true, glm::vec3(0.5f)); } @@ -252,22 +252,9 @@ QVector Model::createJointStates(const FBXGeometry& geometry) { }; void Model::initJointTransforms() { - // compute model transforms - int numStates = _jointStates.size(); - for (int i = 0; i < numStates; ++i) { - JointState& state = _jointStates[i]; - const FBXJoint& joint = state.getFBXJoint(); - int parentIndex = joint.parentIndex; - if (parentIndex == -1) { - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - // NOTE: in practice geometry.offset has a non-unity scale (rather than a translation) - glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - state.initTransform(parentTransform); - } else { - const JointState& parentState = _jointStates.at(parentIndex); - state.initTransform(parentState.getTransform()); - } - } + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; + _rig->initJointTransforms(parentTransform); } void Model::init() { @@ -396,14 +383,8 @@ void Model::init() { } void Model::reset() { - if (_jointStates.isEmpty()) { - return; - } const FBXGeometry& geometry = _geometry->getFBXGeometry(); - for (int i = 0; i < _jointStates.size(); i++) { - _jointStates[i].setRotationInConstrainedFrame(geometry.joints.at(i).rotation, 0.0f); - } - + _rig->reset(geometry.joints); _meshGroupsKnown = false; _readyWhenAdded = false; // in case any of our users are using scenes invalidCalculatedMeshBoxes(); // if we have to reload, we need to assume our mesh boxes are all invalid @@ -436,28 +417,30 @@ bool Model::updateGeometry() { const FBXGeometry& newGeometry = geometry->getFBXGeometry(); QVector newJointStates = createJointStates(newGeometry); - if (! _jointStates.isEmpty()) { + + if (! _rig->jointStatesEmpty()) { // copy the existing joint states const FBXGeometry& oldGeometry = _geometry->getFBXGeometry(); for (QHash::const_iterator it = oldGeometry.jointIndices.constBegin(); - it != oldGeometry.jointIndices.constEnd(); it++) { + it != oldGeometry.jointIndices.constEnd(); it++) { int oldIndex = it.value() - 1; int newIndex = newGeometry.getJointIndex(it.key()); if (newIndex != -1) { - newJointStates[newIndex].copyState(_jointStates[oldIndex]); + newJointStates[newIndex].copyState(_rig->getJointState(oldIndex)); } } - } + } + deleteGeometry(); _dilatedTextures.clear(); setGeometry(geometry); - + _meshGroupsKnown = false; _readyWhenAdded = false; // in case any of our users are using scenes invalidCalculatedMeshBoxes(); // if we have to reload, we need to assume our mesh boxes are all invalid initJointStates(newJointStates); needToRebuild = true; - } else if (_jointStates.isEmpty()) { + } else if (_rig->jointStatesEmpty()) { const FBXGeometry& fbxGeometry = geometry->getFBXGeometry(); if (fbxGeometry.joints.size() > 0) { initJointStates(createJointStates(fbxGeometry)); @@ -493,22 +476,9 @@ bool Model::updateGeometry() { // virtual void Model::initJointStates(QVector states) { - _jointStates = states; - initJointTransforms(); - - int numStates = _jointStates.size(); - float radius = 0.0f; - for (int i = 0; i < numStates; ++i) { - float distance = glm::length(_jointStates[i].getPosition()); - if (distance > radius) { - radius = distance; - } - _jointStates[i].buildConstraint(); - } - for (int i = 0; i < _jointStates.size(); i++) { - _jointStates[i].slaveVisibleTransform(); - } - _boundingRadius = radius; + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; + _boundingRadius = _rig->initJointStates(states, parentTransform); } bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, @@ -1070,51 +1040,24 @@ glm::vec3 Model::calculateScaledOffsetPoint(const glm::vec3& point) const { return translatedPoint; } - bool Model::getJointState(int index, glm::quat& rotation) const { - if (index == -1 || index >= _jointStates.size()) { - return false; - } - const JointState& state = _jointStates.at(index); - rotation = state.getRotationInConstrainedFrame(); - return !state.rotationIsDefault(rotation); + return _rig->getJointStateRotation(index, rotation); } bool Model::getVisibleJointState(int index, glm::quat& rotation) const { - if (index == -1 || index >= _jointStates.size()) { - return false; - } - const JointState& state = _jointStates.at(index); - rotation = state.getVisibleRotationInConstrainedFrame(); - return !state.rotationIsDefault(rotation); + return _rig->getVisibleJointState(index, rotation); } void Model::clearJointState(int index) { - if (index != -1 && index < _jointStates.size()) { - JointState& state = _jointStates[index]; - state.setRotationInConstrainedFrame(glm::quat(), 0.0f); - } + _rig->clearJointState(index); } void Model::clearJointAnimationPriority(int index) { - if (_rig) { - _rig->clearJointAnimationPriority(index); - } else { - if (index != -1 && index < _jointStates.size()) { - _jointStates[index]._animationPriority = 0.0f; - } - } + _rig->clearJointAnimationPriority(index); } void Model::setJointState(int index, bool valid, const glm::quat& rotation, float priority) { - if (index != -1 && index < _jointStates.size()) { - JointState& state = _jointStates[index]; - if (valid) { - state.setRotationInConstrainedFrame(rotation, priority); - } else { - state.restoreRotation(1.0f, priority); - } - } + _rig->setJointState(index, valid, rotation, priority); } int Model::getParentJointIndex(int jointIndex) const { @@ -1189,62 +1132,31 @@ void Model::setCollisionModelURL(const QUrl& url) { } bool Model::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position) const { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { - return false; - } - // position is in world-frame - position = _translation + _rotation * _jointStates[jointIndex].getPosition(); - return true; + return _rig->getJointPositionInWorldFrame(jointIndex, position, _translation, _rotation); } bool Model::getJointPosition(int jointIndex, glm::vec3& position) const { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { - return false; - } - // position is in model-frame - position = extractTranslation(_jointStates[jointIndex].getTransform()); - return true; + return _rig->getJointPosition(jointIndex, position); } bool Model::getJointRotationInWorldFrame(int jointIndex, glm::quat& rotation) const { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { - return false; - } - rotation = _rotation * _jointStates[jointIndex].getRotation(); - return true; + return _rig->getJointRotationInWorldFrame(jointIndex, rotation, _rotation); } bool Model::getJointRotation(int jointIndex, glm::quat& rotation) const { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { - return false; - } - rotation = _jointStates[jointIndex].getRotation(); - return true; + return _rig->getJointRotation(jointIndex, rotation); } bool Model::getJointCombinedRotation(int jointIndex, glm::quat& rotation) const { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { - return false; - } - rotation = _rotation * _jointStates[jointIndex].getRotation(); - return true; + return _rig->getJointCombinedRotation(jointIndex, rotation, _rotation); } bool Model::getVisibleJointPositionInWorldFrame(int jointIndex, glm::vec3& position) const { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { - return false; - } - // position is in world-frame - position = _translation + _rotation * _jointStates[jointIndex].getVisiblePosition(); - return true; + return _rig->getVisibleJointPositionInWorldFrame(jointIndex, position, _translation, _rotation); } bool Model::getVisibleJointRotationInWorldFrame(int jointIndex, glm::quat& rotation) const { - if (jointIndex == -1 || jointIndex >= _jointStates.size()) { - return false; - } - rotation = _rotation * _jointStates[jointIndex].getVisibleRotation(); - return true; + return _rig->getVisibleJointRotationInWorldFrame(jointIndex, rotation, _rotation); } QStringList Model::getJointNames() const { @@ -1438,12 +1350,14 @@ void Model::updateClusterMatrices() { if (_showTrueJointTransforms) { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - state.clusterMatrices[j] = modelToWorld * _jointStates[cluster.jointIndex].getTransform() * cluster.inverseBindMatrix; + state.clusterMatrices[j] = + modelToWorld * _rig->getJointTransform(cluster.jointIndex) * cluster.inverseBindMatrix; } } else { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - state.clusterMatrices[j] = modelToWorld * _jointStates[cluster.jointIndex].getVisibleTransform() * cluster.inverseBindMatrix; + state.clusterMatrices[j] = + modelToWorld * _rig->getJointVisibleTransform(cluster.jointIndex) * cluster.inverseBindMatrix; } } } @@ -1457,16 +1371,12 @@ void Model::simulateInternal(float deltaTime) { handle->simulate(deltaTime); } - for (int i = 0; i < _jointStates.size(); i++) { - updateJointState(i); - } - for (int i = 0; i < _jointStates.size(); i++) { - _jointStates[i].resetTransformChanged(); - } + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; + _rig->simulateInternal(parentTransform); _shapesAreDirty = !_shapes.isEmpty(); - const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 modelToWorld = glm::mat4_cast(_rotation); for (int i = 0; i < _meshStates.size(); i++) { MeshState& state = _meshStates[i]; @@ -1474,12 +1384,14 @@ void Model::simulateInternal(float deltaTime) { if (_showTrueJointTransforms) { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - state.clusterMatrices[j] = modelToWorld * _jointStates[cluster.jointIndex].getTransform() * cluster.inverseBindMatrix; + state.clusterMatrices[j] = + modelToWorld * _rig->getJointTransform(cluster.jointIndex) * cluster.inverseBindMatrix; } } else { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - state.clusterMatrices[j] = modelToWorld * _jointStates[cluster.jointIndex].getVisibleTransform() * cluster.inverseBindMatrix; + state.clusterMatrices[j] = + modelToWorld * _rig->getJointVisibleTransform(cluster.jointIndex) * cluster.inverseBindMatrix; } } } @@ -1492,261 +1404,42 @@ void Model::simulateInternal(float deltaTime) { } void Model::updateJointState(int index) { - JointState& state = _jointStates[index]; - const FBXJoint& joint = state.getFBXJoint(); - - // compute model transforms - int parentIndex = joint.parentIndex; - if (parentIndex == -1) { - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - state.computeTransform(parentTransform); - } else { - // guard against out-of-bounds access to _jointStates - if (joint.parentIndex >= 0 && joint.parentIndex < _jointStates.size()) { - const JointState& parentState = _jointStates.at(parentIndex); - state.computeTransform(parentState.getTransform(), parentState.getTransformChanged()); - } - } -} - -void Model::updateVisibleJointStates() { - if (_showTrueJointTransforms) { - // no need to update visible transforms - return; - } - for (int i = 0; i < _jointStates.size(); i++) { - _jointStates[i].slaveVisibleTransform(); - } + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; + _rig->updateJointState(index, parentTransform); } bool Model::setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, bool useRotation, - int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, float priority) { - if (jointIndex == -1 || _jointStates.isEmpty()) { - return false; - } + int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, float priority) { const FBXGeometry& geometry = _geometry->getFBXGeometry(); const QVector& freeLineage = geometry.joints.at(jointIndex).freeLineage; - if (freeLineage.isEmpty()) { - return false; + glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; + if (_rig->setJointPosition(jointIndex, position, rotation, useRotation, + lastFreeIndex, allIntermediatesFree, alignment, priority, freeLineage, parentTransform)) { + _shapesAreDirty = !_shapes.isEmpty(); + return true; } - if (lastFreeIndex == -1) { - lastFreeIndex = freeLineage.last(); - } - - // this is a cyclic coordinate descent algorithm: see - // http://www.ryanjuckett.com/programming/animation/21-cyclic-coordinate-descent-in-2d - const int ITERATION_COUNT = 1; - glm::vec3 worldAlignment = alignment; - for (int i = 0; i < ITERATION_COUNT; i++) { - // first, try to rotate the end effector as close as possible to the target rotation, if any - glm::quat endRotation; - if (useRotation) { - JointState& state = _jointStates[jointIndex]; - - state.setRotationInBindFrame(rotation, priority); - endRotation = state.getRotationInBindFrame(); - } - - // then, we go from the joint upwards, rotating the end as close as possible to the target - glm::vec3 endPosition = extractTranslation(_jointStates[jointIndex].getTransform()); - for (int j = 1; freeLineage.at(j - 1) != lastFreeIndex; j++) { - int index = freeLineage.at(j); - JointState& state = _jointStates[index]; - const FBXJoint& joint = state.getFBXJoint(); - if (!(joint.isFree || allIntermediatesFree)) { - continue; - } - glm::vec3 jointPosition = extractTranslation(state.getTransform()); - glm::vec3 jointVector = endPosition - jointPosition; - glm::quat oldCombinedRotation = state.getRotation(); - glm::quat combinedDelta; - float combinedWeight; - if (useRotation) { - combinedDelta = safeMix(rotation * glm::inverse(endRotation), - rotationBetween(jointVector, position - jointPosition), 0.5f); - combinedWeight = 2.0f; - - } else { - combinedDelta = rotationBetween(jointVector, position - jointPosition); - combinedWeight = 1.0f; - } - if (alignment != glm::vec3() && j > 1) { - jointVector = endPosition - jointPosition; - glm::vec3 positionSum; - for (int k = j - 1; k > 0; k--) { - int index = freeLineage.at(k); - updateJointState(index); - positionSum += extractTranslation(_jointStates.at(index).getTransform()); - } - glm::vec3 projectedCenterOfMass = glm::cross(jointVector, - glm::cross(positionSum / (j - 1.0f) - jointPosition, jointVector)); - glm::vec3 projectedAlignment = glm::cross(jointVector, glm::cross(worldAlignment, jointVector)); - const float LENGTH_EPSILON = 0.001f; - if (glm::length(projectedCenterOfMass) > LENGTH_EPSILON && glm::length(projectedAlignment) > LENGTH_EPSILON) { - combinedDelta = safeMix(combinedDelta, rotationBetween(projectedCenterOfMass, projectedAlignment), - 1.0f / (combinedWeight + 1.0f)); - } - } - state.applyRotationDelta(combinedDelta, true, priority); - glm::quat actualDelta = state.getRotation() * glm::inverse(oldCombinedRotation); - endPosition = actualDelta * jointVector + jointPosition; - if (useRotation) { - endRotation = actualDelta * endRotation; - } - } - } - - // now update the joint states from the top - for (int j = freeLineage.size() - 1; j >= 0; j--) { - updateJointState(freeLineage.at(j)); - } - _shapesAreDirty = !_shapes.isEmpty(); - - return true; + return false; } void Model::inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::quat& targetRotation, float priority) { - // NOTE: targetRotation is from bind- to model-frame - - if (endIndex == -1 || _jointStates.isEmpty()) { - return; - } - const FBXGeometry& geometry = _geometry->getFBXGeometry(); const QVector& freeLineage = geometry.joints.at(endIndex).freeLineage; - if (freeLineage.isEmpty()) { - return; - } - int numFree = freeLineage.size(); - - // store and remember topmost parent transform - glm::mat4 topParentTransform; - { - int index = freeLineage.last(); - const JointState& state = _jointStates.at(index); - const FBXJoint& joint = state.getFBXJoint(); - int parentIndex = joint.parentIndex; - if (parentIndex == -1) { - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - topParentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - } else { - topParentTransform = _jointStates[parentIndex].getTransform(); - } - } - - // this is a cyclic coordinate descent algorithm: see - // http://www.ryanjuckett.com/programming/animation/21-cyclic-coordinate-descent-in-2d - - // keep track of the position of the end-effector - JointState& endState = _jointStates[endIndex]; - glm::vec3 endPosition = endState.getPosition(); - float distanceToGo = glm::distance(targetPosition, endPosition); - - const int MAX_ITERATION_COUNT = 2; - const float ACCEPTABLE_IK_ERROR = 0.005f; // 5mm - int numIterations = 0; - do { - ++numIterations; - // moving up, rotate each free joint to get endPosition closer to target - for (int j = 1; j < numFree; j++) { - int nextIndex = freeLineage.at(j); - JointState& nextState = _jointStates[nextIndex]; - FBXJoint nextJoint = nextState.getFBXJoint(); - if (! nextJoint.isFree) { - continue; - } - - glm::vec3 pivot = nextState.getPosition(); - glm::vec3 leverArm = endPosition - pivot; - float leverLength = glm::length(leverArm); - if (leverLength < EPSILON) { - continue; - } - glm::quat deltaRotation = rotationBetween(leverArm, targetPosition - pivot); - - // We want to mix the shortest rotation with one that will pull the system down with gravity - // so that limbs don't float unrealistically. To do this we compute a simplified center of mass - // where each joint has unit mass and we don't bother averaging it because we only need direction. - if (j > 1) { - - glm::vec3 centerOfMass(0.0f); - for (int k = 0; k < j; ++k) { - int massIndex = freeLineage.at(k); - centerOfMass += _jointStates[massIndex].getPosition() - pivot; - } - // the gravitational effect is a rotation that tends to align the two cross products - const glm::vec3 worldAlignment = glm::vec3(0.0f, -1.0f, 0.0f); - glm::quat gravityDelta = rotationBetween(glm::cross(centerOfMass, leverArm), - glm::cross(worldAlignment, leverArm)); - - float gravityAngle = glm::angle(gravityDelta); - const float MIN_GRAVITY_ANGLE = 0.1f; - float mixFactor = 0.5f; - if (gravityAngle < MIN_GRAVITY_ANGLE) { - // the final rotation is a mix of the two - mixFactor = 0.5f * gravityAngle / MIN_GRAVITY_ANGLE; - } - deltaRotation = safeMix(deltaRotation, gravityDelta, mixFactor); - } - - // Apply the rotation, but use mixRotationDelta() which blends a bit of the default pose - // in the process. This provides stability to the IK solution for most models. - glm::quat oldNextRotation = nextState.getRotation(); - float mixFactor = 0.03f; - nextState.mixRotationDelta(deltaRotation, mixFactor, priority); - - // measure the result of the rotation which may have been modified by - // blending and constraints - glm::quat actualDelta = nextState.getRotation() * glm::inverse(oldNextRotation); - endPosition = pivot + actualDelta * leverArm; - } - - // recompute transforms from the top down - glm::mat4 parentTransform = topParentTransform; - for (int j = numFree - 1; j >= 0; --j) { - JointState& freeState = _jointStates[freeLineage.at(j)]; - freeState.computeTransform(parentTransform); - parentTransform = freeState.getTransform(); - } - - // measure our success - endPosition = endState.getPosition(); - distanceToGo = glm::distance(targetPosition, endPosition); - } while (numIterations < MAX_ITERATION_COUNT && distanceToGo < ACCEPTABLE_IK_ERROR); - - // set final rotation of the end joint - endState.setRotationInBindFrame(targetRotation, priority, true); - + glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; + _rig->inverseKinematics(endIndex, targetPosition, targetRotation, priority, freeLineage, parentTransform); _shapesAreDirty = !_shapes.isEmpty(); } bool Model::restoreJointPosition(int jointIndex, float fraction, float priority) { - if (jointIndex == -1 || _jointStates.isEmpty()) { - return false; - } const FBXGeometry& geometry = _geometry->getFBXGeometry(); const QVector& freeLineage = geometry.joints.at(jointIndex).freeLineage; - - foreach (int index, freeLineage) { - JointState& state = _jointStates[index]; - state.restoreRotation(fraction, priority); - } - return true; + return _rig->restoreJointPosition(jointIndex, fraction, priority, freeLineage); } float Model::getLimbLength(int jointIndex) const { - if (jointIndex == -1 || _jointStates.isEmpty()) { - return 0.0f; - } const FBXGeometry& geometry = _geometry->getFBXGeometry(); const QVector& freeLineage = geometry.joints.at(jointIndex).freeLineage; - float length = 0.0f; - float lengthScale = (_scale.x + _scale.y + _scale.z) / 3.0f; - for (int i = freeLineage.size() - 2; i >= 0; i--) { - length += geometry.joints.at(freeLineage.at(i)).distanceToParent * lengthScale; - } - return length; + return _rig->getLimbLength(jointIndex, freeLineage, _scale, geometry.joints); } void Model::renderJointCollisionShapes(float alpha) { @@ -1816,7 +1509,7 @@ void Model::applyNextGeometry() { void Model::deleteGeometry() { _blendedVertexBuffers.clear(); - _jointStates.clear(); + _rig->clearJointStates(); _meshStates.clear(); clearShapes(); @@ -1945,7 +1638,9 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran args, locations); { - updateVisibleJointStates(); + if (!_showTrueJointTransforms) { + _rig->updateVisibleJointStates(); + } // else no need to update visible transforms } // if our index is ever out of range for either meshes or networkMeshes, then skip it, and set our _meshGroupsKnown diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 1b4df6821c..f8d8cbbd74 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -64,7 +64,7 @@ public: static void setAbstractViewStateInterface(AbstractViewStateInterface* viewState) { _viewState = viewState; } - Model(QObject* parent = nullptr, RigPointer rig = nullptr); + Model(RigPointer rig, QObject* parent = nullptr); virtual ~Model(); /// enables/disables scale to fit behavior, the model will be automatically scaled to the specified largest dimension @@ -162,7 +162,7 @@ public: const QSharedPointer getCollisionGeometry(bool delayLoad = true); /// Returns the number of joint states in the model. - int getJointStateCount() const { return _jointStates.size(); } + int getJointStateCount() const { return _rig->getJointStateCount(); } /// Fetches the joint state at the specified index. /// \return whether or not the joint state is "valid" (that is, non-default) @@ -224,9 +224,9 @@ public: void setShowTrueJointTransforms(bool show) { _showTrueJointTransforms = show; } - QVector& getJointStates() { return _jointStates; } - const QVector& getJointStates() const { return _jointStates; } - + // QVector& getJointStates() { return _rig->getJointStates(); } + // const QVector& getJointStates() const { return _jointStates; } + void inverseKinematics(int jointIndex, glm::vec3 position, const glm::quat& rotation, float priority); Q_INVOKABLE void setTextureWithNameToURL(const QString& name, const QUrl& url) @@ -259,8 +259,6 @@ protected: glm::vec3 _registrationPoint = glm::vec3(0.5f); /// the point in model space our center is snapped to bool _showTrueJointTransforms; - - QVector _jointStates; class MeshState { public: @@ -283,8 +281,6 @@ protected: /// Updates the state of the joint at the specified index. virtual void updateJointState(int index); - virtual void updateVisibleJointStates(); - /// \param jointIndex index of joint in model structure /// \param position position of joint in model-frame /// \param rotation rotation of joint in model-frame From 5a0c1a94020a23d953916cc480b52d454e1c8bc2 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 22 Jul 2015 21:03:48 -0700 Subject: [PATCH 056/242] Checkpoint testing stuff. --- libraries/animation/src/Rig.h | 4 +++- libraries/avatars/src/AvatarData.cpp | 1 + libraries/avatars/src/AvatarData.h | 3 +++ tests/rig/CMakeLists.txt | 2 +- tests/rig/src/RigTests.cpp | 15 +++++++++++++++ 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 89ea99f162..3819917be0 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -11,12 +11,14 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // /* TBD: - - What is responsibilities of Animation/AnimationPointer/AnimationCache/AnimationDetails? Is there common/copied code (e.g., ScriptableAvatar::update)? + - What is responsibilities of Animation/AnimationPointer/AnimationCache/AnimationDetails/AnimationObject/AnimationLoop? +Is there common/copied code (e.g., ScriptableAvatar::update)? - How do attachments interact with the physics of the attached entity? E.g., do hand joints need to reflect held object physics? - Is there any current need (i.e., for initial campatability) to have multiple animations per role (e.g., idle) with the system choosing randomly? - Distribute some doc from here to the right files if it turns out to be correct: - AnimationDetails is a script-useable copy of animation state, analogous to EntityItemProperties, but without anything equivalent to editEntity. + But what's the intended difference vs AnimationObjection? Maybe AnimationDetails is to Animation as AnimationObject is to AnimationPointer? */ #ifndef __hifi__Rig__ diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 3a3b895c66..942dbeeaf7 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1087,6 +1087,7 @@ void AvatarData::setJointMappingsFromNetworkReply() { } networkReply->deleteLater(); + emit jointsLoaded(); } void AvatarData::sendAvatarDataPacket() { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index a020be0f7a..285460651a 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -312,6 +312,9 @@ public: bool shouldDie() const { return _owningAvatarMixer.isNull() || getUsecsSinceLastUpdate() > AVATAR_SILENCE_THRESHOLD_USECS; } +signals: + void jointsLoaded(); // So that test cases or anyone waiting on asynchronous loading can be informed. + public slots: void sendAvatarDataPacket(); void sendIdentityPacket(); diff --git a/tests/rig/CMakeLists.txt b/tests/rig/CMakeLists.txt index 2e9dbc9424..5e965c3ee8 100644 --- a/tests/rig/CMakeLists.txt +++ b/tests/rig/CMakeLists.txt @@ -1,7 +1,7 @@ # Declare dependencies macro (setup_testcase_dependencies) # link in the shared libraries - link_hifi_libraries(shared animation gpu fbx model) + link_hifi_libraries(shared animation gpu fbx model avatars networking audio) copy_dlls_beside_windows_executable() endmacro () diff --git a/tests/rig/src/RigTests.cpp b/tests/rig/src/RigTests.cpp index ca07cda844..37f7a2bbab 100644 --- a/tests/rig/src/RigTests.cpp +++ b/tests/rig/src/RigTests.cpp @@ -40,11 +40,26 @@ */ #include +//#include "FSTReader.h" +// There are two good ways we could organize this: +// 1. Create a MyAvatar the same way that Interface does, and poke at it. +// We can't do that because MyAvatar (and even Avatar) are in interface, not a library, and our build system won't allow that dependency. +// 2. Create just the minimum skeleton in the most direct way possible, using only very basic library APIs (such as fbx). +// I don't think we can do that because not everything we need is exposed directly from, e.g., the fst and fbx readers. +// So here we do neither. Using as much as we can from AvatarData (which is in the avatar and further requires network and audio), and +// duplicating whatever other code we need from (My)Avatar. Ugh. We may refactor that later, but right now, cleaning this up is not on our critical path. +#include "AvatarData.h" #include "RigTests.h" QTEST_MAIN(RigTests) void RigTests::initTestCase() { + AvatarData avatar; + QEventLoop loop; // Create an event loop that will quit when we get the finished signal + QObject::connect(&avatar, &AvatarData::jointsLoaded, &loop, &QEventLoop::quit); + avatar.setSkeletonModelURL(QUrl("https://hifi-public.s3.amazonaws.com/marketplace/contents/4a690585-3fa3-499e-9f8b-fd1226e561b1/e47e6898027aa40f1beb6adecc6a7db5.fst")); // Zach + //std::cout << "sleep start" << std::endl; + loop.exec(); // Nothing is going to happen on this whole run thread until we get this _rig = new Rig(); } From 6657026a120d9bfe51602a3be8eb3140572c3538 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 23 Jul 2015 08:34:44 -0700 Subject: [PATCH 057/242] remove cruft: legacy shapes for skeleton bones --- interface/src/avatar/Hand.cpp | 59 -------- interface/src/avatar/Hand.h | 4 - interface/src/avatar/SkeletonModel.cpp | 140 +------------------ interface/src/avatar/SkeletonModel.h | 6 - libraries/fbx/src/FBXReader.cpp | 46 ++---- libraries/fbx/src/FBXReader.h | 4 - libraries/fbx/src/OBJReader.cpp | 5 - libraries/render-utils/src/GeometryCache.cpp | 2 +- 8 files changed, 13 insertions(+), 253 deletions(-) diff --git a/interface/src/avatar/Hand.cpp b/interface/src/avatar/Hand.cpp index 63604d710d..017f11a610 100644 --- a/interface/src/avatar/Hand.cpp +++ b/interface/src/avatar/Hand.cpp @@ -40,65 +40,6 @@ void Hand::simulate(float deltaTime, bool isMine) { } } -// We create a static CollisionList that is recycled for each collision test. -const float MAX_COLLISIONS_PER_AVATAR = 32; -static CollisionList handCollisions(MAX_COLLISIONS_PER_AVATAR); - -void Hand::collideAgainstAvatar(Avatar* avatar, bool isMyHand) { - if (!avatar || avatar == _owningAvatar) { - // don't collide hands against ourself (that is done elsewhere) - return; - } - - const SkeletonModel& skeletonModel = _owningAvatar->getSkeletonModel(); - int jointIndices[2]; - jointIndices[0] = skeletonModel.getLeftHandJointIndex(); - jointIndices[1] = skeletonModel.getRightHandJointIndex(); - - for (size_t i = 0; i < 2; i++) { - int jointIndex = jointIndices[i]; - if (jointIndex < 0) { - continue; - } - - handCollisions.clear(); - QVector shapes; - skeletonModel.getHandShapes(jointIndex, shapes); - - if (avatar->findCollisions(shapes, handCollisions)) { - glm::vec3 totalPenetration(0.0f); - glm::vec3 averageContactPoint; - for (int j = 0; j < handCollisions.size(); ++j) { - CollisionInfo* collision = handCollisions.getCollision(j); - totalPenetration += collision->_penetration; - averageContactPoint += collision->_contactPoint; - } - if (isMyHand) { - // our hand against other avatar - // TODO: resolve this penetration when we don't think the other avatar will yield - //palm.addToPenetration(averagePenetration); - } else { - // someone else's hand against MyAvatar - // TODO: submit collision info to MyAvatar which should lean accordingly - averageContactPoint /= (float)handCollisions.size(); - avatar->applyCollision(averageContactPoint, totalPenetration); - - CollisionInfo collision; - collision._penetration = totalPenetration; - collision._contactPoint = averageContactPoint; - emit avatar->collisionWithAvatar(avatar->getSessionUUID(), _owningAvatar->getSessionUUID(), collision); - } - } - } -} - -void Hand::resolvePenetrations() { - for (size_t i = 0; i < getNumPalms(); ++i) { - PalmData& palm = getPalms()[i]; - palm.resolvePenetrations(); - } -} - void Hand::render(RenderArgs* renderArgs, bool isMine) { gpu::Batch& batch = *renderArgs->_batch; if (renderArgs->_renderMode != RenderArgs::SHADOW_RENDER_MODE && diff --git a/interface/src/avatar/Hand.h b/interface/src/avatar/Hand.h index ddc9d13489..750633959a 100644 --- a/interface/src/avatar/Hand.h +++ b/interface/src/avatar/Hand.h @@ -40,10 +40,6 @@ public: void simulate(float deltaTime, bool isMine); void render(RenderArgs* renderArgs, bool isMine); - void collideAgainstAvatar(Avatar* avatar, bool isMyHand); - - void resolvePenetrations(); - private: // disallow copies of the Hand, copy of owning Avatar is disallowed too Hand(const Hand&); diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index d92481494a..318db274a9 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -159,41 +159,6 @@ void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { _boundingShape.setRotation(_rotation); } -void SkeletonModel::getHandShapes(int jointIndex, QVector& shapes) const { - if (jointIndex < 0 || jointIndex >= int(_shapes.size())) { - return; - } - if (jointIndex == getLeftHandJointIndex() - || jointIndex == getRightHandJointIndex()) { - // get all shapes that have this hand as an ancestor in the skeleton heirarchy - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - for (int i = 0; i < _jointStates.size(); i++) { - const FBXJoint& joint = geometry.joints[i]; - int parentIndex = joint.parentIndex; - Shape* shape = _shapes[i]; - if (i == jointIndex) { - // this shape is the hand - if (shape) { - shapes.push_back(shape); - } - if (parentIndex != -1 && _shapes[parentIndex]) { - // also add the forearm - shapes.push_back(_shapes[parentIndex]); - } - } else if (shape) { - while (parentIndex != -1) { - if (parentIndex == jointIndex) { - // this shape is a child of the hand - shapes.push_back(shape); - break; - } - parentIndex = geometry.joints[parentIndex].parentIndex; - } - } - } - } -} - void SkeletonModel::renderIKConstraints(gpu::Batch& batch) { renderJointConstraints(batch, getRightHandJointIndex()); renderJointConstraints(batch, getLeftHandJointIndex()); @@ -644,46 +609,12 @@ void SkeletonModel::buildShapes() { // rootJointIndex == -1 if the avatar model has no skeleton return; } - - float uniformScale = extractUniformScale(_scale); - const int numStates = _jointStates.size(); - for (int i = 0; i < numStates; i++) { - JointState& state = _jointStates[i]; - const FBXJoint& joint = state.getFBXJoint(); - float radius = uniformScale * joint.boneRadius; - float halfHeight = 0.5f * uniformScale * joint.distanceToParent; - Shape::Type type = joint.shapeType; - int parentIndex = joint.parentIndex; - if (parentIndex == -1 || radius < EPSILON) { - type = INVALID_SHAPE; - } else if (type == CAPSULE_SHAPE && halfHeight < EPSILON) { - // this shape is forced to be a sphere - type = SPHERE_SHAPE; - } - Shape* shape = NULL; - if (type == SPHERE_SHAPE) { - shape = new SphereShape(radius); - shape->setEntity(this); - } else if (type == CAPSULE_SHAPE) { - assert(parentIndex != -1); - shape = new CapsuleShape(radius, halfHeight); - shape->setEntity(this); - } - if (shape && parentIndex != -1) { - // always disable collisions between joint and its parent - disableCollisions(i, parentIndex); - } - _shapes.push_back(shape); - } - - // This method moves the shapes to their default positions in Model frame. computeBoundingShape(geometry); } void SkeletonModel::computeBoundingShape(const FBXGeometry& geometry) { // compute default joint transforms int numStates = _jointStates.size(); - assert(numStates == _shapes.size()); QVector transforms; transforms.fill(glm::mat4(), numStates); @@ -704,39 +635,11 @@ void SkeletonModel::computeBoundingShape(const FBXGeometry& geometry) { * joint.preTransform * glm::mat4_cast(modifiedRotation) * joint.postTransform; } - // Each joint contributes its point to the bounding box + // Each joint contributes a sphere at its position + glm::vec3 axis(joint.boneRadius); glm::vec3 jointPosition = extractTranslation(transforms[i]); - totalExtents.addPoint(jointPosition); - - Shape* shape = _shapes[i]; - if (!shape) { - continue; - } - - // Each joint with a shape contributes to the totalExtents: a box - // that contains the sphere centered at the end of the joint with radius of the bone. - - // TODO: skip hand and arm shapes for bounding box calculation - int type = shape->getType(); - if (type == CAPSULE_SHAPE) { - // add the two furthest surface points of the capsule - CapsuleShape* capsule = static_cast(shape); - float radius = capsule->getRadius(); - glm::vec3 axis(radius); - Extents shapeExtents; - shapeExtents.reset(); - shapeExtents.addPoint(jointPosition + axis); - shapeExtents.addPoint(jointPosition - axis); - totalExtents.addExtents(shapeExtents); - } else if (type == SPHERE_SHAPE) { - float radius = shape->getBoundingRadius(); - glm::vec3 axis(radius); - Extents shapeExtents; - shapeExtents.reset(); - shapeExtents.addPoint(jointPosition + axis); - shapeExtents.addPoint(jointPosition - axis); - totalExtents.addExtents(shapeExtents); - } + totalExtents.addPoint(jointPosition + axis); + totalExtents.addPoint(jointPosition - axis); } // compute bounding shape parameters @@ -752,43 +655,8 @@ void SkeletonModel::computeBoundingShape(const FBXGeometry& geometry) { _boundingRadius = 0.5f * glm::length(diagonal); } -void SkeletonModel::resetShapePositionsToDefaultPose() { - // DEBUG method. - // Moves shapes to the joint default locations for debug visibility into - // how the bounding shape is computed. - - if (!_geometry || _shapes.isEmpty()) { - // geometry or joints have not yet been created - return; - } - - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - if (geometry.joints.isEmpty()) { - return; - } - - // The shapes are moved to their default positions in computeBoundingShape(). - computeBoundingShape(geometry); - - // Then we move them into world frame for rendering at the Model's location. - for (int i = 0; i < _shapes.size(); i++) { - Shape* shape = _shapes[i]; - if (shape) { - shape->setTranslation(_translation + _rotation * shape->getTranslation()); - shape->setRotation(_rotation * shape->getRotation()); - } - } - _boundingShape.setTranslation(_translation + _rotation * _boundingShapeLocalOffset); - _boundingShape.setRotation(_rotation); -} - void SkeletonModel::renderBoundingCollisionShapes(gpu::Batch& batch, float alpha) { const int BALL_SUBDIVISIONS = 10; - if (_shapes.isEmpty()) { - // the bounding shape has not been propery computed - // so no need to render it - return; - } // draw a blue sphere at the capsule endpoint glm::vec3 endPoint; diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 3d63238cf2..9d0c51e521 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -32,10 +32,6 @@ public: void simulate(float deltaTime, bool fullUpdate = true); - /// \param jointIndex index of hand joint - /// \param shapes[out] list in which is stored pointers to hand shapes - void getHandShapes(int jointIndex, QVector& shapes) const; - void renderIKConstraints(gpu::Batch& batch); /// Returns the index of the left hand joint, or -1 if not found. @@ -106,8 +102,6 @@ public: const CapsuleShape& getBoundingShape() const { return _boundingShape; } const glm::vec3 getBoundingShapeOffset() const { return _boundingShapeLocalOffset; } - void resetShapePositionsToDefaultPose(); // DEBUG method - bool hasSkeleton(); float getHeadClipDistance() const { return _headClipDistance; } diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 4d7bff4df0..f67fe133f2 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1198,7 +1198,7 @@ class JointShapeInfo { public: JointShapeInfo() : numVertices(0), sumVertexWeights(0.0f), sumWeightedRadii(0.0f), numVertexWeights(0), - averageVertex(0.0f), boneBegin(0.0f), averageRadius(0.0f) { + boneBegin(0.0f), averageRadius(0.0f) { } // NOTE: the points here are in the "joint frame" which has the "jointEnd" at the origin @@ -1206,9 +1206,8 @@ public: float sumVertexWeights; // sum of all vertex weights float sumWeightedRadii; // sum of weighted vertices int numVertexWeights; // num vertices that contributed to sums - glm::vec3 averageVertex;// average of all mesh vertices (in joint frame) glm::vec3 boneBegin; // parent joint location (in joint frame) - float averageRadius; // average distance from mesh points to averageVertex + float averageRadius; }; class AnimationCurve { @@ -2219,8 +2218,6 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, joint.boneRadius = 0.0f; joint.inverseBindRotation = joint.inverseDefaultRotation; joint.name = model.name; - joint.shapePosition = glm::vec3(0.0f); - joint.shapeType = INVALID_SHAPE; foreach (const QString& childID, childMap.values(modelID)) { QString type = typeFlags.value(childID); @@ -2490,7 +2487,6 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, int jointIndex = fbxCluster.jointIndex; FBXJoint& joint = geometry.joints[jointIndex]; glm::mat4 transformJointToMesh = inverseModelTransform * joint.bindTransform; - glm::quat rotateMeshToJoint = glm::inverse(extractRotation(transformJointToMesh)); glm::vec3 boneEnd = extractTranslation(transformJointToMesh); glm::vec3 boneBegin = boneEnd; glm::vec3 boneDirection; @@ -2524,8 +2520,6 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, jointShapeInfo.sumWeightedRadii += radiusWeight * radiusScale * glm::distance(vertex, boneEnd - boneDirection * proj); ++jointShapeInfo.numVertexWeights; - glm::vec3 vertexInJointFrame = rotateMeshToJoint * (radiusScale * (vertex - boneEnd)); - jointShapeInfo.averageVertex += vertexInJointFrame; ++jointShapeInfo.numVertices; } @@ -2571,7 +2565,6 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, JointShapeInfo& jointShapeInfo = jointShapeInfos[jointIndex]; glm::mat4 transformJointToMesh = inverseModelTransform * joint.bindTransform; - glm::quat rotateMeshToJoint = glm::inverse(extractRotation(transformJointToMesh)); glm::vec3 boneEnd = extractTranslation(transformJointToMesh); glm::vec3 boneBegin = boneEnd; @@ -2594,9 +2587,6 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, jointShapeInfo.sumVertexWeights += radiusWeight; jointShapeInfo.sumWeightedRadii += radiusWeight * radiusScale * glm::distance(vertex, boneEnd - boneDirection * proj); ++jointShapeInfo.numVertexWeights; - - glm::vec3 vertexInJointFrame = rotateMeshToJoint * (radiusScale * (vertex - boneEnd)); - jointShapeInfo.averageVertex += vertexInJointFrame; averageVertex += vertex; } int numVertices = extracted.mesh.vertices.size(); @@ -2622,7 +2612,7 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, } - // now that all joints have been scanned, compute a collision shape for each joint + // now that all joints have been scanned, compute a radius for each bone glm::vec3 defaultCapsuleAxis(0.0f, 1.0f, 0.0f); for (int i = 0; i < geometry.joints.size(); ++i) { FBXJoint& joint = geometry.joints[i]; @@ -2640,40 +2630,20 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, joint.boneRadius = jointShapeInfo.sumWeightedRadii / jointShapeInfo.sumVertexWeights; } - // we use a capsule if the joint had ANY mesh vertices successfully projected onto the bone + // the joint is "capsule-like" if it had ANY mesh vertices successfully projected onto the bone // AND its boneRadius is not too close to zero bool collideLikeCapsule = jointShapeInfo.numVertexWeights > 0 && glm::length(jointShapeInfo.boneBegin) > EPSILON; - if (collideLikeCapsule) { - joint.shapeRotation = rotationBetween(defaultCapsuleAxis, jointShapeInfo.boneBegin); - joint.shapePosition = 0.5f * jointShapeInfo.boneBegin; - joint.shapeType = CAPSULE_SHAPE; - } else { - // collide the joint like a sphere - joint.shapeType = SPHERE_SHAPE; - if (jointShapeInfo.numVertices > 0) { - jointShapeInfo.averageVertex /= (float)jointShapeInfo.numVertices; - joint.shapePosition = jointShapeInfo.averageVertex; - } else { - joint.shapePosition = glm::vec3(0.0f); - } + if (!collideLikeCapsule) { + // this joint's mesh did not successfully project onto the bone axis + // so it isn't "capsule-like" and we need to estimate its radius a different way: + // the average radius to the average point. if (jointShapeInfo.numVertexWeights == 0 && jointShapeInfo.numVertices > 0) { - // the bone projection algorithm was not able to compute the joint radius - // so we use an alternative measure jointShapeInfo.averageRadius /= (float)jointShapeInfo.numVertices; joint.boneRadius = jointShapeInfo.averageRadius; } - - float distanceFromEnd = glm::length(joint.shapePosition); - float distanceFromBegin = glm::distance(joint.shapePosition, jointShapeInfo.boneBegin); - if (distanceFromEnd > joint.distanceToParent && distanceFromBegin > joint.distanceToParent) { - // The shape is further from both joint endpoints than the endpoints are from each other - // which probably means the model has a bad transform somewhere. We disable this shape - // by setting its type to INVALID_SHAPE. - joint.shapeType = INVALID_SHAPE; - } } } geometry.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 200cd4a121..fbb303a93b 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -25,7 +25,6 @@ #include #include -#include #include #include @@ -78,9 +77,6 @@ public: glm::quat inverseBindRotation; glm::mat4 bindTransform; QString name; - glm::vec3 shapePosition; // in joint frame - glm::quat shapeRotation; // in joint frame - quint8 shapeType; bool isSkeletonJoint; }; diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 2ec80e3d85..35ba437745 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -427,8 +427,6 @@ FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping, Q geometry.joints[0].rotationMin = glm::vec3(0, 0, 0); geometry.joints[0].rotationMax = glm::vec3(0, 0, 0); geometry.joints[0].name = "OBJ"; - geometry.joints[0].shapePosition = glm::vec3(0, 0, 0); - geometry.joints[0].shapeType = SPHERE_SHAPE; geometry.joints[0].isSkeletonJoint = true; geometry.jointIndices["x"] = 1; @@ -604,9 +602,6 @@ void fbxDebugDump(const FBXGeometry& fbxgeo) { qCDebug(modelformat) << " inverseBindRotation" << joint.inverseBindRotation; qCDebug(modelformat) << " bindTransform" << joint.bindTransform; qCDebug(modelformat) << " name" << joint.name; - qCDebug(modelformat) << " shapePosition" << joint.shapePosition; - qCDebug(modelformat) << " shapeRotation" << joint.shapeRotation; - qCDebug(modelformat) << " shapeType" << joint.shapeType; qCDebug(modelformat) << " isSkeletonJoint" << joint.isSkeletonJoint; } diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 6c03d57de3..cd63f7303a 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1780,7 +1780,7 @@ NetworkGeometry::NetworkGeometry(const QUrl& url, const QSharedPointer(), -1, 0.0f, 0.0f, glm::vec3(), glm::mat4(), glm::quat(), glm::quat(), glm::quat(), glm::mat4(), glm::mat4(), glm::vec3(), glm::vec3(), glm::quat(), glm::quat(), - glm::mat4(), QString(""), glm::vec3(), glm::quat(), SHAPE_TYPE_NONE, false}; + glm::mat4(), QString(""), false}; _geometry.joints.append(joint); _geometry.leftEyeJointIndex = -1; _geometry.rightEyeJointIndex = -1; From 7c4f6b665bcd614cc6739fd1e28531621385cec4 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 23 Jul 2015 11:04:58 -0700 Subject: [PATCH 058/242] Use a default eye diameter for models without eyes, e.g., the masks --- interface/src/avatar/Avatar.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index bf41449f13..9f457e558d 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -468,22 +468,29 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { if (alpha > 0.0f) { QSharedPointer geometry = getHead()->getFaceModel().getGeometry(); if (geometry) { + const float DEFAULT_EYE_DIAMETER = 0.048f; // Typical human eye const float RADIUS_INCREMENT = 0.005f; Transform transform; glm::vec3 position = getHead()->getLeftEyePosition(); transform.setTranslation(position); batch.setModelTransform(transform); - DependencyManager::get()->renderSolidSphere(batch, - geometry->getFBXGeometry().leftEyeSize * _scale / 2.0f + RADIUS_INCREMENT, 15, 15, - glm::vec4(LOOKING_AT_ME_COLOR, alpha)); + float eyeDiameter = geometry->getFBXGeometry().leftEyeSize; + if (eyeDiameter == 0.0f) { + eyeDiameter = DEFAULT_EYE_DIAMETER; + } + DependencyManager::get()->renderSolidSphere(batch, + eyeDiameter * _scale / 2.0f + RADIUS_INCREMENT, 15, 15, glm::vec4(LOOKING_AT_ME_COLOR, alpha)); position = getHead()->getRightEyePosition(); transform.setTranslation(position); batch.setModelTransform(transform); - DependencyManager::get()->renderSolidSphere(batch, - geometry->getFBXGeometry().rightEyeSize * _scale / 2.0f + RADIUS_INCREMENT, 15, 15, - glm::vec4(LOOKING_AT_ME_COLOR, alpha)); + eyeDiameter = geometry->getFBXGeometry().rightEyeSize; + if (eyeDiameter == 0.0f) { + eyeDiameter = DEFAULT_EYE_DIAMETER; + } + DependencyManager::get()->renderSolidSphere(batch, + eyeDiameter * _scale / 2.0f + RADIUS_INCREMENT, 15, 15, glm::vec4(LOOKING_AT_ME_COLOR, alpha)); } } From 4566d16402236ac77a7b7784e5387159d6837ac8 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 23 Jul 2015 13:03:38 -0700 Subject: [PATCH 059/242] move Model's AnimationHandles to Rig. Move a bunch of Model methods from public to protected --- interface/src/avatar/Avatar.cpp | 4 +- interface/src/avatar/MyAvatar.cpp | 4 +- libraries/animation/src/Rig.cpp | 15 +- libraries/animation/src/Rig.h | 3 +- libraries/render-utils/src/Model.cpp | 25 +-- libraries/render-utils/src/Model.h | 279 +++++++++++++-------------- 6 files changed, 161 insertions(+), 169 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 10866fa9ca..2bc7e788ad 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -823,7 +823,7 @@ QVector Avatar::getJointRotations() const { } QVector jointRotations(_skeletonModel.getJointStateCount()); for (int i = 0; i < _skeletonModel.getJointStateCount(); ++i) { - _skeletonModel.getJointState(i, jointRotations[i]); + _skeletonModel.getJointRotation(i, jointRotations[i]); } return jointRotations; } @@ -833,7 +833,7 @@ glm::quat Avatar::getJointRotation(int index) const { return AvatarData::getJointRotation(index); } glm::quat rotation; - _skeletonModel.getJointState(index, rotation); + _skeletonModel.getJointRotation(index, rotation); return rotation; } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 56f5241316..178ebc0487 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -216,10 +216,10 @@ void MyAvatar::simulate(float deltaTime) { { PerformanceTimer perfTimer("joints"); // copy out the skeleton joints from the model - _jointData.resize(_skeletonModel.getJointStateCount()); + _jointData.resize(_rig->getJointStateCount()); for (int i = 0; i < _jointData.size(); i++) { JointData& data = _jointData[i]; - data.valid = _skeletonModel.getJointState(i, data.rotation); + data.valid = _rig->getJointStateRotation(i, data.rotation); } } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 12bfd606da..0a889fce88 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -41,6 +41,14 @@ bool Rig::isRunningAnimation(AnimationHandlePointer animationHandle) { return _runningAnimations.contains(animationHandle); } +void Rig::deleteAnimations() { + for (QSet::iterator it = _animationHandles.begin(); it != _animationHandles.end(); ) { + (*it)->clearJoints(); + it = _animationHandles.erase(it); + } +} + + float Rig::initJointStates(QVector states, glm::mat4 parentTransform) { _jointStates = states; initJointTransforms(parentTransform); @@ -235,7 +243,12 @@ glm::mat4 Rig::getJointVisibleTransform(int jointIndex) const { return _jointStates[jointIndex].getVisibleTransform(); } -void Rig::simulateInternal(glm::mat4 parentTransform) { +void Rig::simulateInternal(float deltaTime, glm::mat4 parentTransform) { + // update animations + foreach (const AnimationHandlePointer& handle, _runningAnimations) { + handle->simulate(deltaTime); + } + for (int i = 0; i < _jointStates.size(); i++) { updateJointState(i, parentTransform); } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 6d282bf8f4..e60116a35e 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -49,6 +49,7 @@ public: void addRunningAnimation(AnimationHandlePointer animationHandle); bool isRunningAnimation(AnimationHandlePointer animationHandle); const QList& getRunningAnimations() const { return _runningAnimations; } + void deleteAnimations(); float initJointStates(QVector states, glm::mat4 parentTransform); bool jointStatesEmpty() { return _jointStates.isEmpty(); }; @@ -81,7 +82,7 @@ public: void setJointTransform(int jointIndex, glm::mat4 newTransform); glm::mat4 getJointVisibleTransform(int jointIndex) const; void setJointVisibleTransform(int jointIndex, glm::mat4 newTransform); - void simulateInternal(glm::mat4 parentTransform); + void simulateInternal(float deltaTime, glm::mat4 parentTransform); bool setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, bool useRotation, int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, float priority, const QVector& freeLineage, glm::mat4 parentTransform); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index af60c91664..bd33e75207 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1365,18 +1365,13 @@ void Model::updateClusterMatrices() { void Model::simulateInternal(float deltaTime) { // update the world space transforms for all joints - - // update animations - foreach (const AnimationHandlePointer& handle, _runningAnimations) { - handle->simulate(deltaTime); - } const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - _rig->simulateInternal(parentTransform); + _rig->simulateInternal(deltaTime, parentTransform); _shapesAreDirty = !_shapes.isEmpty(); - + glm::mat4 modelToWorld = glm::mat4_cast(_rotation); for (int i = 0; i < _meshStates.size(); i++) { MeshState& state = _meshStates[i]; @@ -1512,21 +1507,13 @@ void Model::deleteGeometry() { _rig->clearJointStates(); _meshStates.clear(); clearShapes(); - - for (QSet::iterator it = _animationHandles.begin(); it != _animationHandles.end(); ) { - AnimationHandlePointer handle = it->lock(); - if (handle) { - handle->clearJoints(); - it++; - } else { - it = _animationHandles.erase(it); - } - } - + + _rig->deleteAnimations(); + if (_geometry) { _geometry->clearLoadPriority(this); } - + _blendedBlendshapeCoefficients.clear(); } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index f8d8cbbd74..30c5211990 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -57,7 +57,7 @@ inline uint qHash(const std::shared_ptr& a, uint seed) { /// A generic 3D model displaying geometry loaded from a URL. class Model : public QObject, public PhysicsEntity { Q_OBJECT - + public: typedef RenderArgs::RenderMode RenderMode; @@ -66,58 +66,7 @@ public: Model(RigPointer rig, QObject* parent = nullptr); virtual ~Model(); - - /// enables/disables scale to fit behavior, the model will be automatically scaled to the specified largest dimension - void setScaleToFit(bool scaleToFit, float largestDimension = 0.0f, bool forceRescale = false); - bool getScaleToFit() const { return _scaleToFit; } /// is scale to fit enabled - bool getIsScaledToFit() const { return _scaledToFit; } /// is model scaled to fit - const glm::vec3& getScaleToFitDimensions() const { return _scaleToFitDimensions; } /// the dimensions model is scaled to - void setScaleToFit(bool scaleToFit, const glm::vec3& dimensions); - void setSnapModelToCenter(bool snapModelToCenter) { - setSnapModelToRegistrationPoint(snapModelToCenter, glm::vec3(0.5f,0.5f,0.5f)); - }; - bool getSnapModelToCenter() { - return _snapModelToRegistrationPoint && _registrationPoint == glm::vec3(0.5f,0.5f,0.5f); - } - - void setSnapModelToRegistrationPoint(bool snapModelToRegistrationPoint, const glm::vec3& registrationPoint); - bool getSnapModelToRegistrationPoint() { return _snapModelToRegistrationPoint; } - - void setScale(const glm::vec3& scale); - const glm::vec3& getScale() const { return _scale; } - - void setOffset(const glm::vec3& offset); - const glm::vec3& getOffset() const { return _offset; } - - void setPupilDilation(float dilation) { _pupilDilation = dilation; } - float getPupilDilation() const { return _pupilDilation; } - - void setBlendshapeCoefficients(const QVector& coefficients) { _blendshapeCoefficients = coefficients; } - const QVector& getBlendshapeCoefficients() const { return _blendshapeCoefficients; } - - bool isActive() const { return _geometry && _geometry->isLoaded(); } - - bool isRenderable() const { return !_meshStates.isEmpty() || (isActive() && _geometry->getMeshes().isEmpty()); } - - void setVisibleInScene(bool newValue, std::shared_ptr scene); - bool isVisible() const { return _isVisible; } - - bool isLoaded() const { return _geometry && _geometry->isLoaded(); } - bool isLoadedWithTextures() const { return _geometry && _geometry->isLoadedWithTextures(); } - - void init(); - void reset(); - virtual void simulate(float deltaTime, bool fullUpdate = true); - - void renderSetup(RenderArgs* args); - - // new Scene/Engine rendering support - bool needsFixupInScene() { return !_readyWhenAdded && readyToAddToScene(); } - bool readyToAddToScene(RenderArgs* renderArgs = nullptr) { return !_needsReload && isRenderable() && isActive() && isLoaded(); } - bool addToScene(std::shared_ptr scene, render::PendingChanges& pendingChanges); - bool addToScene(std::shared_ptr scene, render::PendingChanges& pendingChanges, render::Item::Status::Getters& statusGetters); - void removeFromScene(std::shared_ptr scene, render::PendingChanges& pendingChanges); /// Sets the URL of the model to render. /// \param fallback the URL of a fallback model to render if the requested model fails to load @@ -127,22 +76,127 @@ public: bool retainCurrent = false, bool delayLoad = false); const QUrl& getURL() const { return _url; } + // new Scene/Engine rendering support + void setVisibleInScene(bool newValue, std::shared_ptr scene); + bool needsFixupInScene() { return !_readyWhenAdded && readyToAddToScene(); } + bool readyToAddToScene(RenderArgs* renderArgs = nullptr) { + return !_needsReload && isRenderable() && isActive() && isLoaded(); + } + bool initWhenReady(render::ScenePointer scene); + bool addToScene(std::shared_ptr scene, render::PendingChanges& pendingChanges); + bool addToScene(std::shared_ptr scene, + render::PendingChanges& pendingChanges, + render::Item::Status::Getters& statusGetters); + void removeFromScene(std::shared_ptr scene, render::PendingChanges& pendingChanges); + void renderSetup(RenderArgs* args); + bool isRenderable() const { return !_meshStates.isEmpty() || (isActive() && _geometry->getMeshes().isEmpty()); } + virtual void renderJointCollisionShapes(float alpha); + + bool isVisible() const { return _isVisible; } + + AABox getPartBounds(int meshIndex, int partIndex); + void renderPart(RenderArgs* args, int meshIndex, int partIndex, bool translucent); + + bool maybeStartBlender(); + + /// Sets blended vertices computed in a separate thread. + void setBlendedVertices(int blendNumber, const QWeakPointer& geometry, + const QVector& vertices, const QVector& normals); + + bool isLoaded() const { return _geometry && _geometry->isLoaded(); } + bool isLoadedWithTextures() const { return _geometry && _geometry->isLoadedWithTextures(); } + + void setIsWireframe(bool isWireframe) { _isWireframe = isWireframe; } + bool isWireframe() const { return _isWireframe; } + + void init(); + void reset(); + + void setScaleToFit(bool scaleToFit, const glm::vec3& dimensions); + + void setSnapModelToRegistrationPoint(bool snapModelToRegistrationPoint, const glm::vec3& registrationPoint); + bool getSnapModelToRegistrationPoint() { return _snapModelToRegistrationPoint; } + + virtual void simulate(float deltaTime, bool fullUpdate = true); + + /// Returns a reference to the shared geometry. + const QSharedPointer& getGeometry() const { return _geometry; } + + bool isActive() const { return _geometry && _geometry->isLoaded(); } + + Q_INVOKABLE void setTextureWithNameToURL(const QString& name, const QUrl& url) + { _geometry->setTextureWithNameToURL(name, url); } + + bool convexHullContains(glm::vec3 point); + + QStringList getJointNames() const; + + /// Sets the joint state at the specified index. + void setJointState(int index, bool valid, const glm::quat& rotation = glm::quat(), float priority = 1.0f); + + bool findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, + BoxFace& face, QString& extraInfo, bool pickAgainstTriangles = false); + // Set the model to use for collisions Q_INVOKABLE void setCollisionModelURL(const QUrl& url); const QUrl& getCollisionURL() const { return _collisionUrl; } - - void setIsWireframe(bool isWireframe) { _isWireframe = isWireframe; } - bool isWireframe() const { return _isWireframe; } - + + /// Returns a reference to the shared collision geometry. + const QSharedPointer getCollisionGeometry(bool delayLoad = true); + + void setOffset(const glm::vec3& offset); + const glm::vec3& getOffset() const { return _offset; } + /// Sets the distance parameter used for LOD computations. void setLODDistance(float distance) { _lodDistance = distance; } - + + const QList& getRunningAnimations() const { return _rig->getRunningAnimations(); } + /// Clear the joint animation priority + void clearJointAnimationPriority(int index); + + void setScaleToFit(bool scaleToFit, float largestDimension = 0.0f, bool forceRescale = false); + bool getScaleToFit() const { return _scaleToFit; } /// is scale to fit enabled + + void setSnapModelToCenter(bool snapModelToCenter) { + setSnapModelToRegistrationPoint(snapModelToCenter, glm::vec3(0.5f,0.5f,0.5f)); + }; + bool getSnapModelToCenter() { + return _snapModelToRegistrationPoint && _registrationPoint == glm::vec3(0.5f,0.5f,0.5f); + } + + /// Returns the number of joint states in the model. + int getJointStateCount() const { return _rig->getJointStateCount(); } + bool getJointPositionInWorldFrame(int jointIndex, glm::vec3& position) const; + bool getJointRotationInWorldFrame(int jointIndex, glm::quat& rotation) const; + bool getJointCombinedRotation(int jointIndex, glm::quat& rotation) const; + /// \param jointIndex index of joint in model structure + /// \param rotation[out] rotation of joint in model-frame + /// \return true if joint exists + bool getJointRotation(int jointIndex, glm::quat& rotation) const; + + void inverseKinematics(int jointIndex, glm::vec3 position, const glm::quat& rotation, float priority); + /// Returns the extents of the model in its bind pose. Extents getBindExtents() const; /// Returns the extents of the model's mesh Extents getMeshExtents() const; + void setScale(const glm::vec3& scale); + const glm::vec3& getScale() const { return _scale; } + + /// enables/disables scale to fit behavior, the model will be automatically scaled to the specified largest dimension + bool getIsScaledToFit() const { return _scaledToFit; } /// is model scaled to fit + const glm::vec3& getScaleToFitDimensions() const { return _scaleToFitDimensions; } /// the dimensions model is scaled to + +protected: + + void setPupilDilation(float dilation) { _pupilDilation = dilation; } + float getPupilDilation() const { return _pupilDilation; } + + void setBlendshapeCoefficients(const QVector& coefficients) { _blendshapeCoefficients = coefficients; } + const QVector& getBlendshapeCoefficients() const { return _blendshapeCoefficients; } + /// Returns the unscaled extents of the model's mesh Extents getUnscaledMeshExtents() const; @@ -155,15 +209,6 @@ public: /// Returns the scaled equivalent of a point in model space. glm::vec3 calculateScaledOffsetPoint(const glm::vec3& point) const; - /// Returns a reference to the shared geometry. - const QSharedPointer& getGeometry() const { return _geometry; } - - /// Returns a reference to the shared collision geometry. - const QSharedPointer getCollisionGeometry(bool delayLoad = true); - - /// Returns the number of joint states in the model. - int getJointStateCount() const { return _rig->getJointStateCount(); } - /// Fetches the joint state at the specified index. /// \return whether or not the joint state is "valid" (that is, non-default) bool getJointState(int index, glm::quat& rotation) const; @@ -171,25 +216,15 @@ public: /// Fetches the visible joint state at the specified index. /// \return whether or not the joint state is "valid" (that is, non-default) bool getVisibleJointState(int index, glm::quat& rotation) const; - + /// Clear the joint states void clearJointState(int index); - - /// Clear the joint animation priority - void clearJointAnimationPriority(int index); - - /// Sets the joint state at the specified index. - void setJointState(int index, bool valid, const glm::quat& rotation = glm::quat(), float priority = 1.0f); - + /// Returns the index of the parent of the indexed joint, or -1 if not found. int getParentJointIndex(int jointIndex) const; - + /// Returns the index of the last free ancestor of the indexed joint, or -1 if not found. int getLastFreeJointIndex(int jointIndex) const; - - bool getJointPositionInWorldFrame(int jointIndex, glm::vec3& position) const; - bool getJointRotationInWorldFrame(int jointIndex, glm::quat& rotation) const; - bool getJointCombinedRotation(int jointIndex, glm::quat& rotation) const; bool getVisibleJointPositionInWorldFrame(int jointIndex, glm::vec3& position) const; bool getVisibleJointRotationInWorldFrame(int jointIndex, glm::quat& rotation) const; @@ -199,52 +234,15 @@ public: /// \return true if joint exists bool getJointPosition(int jointIndex, glm::vec3& position) const; - /// \param jointIndex index of joint in model structure - /// \param rotation[out] rotation of joint in model-frame - /// \return true if joint exists - bool getJointRotation(int jointIndex, glm::quat& rotation) const; - - QStringList getJointNames() const; - - AnimationHandlePointer createAnimationHandle(); - - const QList& getRunningAnimations() const { return _runningAnimations; } - // virtual overrides from PhysicsEntity virtual void buildShapes(); virtual void updateShapePositions(); - virtual void renderJointCollisionShapes(float alpha); - - bool maybeStartBlender(); - - /// Sets blended vertices computed in a separate thread. - void setBlendedVertices(int blendNumber, const QWeakPointer& geometry, - const QVector& vertices, const QVector& normals); - void setShowTrueJointTransforms(bool show) { _showTrueJointTransforms = show; } - // QVector& getJointStates() { return _rig->getJointStates(); } - // const QVector& getJointStates() const { return _jointStates; } - - void inverseKinematics(int jointIndex, glm::vec3 position, const glm::quat& rotation, float priority); - - Q_INVOKABLE void setTextureWithNameToURL(const QString& name, const QUrl& url) - { _geometry->setTextureWithNameToURL(name, url); } - - bool findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, - BoxFace& face, QString& extraInfo, bool pickAgainstTriangles = false); - bool convexHullContains(glm::vec3 point); - - AABox getPartBounds(int meshIndex, int partIndex); - void renderPart(RenderArgs* args, int meshIndex, int partIndex, bool translucent); - - bool initWhenReady(render::ScenePointer scene); - -protected: QSharedPointer _geometry; void setGeometry(const QSharedPointer& newGeometry); - + glm::vec3 _scale; glm::vec3 _offset; @@ -257,21 +255,21 @@ protected: bool _snapModelToRegistrationPoint; /// is the model's offset automatically adjusted to a registration point in model space bool _snappedToRegistrationPoint; /// are we currently snapped to a registration point glm::vec3 _registrationPoint = glm::vec3(0.5f); /// the point in model space our center is snapped to - + bool _showTrueJointTransforms; class MeshState { public: QVector clusterMatrices; }; - + QVector _meshStates; - + // returns 'true' if needs fullUpdate after geometry change bool updateGeometry(); virtual void initJointStates(QVector states); - + void setScaleInternal(const glm::vec3& scale); void scaleToFit(); void snapToRegistrationPoint(); @@ -298,11 +296,11 @@ protected: /// the original position /// \return true if the joint was found bool restoreJointPosition(int jointIndex, float fraction = 1.0f, float priority = 0.0f); - + /// Computes and returns the extended length of the limb terminating at the specified joint and starting at the joint's /// first free ancestor. float getLimbLength(int jointIndex) const; - + /// Allow sub classes to force invalidating the bboxes void invalidCalculatedMeshBoxes() { _calculatedMeshBoxesValid = false; @@ -316,28 +314,25 @@ protected: // hook for derived classes to be notified when setUrl invalidates the current model. virtual void onInvalidate() {}; -protected slots: void geometryRefreshed(); - + private: - - friend class AnimationHandle; - + void applyNextGeometry(); void deleteGeometry(); QVector createJointStates(const FBXGeometry& geometry); void initJointTransforms(); - + QSharedPointer _nextGeometry; float _lodDistance; float _lodHysteresis; float _nextLODHysteresis; QSharedPointer _collisionGeometry; - + float _pupilDilation; QVector _blendshapeCoefficients; - + QUrl _url; QUrl _collisionUrl; bool _isVisible; @@ -347,10 +342,6 @@ private: gpu::Batch _renderBatch; QVector > > _dilatedTextures; - - QSet _animationHandles; - - QList _runningAnimations; QVector _blendedBlendshapeCoefficients; int _blendNumber; @@ -375,12 +366,12 @@ private: QHash, AABox> _calculatedMeshPartBoxes; // world coordinate AABoxes for all sub mesh part boxes QHash, qint64> _calculatedMeshPartOffset; bool _calculatedMeshPartOffsetValid; - - + + bool _calculatedMeshPartBoxesValid; QVector _calculatedMeshBoxes; // world coordinate AABoxes for all sub mesh boxes bool _calculatedMeshBoxesValid; - + QVector< QVector > _calculatedMeshTriangles; // world coordinate triangles for all sub meshes bool _calculatedMeshTrianglesValid; QMutex _mutex; @@ -420,10 +411,10 @@ private: IS_SHADOW_FLAG, IS_MIRROR_FLAG, //THis means that the mesh is rendered mirrored, not the same as "Rear view mirror" IS_WIREFRAME_FLAG, - + NUM_FLAGS, }; - + enum Flag { IS_TRANSLUCENT = (1 << IS_TRANSLUCENT_FLAG), HAS_LIGHTMAP = (1 << HAS_LIGHTMAP_FLAG), @@ -489,7 +480,7 @@ private: RenderKey(int bitmask) : _flags(bitmask) {} }; - + class RenderPipeline { public: gpu::PipelinePointer _pipeline; @@ -503,7 +494,7 @@ private: public: typedef RenderKey Key; - + void addRenderPipeline(Key key, gpu::ShaderPointer& vertexShader, gpu::ShaderPointer& pixelShader); void initLocations(gpu::ShaderPointer& program, Locations& locations); @@ -511,8 +502,8 @@ private: static RenderPipelineLib _renderPipelineLib; bool _renderCollisionHull; - - + + QSet> _transparentRenderItems; QSet> _opaqueRenderItems; QMap _renderItems; From 9593668110ee4971971ee006b0df244619e1b60c Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 23 Jul 2015 15:08:22 -0700 Subject: [PATCH 060/242] Bring animation file headers up to date. --- libraries/animation/src/AnimationCache.cpp | 2 +- libraries/animation/src/AnimationCache.h | 2 +- libraries/animation/src/AnimationHandle.cpp | 2 +- libraries/animation/src/AnimationHandle.h | 2 +- libraries/animation/src/AnimationLoop.cpp | 2 +- libraries/animation/src/AnimationLoop.h | 2 +- libraries/animation/src/AnimationObject.cpp | 2 +- libraries/animation/src/AnimationObject.h | 2 +- libraries/animation/src/JointState.cpp | 2 +- libraries/animation/src/JointState.h | 2 +- libraries/animation/src/Rig.cpp | 2 +- libraries/animation/src/Rig.h | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/libraries/animation/src/AnimationCache.cpp b/libraries/animation/src/AnimationCache.cpp index 99224f7dce..fef20b3cdb 100644 --- a/libraries/animation/src/AnimationCache.cpp +++ b/libraries/animation/src/AnimationCache.cpp @@ -1,6 +1,6 @@ // // AnimationCache.cpp -// libraries/script-engine/src/ +// libraries/animation/src/ // // Created by Andrzej Kapolka on 4/14/14. // Copyright (c) 2014 High Fidelity, Inc. All rights reserved. diff --git a/libraries/animation/src/AnimationCache.h b/libraries/animation/src/AnimationCache.h index c90c4c9225..840d7a0355 100644 --- a/libraries/animation/src/AnimationCache.h +++ b/libraries/animation/src/AnimationCache.h @@ -1,6 +1,6 @@ // // AnimationCache.h -// libraries/script-engine/src/ +// libraries/animation/src/ // // Created by Andrzej Kapolka on 4/14/14. // Copyright (c) 2014 High Fidelity, Inc. All rights reserved. diff --git a/libraries/animation/src/AnimationHandle.cpp b/libraries/animation/src/AnimationHandle.cpp index d11dcacfc6..996b8cb1fb 100644 --- a/libraries/animation/src/AnimationHandle.cpp +++ b/libraries/animation/src/AnimationHandle.cpp @@ -1,6 +1,6 @@ // // AnimationHandle.cpp -// interface/src/renderer +// libraries/animation/src/ // // Created by Andrzej Kapolka on 10/18/13. // Copyright 2013 High Fidelity, Inc. diff --git a/libraries/animation/src/AnimationHandle.h b/libraries/animation/src/AnimationHandle.h index a8c9d800a4..9075118f43 100644 --- a/libraries/animation/src/AnimationHandle.h +++ b/libraries/animation/src/AnimationHandle.h @@ -1,6 +1,6 @@ // // AnimationHandle.h -// interface/src/renderer +// libraries/animation/src/ // // Created by Andrzej Kapolka on 10/18/13. // Copyright 2013 High Fidelity, Inc. diff --git a/libraries/animation/src/AnimationLoop.cpp b/libraries/animation/src/AnimationLoop.cpp index 43e049f851..a2a27170c2 100644 --- a/libraries/animation/src/AnimationLoop.cpp +++ b/libraries/animation/src/AnimationLoop.cpp @@ -1,6 +1,6 @@ // // AnimationLoop.cpp -// libraries/animation +// libraries/animation/src/ // // Created by Brad Hefta-Gaub on 11/12/14. // Copyright (c) 2014 High Fidelity, Inc. All rights reserved. diff --git a/libraries/animation/src/AnimationLoop.h b/libraries/animation/src/AnimationLoop.h index d4537c4656..02161544ba 100644 --- a/libraries/animation/src/AnimationLoop.h +++ b/libraries/animation/src/AnimationLoop.h @@ -1,6 +1,6 @@ // // AnimationLoop.h -// libraries/script-engine/src/ +// libraries/animation/src/ // // Created by Brad Hefta-Gaub on 11/12/14. // Copyright (c) 2014 High Fidelity, Inc. All rights reserved. diff --git a/libraries/animation/src/AnimationObject.cpp b/libraries/animation/src/AnimationObject.cpp index ede1e82623..25a5743121 100644 --- a/libraries/animation/src/AnimationObject.cpp +++ b/libraries/animation/src/AnimationObject.cpp @@ -1,6 +1,6 @@ // // AnimationObject.cpp -// libraries/script-engine/src/ +// libraries/animation/src/ // // Created by Andrzej Kapolka on 4/17/14. // Copyright (c) 2014 High Fidelity, Inc. All rights reserved. diff --git a/libraries/animation/src/AnimationObject.h b/libraries/animation/src/AnimationObject.h index 078fc31fb3..aa69e78ceb 100644 --- a/libraries/animation/src/AnimationObject.h +++ b/libraries/animation/src/AnimationObject.h @@ -1,6 +1,6 @@ // // AnimationObject.h -// libraries/script-engine/src/ +// libraries/animation/src/ // // Created by Andrzej Kapolka on 4/17/14. // Copyright (c) 2014 High Fidelity, Inc. All rights reserved. diff --git a/libraries/animation/src/JointState.cpp b/libraries/animation/src/JointState.cpp index a82a57f0ed..8f14342e80 100644 --- a/libraries/animation/src/JointState.cpp +++ b/libraries/animation/src/JointState.cpp @@ -1,6 +1,6 @@ // // JointState.cpp -// interface/src/renderer +// libraries/animation/src/ // // Created by Andrzej Kapolka on 10/18/13. // Copyright 2013 High Fidelity, Inc. diff --git a/libraries/animation/src/JointState.h b/libraries/animation/src/JointState.h index 0ef84e50c4..f15590e5f2 100644 --- a/libraries/animation/src/JointState.h +++ b/libraries/animation/src/JointState.h @@ -1,6 +1,6 @@ // // JointState.h -// interface/src/renderer +// libraries/animation/src/ // // Created by Andrzej Kapolka on 10/18/13. // Copyright 2013 High Fidelity, Inc. diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 0a889fce88..4466bdbe26 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1,6 +1,6 @@ // // Rig.cpp -// libraries/script-engine/src/ +// libraries/animation/src/ // // Created by Howard Stearns, Seth Alves, Anthony Thibault, Andrew Meadows on 7/15/15. // Copyright (c) 2015 High Fidelity, Inc. All rights reserved. diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index e60116a35e..7e209841a1 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -1,6 +1,6 @@ // // Rig.h -// libraries/script-engine/src/ +// libraries/animation/src/ // // Produces animation data and hip placement for the current timestamp. // From 81e0a1e629be09cdd811225e40a6399bf32b7d40 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 23 Jul 2015 15:09:06 -0700 Subject: [PATCH 061/242] Better name for signal. --- libraries/avatars/src/AvatarData.cpp | 2 +- libraries/avatars/src/AvatarData.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 942dbeeaf7..aab760810f 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1087,7 +1087,7 @@ void AvatarData::setJointMappingsFromNetworkReply() { } networkReply->deleteLater(); - emit jointsLoaded(); + emit jointMappingLoaded(); } void AvatarData::sendAvatarDataPacket() { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 285460651a..60c643eff9 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -313,7 +313,7 @@ public: bool shouldDie() const { return _owningAvatarMixer.isNull() || getUsecsSinceLastUpdate() > AVATAR_SILENCE_THRESHOLD_USECS; } signals: - void jointsLoaded(); // So that test cases or anyone waiting on asynchronous loading can be informed. + void jointMappingLoaded(); // So that test cases or anyone waiting on asynchronous loading can be informed. public slots: void sendAvatarDataPacket(); From eea3ce4369237b32be6d61993057c9e5588709d6 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 23 Jul 2015 15:11:21 -0700 Subject: [PATCH 062/242] Make an actual rig in the test case. --- tests/rig/src/RigTests.cpp | 61 +++++++++++++++++++++++++++----------- tests/rig/src/RigTests.h | 6 ++-- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/tests/rig/src/RigTests.cpp b/tests/rig/src/RigTests.cpp index 37f7a2bbab..100d70ed25 100644 --- a/tests/rig/src/RigTests.cpp +++ b/tests/rig/src/RigTests.cpp @@ -40,30 +40,55 @@ */ #include -//#include "FSTReader.h" -// There are two good ways we could organize this: -// 1. Create a MyAvatar the same way that Interface does, and poke at it. -// We can't do that because MyAvatar (and even Avatar) are in interface, not a library, and our build system won't allow that dependency. -// 2. Create just the minimum skeleton in the most direct way possible, using only very basic library APIs (such as fbx). -// I don't think we can do that because not everything we need is exposed directly from, e.g., the fst and fbx readers. -// So here we do neither. Using as much as we can from AvatarData (which is in the avatar and further requires network and audio), and -// duplicating whatever other code we need from (My)Avatar. Ugh. We may refactor that later, but right now, cleaning this up is not on our critical path. +#include + #include "AvatarData.h" +#include "OBJReader.h" +#include "FBXReader.h" + +#include "AvatarRig.h" // We might later test Rig vs AvatarRig separately, but for now, we're concentrating on the main use case. #include "RigTests.h" QTEST_MAIN(RigTests) void RigTests::initTestCase() { - AvatarData avatar; - QEventLoop loop; // Create an event loop that will quit when we get the finished signal - QObject::connect(&avatar, &AvatarData::jointsLoaded, &loop, &QEventLoop::quit); - avatar.setSkeletonModelURL(QUrl("https://hifi-public.s3.amazonaws.com/marketplace/contents/4a690585-3fa3-499e-9f8b-fd1226e561b1/e47e6898027aa40f1beb6adecc6a7db5.fst")); // Zach - //std::cout << "sleep start" << std::endl; - loop.exec(); // Nothing is going to happen on this whole run thread until we get this - _rig = new Rig(); -} -void RigTests::dummyPassTest() { + // There are two good ways we could organize this: + // 1. Create a MyAvatar the same way that Interface does, and poke at it. + // We can't do that because MyAvatar (and even Avatar) are in interface, not a library, and our build system won't allow that dependency. + // 2. Create just the minimum skeleton in the most direct way possible, using only very basic library APIs (such as fbx). + // I don't think we can do that because not everything we need is exposed directly from, e.g., the fst and fbx readers. + // So here we do neither. Using as much as we can from AvatarData (which is in the avatar and further requires network and audio), and + // duplicating whatever other code we need from (My)Avatar. Ugh. We may refactor that later, but right now, cleaning this up is not on our critical path. + + // Joint mapping from fst + auto avatar = std::make_shared(); + QEventLoop loop; // Create an event loop that will quit when we get the finished signal + QObject::connect(avatar.get(), &AvatarData::jointMappingLoaded, &loop, &QEventLoop::quit); + avatar->setSkeletonModelURL(QUrl("https://hifi-public.s3.amazonaws.com/marketplace/contents/4a690585-3fa3-499e-9f8b-fd1226e561b1/e47e6898027aa40f1beb6adecc6a7db5.fst")); // Zach fst + loop.exec(); // Blocking all further tests until signalled. + + // Joint geometry from fbx. + QUrl fbxUrl("https://s3.amazonaws.com/hifi-public/models/skeletons/Zack/Zack.fbx"); + QNetworkReply* netReply = OBJReader().request(fbxUrl, false); // Just a convenience hack for synchronoud http request + QCOMPARE(netReply->isFinished() && (netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200), true); + FBXGeometry geometry = readFBX(netReply->readAll(), QVariantHash()); + QCOMPARE(geometry.joints.count(), avatar->getJointNames().count()); + + QVector jointStates; + for (int i = 0; i < geometry.joints.size(); ++i) { + const FBXJoint& joint = geometry.joints[i]; + JointState state; + state.setFBXJoint(&joint); + jointStates.append(state); + } + + _rig = std::make_shared(); + _rig->initJointStates(jointStates, glm::mat4()); + std::cout << "Rig is ready " << geometry.joints.count() << " joints " << std::endl; + } + +/*void RigTests::dummyPassTest() { bool x = true; std::cout << "dummyPassTest x=" << x << std::endl; QCOMPARE(x, true); @@ -73,4 +98,4 @@ void RigTests::dummyFailTest() { bool x = false; std::cout << "dummyFailTest x=" << x << std::endl; QCOMPARE(x, true); -} +}*/ diff --git a/tests/rig/src/RigTests.h b/tests/rig/src/RigTests.h index 9f8ba22eb8..1ce692b858 100644 --- a/tests/rig/src/RigTests.h +++ b/tests/rig/src/RigTests.h @@ -46,11 +46,11 @@ class RigTests : public QObject { private slots: void initTestCase(); - void dummyPassTest(); - void dummyFailTest(); + /*void dummyPassTest(); + void dummyFailTest();*/ private: - Rig* _rig; + RigPointer _rig; }; #endif // hifi_RigTests_h From 33c97a18337fdbf46a5713aad8a03bf7ca882c93 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 23 Jul 2015 15:14:10 -0700 Subject: [PATCH 063/242] get rid of _firstPersonSkeletonModel in MyAvatar. add flag in Rig for joints being dirty so Model knows when to recompute meshes --- interface/src/avatar/MyAvatar.cpp | 25 +----- interface/src/avatar/MyAvatar.h | 2 - interface/src/avatar/SkeletonModel.cpp | 90 ++++++++------------- interface/src/avatar/SkeletonModel.h | 7 +- libraries/animation/src/AnimationHandle.cpp | 6 +- libraries/animation/src/Rig.cpp | 89 ++++++++++++++++---- libraries/animation/src/Rig.h | 31 +++++-- libraries/render-utils/src/Model.cpp | 24 +----- libraries/render-utils/src/Model.h | 3 - 9 files changed, 137 insertions(+), 140 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 178ebc0487..d46870b479 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -103,11 +103,8 @@ MyAvatar::MyAvatar(RigPointer rig) : _realWorldFieldOfView("realWorldFieldOfView", DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), _rig(rig), - _firstPersonSkeletonModel(this, nullptr, rig), _prevShouldDrawHead(true) { - _firstPersonSkeletonModel.setIsFirstPerson(true); - ShapeCollider::initDispatchTable(); for (int i = 0; i < MAX_DRIVE_KEYS; i++) { _driveKeys[i] = 0.0f; @@ -141,7 +138,6 @@ QByteArray MyAvatar::toByteArray() { void MyAvatar::reset() { _skeletonModel.reset(); - _firstPersonSkeletonModel.reset(); getHead()->reset(); _targetVelocity = glm::vec3(0.0f); @@ -200,7 +196,6 @@ void MyAvatar::simulate(float deltaTime) { { PerformanceTimer perfTimer("skeleton"); _skeletonModel.simulate(deltaTime); - _firstPersonSkeletonModel.simulate(deltaTime); } if (!_skeletonModel.hasSkeleton()) { @@ -1028,15 +1023,8 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { if (_useFullAvatar) { _skeletonModel.setVisibleInScene(_prevShouldDrawHead, scene); - - const QUrl DEFAULT_SKELETON_MODEL_URL = QUrl::fromLocalFile(PathUtils::resourcesPath() + "meshes/defaultAvatar_body.fst"); - _firstPersonSkeletonModel.setURL(_skeletonModelURL, DEFAULT_SKELETON_MODEL_URL, true, !isMyAvatar()); - _firstPersonSkeletonModel.setVisibleInScene(!_prevShouldDrawHead, scene); } else { _skeletonModel.setVisibleInScene(true, scene); - - _firstPersonSkeletonModel.setVisibleInScene(false, scene); - _firstPersonSkeletonModel.reset(); } } @@ -1254,23 +1242,14 @@ void MyAvatar::preRender(RenderArgs* renderArgs) { const bool shouldDrawHead = shouldRenderHead(renderArgs); _skeletonModel.initWhenReady(scene); - if (_useFullAvatar) { - _firstPersonSkeletonModel.initWhenReady(scene); - } if (shouldDrawHead != _prevShouldDrawHead) { if (_useFullAvatar) { - if (shouldDrawHead) { - _skeletonModel.setVisibleInScene(true, scene); - _firstPersonSkeletonModel.setVisibleInScene(false, scene); - } else { - _skeletonModel.setVisibleInScene(false, scene); - _firstPersonSkeletonModel.setVisibleInScene(true, scene); - } + _skeletonModel.setVisibleInScene(true, scene); + _rig->setFirstPerson(!shouldDrawHead); } else { getHead()->getFaceModel().setVisibleInScene(shouldDrawHead, scene); } - } _prevShouldDrawHead = shouldDrawHead; } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 0c598c21be..c6cb48878f 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -287,8 +287,6 @@ private: QString _fullAvatarModelName; RigPointer _rig; - // used for rendering when in first person view or when in an HMD. - SkeletonModel _firstPersonSkeletonModel; bool _prevShouldDrawHead; }; diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 2d9db1abb7..0ee3898a20 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -40,8 +40,7 @@ SkeletonModel::SkeletonModel(Avatar* owningAvatar, QObject* parent, RigPointer r _standingFoot(NO_FOOT), _standingOffset(0.0f), _clampedFootPosition(0.0f), - _headClipDistance(DEFAULT_NEAR_CLIP), - _isFirstPerson(false) + _headClipDistance(DEFAULT_NEAR_CLIP) { assert(_rig); assert(_owningAvatar); @@ -54,7 +53,7 @@ SkeletonModel::~SkeletonModel() { void SkeletonModel::initJointStates(QVector states) { const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - _boundingRadius = _rig->initJointStates(states, parentTransform); + _boundingRadius = _rig->initJointStates(states, parentTransform, geometry.neckJointIndex); // Determine the default eye position for avatar scale = 1.0 int headJointIndex = _geometry->getFBXGeometry().headJointIndex; @@ -98,6 +97,29 @@ void SkeletonModel::initJointStates(QVector states) { const float PALM_PRIORITY = DEFAULT_PRIORITY; const float LEAN_PRIORITY = DEFAULT_PRIORITY; + +void SkeletonModel::updateClusterMatrices() { + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + glm::mat4 modelToWorld = glm::mat4_cast(_rotation); + for (int i = 0; i < _meshStates.size(); i++) { + MeshState& state = _meshStates[i]; + const FBXMesh& mesh = geometry.meshes.at(i); + if (_showTrueJointTransforms) { + for (int j = 0; j < mesh.clusters.size(); j++) { + const FBXCluster& cluster = mesh.clusters.at(j); + state.clusterMatrices[j] = + modelToWorld * _rig->getJointTransform(cluster.jointIndex) * cluster.inverseBindMatrix; + } + } else { + for (int j = 0; j < mesh.clusters.size(); j++) { + const FBXCluster& cluster = mesh.clusters.at(j); + state.clusterMatrices[j] = + modelToWorld * _rig->getJointVisibleTransform(cluster.jointIndex) * cluster.inverseBindMatrix; + } + } + } +} + void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { setTranslation(_owningAvatar->getSkeletonPosition()); static const glm::quat refOrientation = glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); @@ -154,8 +176,11 @@ void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { } } - if (_isFirstPerson) { - cauterizeHead(); + // if (_isFirstPerson) { + // cauterizeHead(); + // updateClusterMatrices(); + // } + if (_rig->getJointsAreDirty()) { updateClusterMatrices(); } @@ -764,7 +789,7 @@ void SkeletonModel::resetShapePositionsToDefaultPose() { // geometry or joints have not yet been created return; } - + const FBXGeometry& geometry = _geometry->getFBXGeometry(); if (geometry.joints.isEmpty()) { return; @@ -820,7 +845,7 @@ void SkeletonModel::renderBoundingCollisionShapes(gpu::Batch& batch, float alpha // draw a green cylinder between the two points glm::vec3 origin(0.0f); - Avatar::renderJointConnectingCone(batch, origin, axis, _boundingShape.getRadius(), _boundingShape.getRadius(), + Avatar::renderJointConnectingCone(batch, origin, axis, _boundingShape.getRadius(), _boundingShape.getRadius(), glm::vec4(0.6f, 0.8f, 0.6f, alpha)); } @@ -828,56 +853,5 @@ bool SkeletonModel::hasSkeleton() { return isActive() ? _geometry->getFBXGeometry().rootJointIndex != -1 : false; } -void SkeletonModel::initHeadBones() { - _headBones.clear(); - const FBXGeometry& fbxGeometry = _geometry->getFBXGeometry(); - const int neckJointIndex = fbxGeometry.neckJointIndex; - std::queue q; - q.push(neckJointIndex); - _headBones.push_back(neckJointIndex); - - // fbxJoints only hold links to parents not children, so we have to do a bit of extra work here. - while (q.size() > 0) { - int jointIndex = q.front(); - for (int i = 0; i < fbxGeometry.joints.size(); i++) { - const FBXJoint& fbxJoint = fbxGeometry.joints[i]; - if (jointIndex == fbxJoint.parentIndex) { - _headBones.push_back(i); - q.push(i); - } - } - q.pop(); - } -} - -void SkeletonModel::invalidateHeadBones() { - _headBones.clear(); -} - -void SkeletonModel::cauterizeHead() { - if (isActive()) { - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - const int neckJointIndex = geometry.neckJointIndex; - if (neckJointIndex > 0 && neckJointIndex < _rig->getJointStateCount()) { - - // lazy init of headBones - if (_headBones.size() == 0) { - initHeadBones(); - } - - // preserve the translation for the neck - // glm::vec4 trans = _jointStates[neckJointIndex].getTransform()[3]; - glm::vec4 trans = _rig->getJointTransform(neckJointIndex)[3]; - glm::vec4 zero(0, 0, 0, 0); - for (const int &i : _headBones) { - glm::mat4 newXform(zero, zero, zero, trans); - _rig->setJointTransform(i, newXform); - _rig->setJointVisibleTransform(i, newXform); - } - } - } -} - void SkeletonModel::onInvalidate() { - invalidateHeadBones(); } diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index c4cd43b4df..0678ae48d4 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -112,9 +112,6 @@ public: float getHeadClipDistance() const { return _headClipDistance; } - void setIsFirstPerson(bool value) { _isFirstPerson = value; } - bool getIsFirstPerson() const { return _isFirstPerson; } - virtual void onInvalidate() override; signals: @@ -138,6 +135,7 @@ protected: void maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, int index); void maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, int index); + void updateClusterMatrices(); void cauterizeHead(); void initHeadBones(); void invalidateHeadBones(); @@ -173,9 +171,6 @@ private: glm::vec3 _clampedFootPosition; float _headClipDistance; // Near clip distance to use if no separate head model - - bool _isFirstPerson; - std::vector _headBones; }; #endif // hifi_SkeletonModel_h diff --git a/libraries/animation/src/AnimationHandle.cpp b/libraries/animation/src/AnimationHandle.cpp index d11dcacfc6..8978abd93c 100644 --- a/libraries/animation/src/AnimationHandle.cpp +++ b/libraries/animation/src/AnimationHandle.cpp @@ -176,8 +176,7 @@ void AnimationHandle::replaceMatchingPriorities(float newPriority) { for (int i = 0; i < _jointMappings.size(); i++) { int mapping = _jointMappings.at(i); if (mapping != -1) { - JointState state = _rig->getJointState(mapping); - if (_priority == state._animationPriority) { + if (_priority == _rig->getJointAnimatinoPriority(mapping)) { _rig->setJointAnimatinoPriority(mapping, newPriority); } } @@ -188,8 +187,7 @@ void AnimationHandle::restoreJoints() { for (int i = 0; i < _jointMappings.size(); i++) { int mapping = _jointMappings.at(i); if (mapping != -1) { - JointState state = _rig->getJointState(mapping); - _rig->restoreJointRotation(mapping, 1.0f, state._animationPriority); + _rig->restoreJointRotation(mapping, 1.0f, _rig->getJointAnimatinoPriority(mapping)); } } } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 0a889fce88..d42f65df32 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include "AnimationHandle.h" #include "Rig.h" @@ -48,9 +50,9 @@ void Rig::deleteAnimations() { } } - -float Rig::initJointStates(QVector states, glm::mat4 parentTransform) { +float Rig::initJointStates(QVector states, glm::mat4 parentTransform, int neckJointIndex) { _jointStates = states; + _neckJointIndex = neckJointIndex; initJointTransforms(parentTransform); int numStates = _jointStates.size(); @@ -66,6 +68,8 @@ float Rig::initJointStates(QVector states, glm::mat4 parentTransform _jointStates[i].slaveVisibleTransform(); } + initHeadBones(); + return radius; } @@ -106,7 +110,8 @@ JointState Rig::getJointState(int jointIndex) const { if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return JointState(); } - return _jointStates[jointIndex]; + // return _jointStates[jointIndex]; + return maybeCauterizeHead(jointIndex); } bool Rig::getJointStateRotation(int index, glm::quat& rotation) const { @@ -144,6 +149,13 @@ void Rig::clearJointAnimationPriority(int index) { } } +float Rig::getJointAnimatinoPriority(int index) { + if (index != -1 && index < _jointStates.size()) { + return _jointStates[index]._animationPriority; + } + return 0.0f; +} + void Rig::setJointAnimatinoPriority(int index, float newPriority) { if (index != -1 && index < _jointStates.size()) { _jointStates[index]._animationPriority = newPriority; @@ -173,7 +185,8 @@ bool Rig::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, return false; } // position is in world-frame - position = translation + rotation * _jointStates[jointIndex].getPosition(); + // position = translation + rotation * _jointStates[jointIndex].getPosition(); + position = translation + rotation * maybeCauterizeHead(jointIndex).getPosition(); return true; } @@ -182,7 +195,7 @@ bool Rig::getJointPosition(int jointIndex, glm::vec3& position) const { return false; } // position is in model-frame - position = extractTranslation(_jointStates[jointIndex].getTransform()); + position = extractTranslation(maybeCauterizeHead(jointIndex).getTransform()); return true; } @@ -190,7 +203,7 @@ bool Rig::getJointRotationInWorldFrame(int jointIndex, glm::quat& result, const if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return false; } - result = rotation * _jointStates[jointIndex].getRotation(); + result = rotation * maybeCauterizeHead(jointIndex).getRotation(); return true; } @@ -198,7 +211,7 @@ bool Rig::getJointRotation(int jointIndex, glm::quat& rotation) const { if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return false; } - rotation = _jointStates[jointIndex].getRotation(); + rotation = maybeCauterizeHead(jointIndex).getRotation(); return true; } @@ -206,7 +219,7 @@ bool Rig::getJointCombinedRotation(int jointIndex, glm::quat& result, const glm: if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return false; } - result = rotation * _jointStates[jointIndex].getRotation(); + result = rotation * maybeCauterizeHead(jointIndex).getRotation(); return true; } @@ -217,7 +230,7 @@ bool Rig::getVisibleJointPositionInWorldFrame(int jointIndex, glm::vec3& positio return false; } // position is in world-frame - position = translation + rotation * _jointStates[jointIndex].getVisiblePosition(); + position = translation + rotation * maybeCauterizeHead(jointIndex).getVisiblePosition(); return true; } @@ -225,7 +238,7 @@ bool Rig::getVisibleJointRotationInWorldFrame(int jointIndex, glm::quat& result, if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return false; } - result = rotation * _jointStates[jointIndex].getVisibleRotation(); + result = rotation * maybeCauterizeHead(jointIndex).getVisibleRotation(); return true; } @@ -233,14 +246,14 @@ glm::mat4 Rig::getJointTransform(int jointIndex) const { if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return glm::mat4(); } - return _jointStates[jointIndex].getTransform(); + return maybeCauterizeHead(jointIndex).getTransform(); } glm::mat4 Rig::getJointVisibleTransform(int jointIndex) const { if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return glm::mat4(); } - return _jointStates[jointIndex].getVisibleTransform(); + return maybeCauterizeHead(jointIndex).getVisibleTransform(); } void Rig::simulateInternal(float deltaTime, glm::mat4 parentTransform) { @@ -492,7 +505,7 @@ glm::vec3 Rig::getJointDefaultTranslationInConstrainedFrame(int jointIndex) { if (jointIndex == -1 || _jointStates.isEmpty()) { return glm::vec3(); } - return _jointStates[jointIndex].getDefaultTranslationInConstrainedFrame(); + return maybeCauterizeHead(jointIndex).getDefaultTranslationInConstrainedFrame(); } glm::quat Rig::setJointRotationInConstrainedFrame(int jointIndex, glm::quat targetRotation, float priority, bool constrain) { @@ -537,5 +550,53 @@ glm::quat Rig::getJointDefaultRotationInParentFrame(int jointIndex) { if (jointIndex == -1 || _jointStates.isEmpty()) { return glm::quat(); } - return _jointStates[jointIndex].getDefaultRotationInParentFrame(); + return maybeCauterizeHead(jointIndex).getDefaultRotationInParentFrame(); +} + +void Rig::initHeadBones() { + if (_neckJointIndex == -1) { + return; + } + _headBones.clear(); + std::queue q; + q.push(_neckJointIndex); + _headBones.push_back(_neckJointIndex); + + // fbxJoints only hold links to parents not children, so we have to do a bit of extra work here. + while (q.size() > 0) { + int jointIndex = q.front(); + for (int i = 0; i < _jointStates.size(); i++) { + const FBXJoint& fbxJoint = _jointStates[i].getFBXJoint(); + if (jointIndex == fbxJoint.parentIndex) { + _headBones.push_back(i); + q.push(i); + } + } + q.pop(); + } +} + +JointState Rig::maybeCauterizeHead(int jointIndex) const { + // if (_headBones.contains(jointIndex)) { + // XXX fix this... make _headBones a hash? add a flag to JointState? + if (_neckJointIndex != -1 && + _isFirstPerson && + std::find(_headBones.begin(), _headBones.end(), jointIndex) != _headBones.end()) { + glm::vec4 trans = _jointStates[jointIndex].getTransform()[3]; + glm::vec4 zero(0, 0, 0, 0); + glm::mat4 newXform(zero, zero, zero, trans); + JointState jointStateCopy = _jointStates[jointIndex]; + jointStateCopy.setTransform(newXform); + jointStateCopy.setVisibleTransform(newXform); + return jointStateCopy; + } else { + return _jointStates[jointIndex]; + } +} + +void Rig::setFirstPerson(bool isFirstPerson) { + if (_isFirstPerson != isFirstPerson) { + _isFirstPerson = isFirstPerson; + _jointsAreDirty = true; + } } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index e60116a35e..1073bd6dab 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -11,14 +11,18 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // /* TBD: - - What iare responsibilities of Animation/AnimationPointer/AnimationCache/AnimationDetails/AnimationObject/AnimationLoop? + - What are responsibilities of Animation/AnimationPointer/AnimationCache/AnimationDetails/AnimationObject/AnimationLoop? Is there common/copied code (e.g., ScriptableAvatar::update)? - - How do attachments interact with the physics of the attached entity? E.g., do hand joints need to reflect held object physics? - - Is there any current need (i.e., for initial campatability) to have multiple animations per role (e.g., idle) with the system choosing randomly? + - How do attachments interact with the physics of the attached entity? E.g., do hand joints need to reflect held object + physics? + - Is there any current need (i.e., for initial campatability) to have multiple animations per role (e.g., idle) with the + system choosing randomly? - Distribute some doc from here to the right files if it turns out to be correct: - - AnimationDetails is a script-useable copy of animation state, analogous to EntityItemProperties, but without anything equivalent to editEntity. - But what's the intended difference vs AnimationObjection? Maybe AnimationDetails is to Animation as AnimationObject is to AnimationPointer? + - AnimationDetails is a script-useable copy of animation state, analogous to EntityItemProperties, but without anything + equivalent to editEntity. + But what's the intended difference vs AnimationObjection? Maybe AnimationDetails is to Animation as AnimationObject + is to AnimationPointer? */ #ifndef __hifi__Rig__ @@ -51,7 +55,7 @@ public: const QList& getRunningAnimations() const { return _runningAnimations; } void deleteAnimations(); - float initJointStates(QVector states, glm::mat4 parentTransform); + float initJointStates(QVector states, glm::mat4 parentTransform, int neckJointIndex); bool jointStatesEmpty() { return _jointStates.isEmpty(); }; int getJointStateCount() const { return _jointStates.size(); } @@ -60,11 +64,12 @@ public: void reset(const QVector& fbxJoints); bool getJointStateRotation(int index, glm::quat& rotation) const; void applyJointRotationDelta(int jointIndex, const glm::quat& delta, bool constrain, float priority); - JointState getJointState(int jointIndex) const; + JointState getJointState(int jointIndex) const; // XXX bool getVisibleJointState(int index, glm::quat& rotation) const; void clearJointState(int index); void clearJointStates(); void clearJointAnimationPriority(int index); + float getJointAnimatinoPriority(int index); void setJointAnimatinoPriority(int index, float newPriority); void setJointState(int index, bool valid, const glm::quat& rotation, float priority); void restoreJointRotation(int index, float fraction, float priority); @@ -102,11 +107,23 @@ public: virtual void updateJointState(int index, glm::mat4 parentTransform) = 0; virtual void updateFaceJointState(int index, glm::mat4 parentTransform) = 0; + virtual void setFirstPerson(bool isFirstPerson); + virtual bool getIsFirstPerson() const { return _isFirstPerson; } + + bool getJointsAreDirty() { return _jointsAreDirty; } + protected: QVector _jointStates; QSet _animationHandles; QList _runningAnimations; + + JointState maybeCauterizeHead(int jointIndex) const; + void initHeadBones(); + bool _isFirstPerson = false; + std::vector _headBones; + bool _jointsAreDirty = false; + int _neckJointIndex = -1; }; #endif /* defined(__hifi__Rig__) */ diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index bd33e75207..c769f6bbe5 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -478,7 +478,7 @@ bool Model::updateGeometry() { void Model::initJointStates(QVector states) { const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - _boundingRadius = _rig->initJointStates(states, parentTransform); + _boundingRadius = _rig->initJointStates(states, parentTransform, geometry.neckJointIndex); } bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, @@ -1341,28 +1341,6 @@ void Model::simulate(float deltaTime, bool fullUpdate) { } } -void Model::updateClusterMatrices() { - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - glm::mat4 modelToWorld = glm::mat4_cast(_rotation); - for (int i = 0; i < _meshStates.size(); i++) { - MeshState& state = _meshStates[i]; - const FBXMesh& mesh = geometry.meshes.at(i); - if (_showTrueJointTransforms) { - for (int j = 0; j < mesh.clusters.size(); j++) { - const FBXCluster& cluster = mesh.clusters.at(j); - state.clusterMatrices[j] = - modelToWorld * _rig->getJointTransform(cluster.jointIndex) * cluster.inverseBindMatrix; - } - } else { - for (int j = 0; j < mesh.clusters.size(); j++) { - const FBXCluster& cluster = mesh.clusters.at(j); - state.clusterMatrices[j] = - modelToWorld * _rig->getJointVisibleTransform(cluster.jointIndex) * cluster.inverseBindMatrix; - } - } - } -} - void Model::simulateInternal(float deltaTime) { // update the world space transforms for all joints diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 30c5211990..d7178a389a 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -308,9 +308,6 @@ protected: _calculatedMeshTrianglesValid = false; } - // rebuild the clusterMatrices from the current jointStates - void updateClusterMatrices(); - // hook for derived classes to be notified when setUrl invalidates the current model. virtual void onInvalidate() {}; From cc26f5165bba9714d12edfc9e1eeedf2b0a30fa8 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 23 Jul 2015 15:40:33 -0700 Subject: [PATCH 064/242] Supply neck index per new protocol. --- tests/rig/src/RigTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rig/src/RigTests.cpp b/tests/rig/src/RigTests.cpp index 100d70ed25..95ddc86b3f 100644 --- a/tests/rig/src/RigTests.cpp +++ b/tests/rig/src/RigTests.cpp @@ -84,7 +84,7 @@ void RigTests::initTestCase() { } _rig = std::make_shared(); - _rig->initJointStates(jointStates, glm::mat4()); + _rig->initJointStates(jointStates, glm::mat4(), geometry.neckJointIndex); std::cout << "Rig is ready " << geometry.joints.count() << " joints " << std::endl; } From 216c499d1472abecc29968bd9f2ba6b0aa7be7ac Mon Sep 17 00:00:00 2001 From: Niraj Venkat Date: Thu, 23 Jul 2015 18:53:43 -0700 Subject: [PATCH 065/242] HAO (Horrendous Ambient Occlusion) --- .../src/AmbientOcclusionEffect.cpp | 114 ++++++++++++------ .../render-utils/src/AmbientOcclusionEffect.h | 17 +++ .../render-utils/src/ambient_occlusion.slf | 104 ++++++++++++---- .../render-utils/src/occlusion_blend.slf | 13 +- .../render-utils/src/occlusion_result.slf | 23 ++++ 5 files changed, 211 insertions(+), 60 deletions(-) create mode 100644 libraries/render-utils/src/occlusion_result.slf diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index 2b1113d61b..e4220243ff 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -2,7 +2,7 @@ // AmbientOcclusionEffect.cpp // interface/src/renderer // -// Created by Andrzej Kapolka on 7/14/13. +// Created by Niraj Venkat on 7/14/13. // Copyright 2013 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -19,10 +19,11 @@ #include #include -#include "AbstractViewStateInterface.h" +#include "gpu/StandardShaderLib.h" #include "AmbientOcclusionEffect.h" #include "RenderUtil.h" #include "TextureCache.h" +#include "FramebufferCache.h" #include "DependencyManager.h" #include "ViewFrustum.h" #include "GeometryCache.h" @@ -34,9 +35,8 @@ #include "gaussian_blur_frag.h" #include "occlusion_blend_vert.h" #include "occlusion_blend_frag.h" - -const int ROTATION_WIDTH = 4; -const int ROTATION_HEIGHT = 4; +//#include "occlusion_result_vert.h" +#include "occlusion_result_frag.h" /* void AmbientOcclusionEffect::init(AbstractViewStateInterface* viewState) { @@ -197,6 +197,8 @@ const gpu::PipelinePointer& AmbientOcclusion::getOcclusionPipeline() { _gBiasLoc = program->getUniforms().findLocation("g_bias"); _gSampleRadiusLoc = program->getUniforms().findLocation("g_sample_rad"); _gIntensityLoc = program->getUniforms().findLocation("g_intensity"); + _bufferWidthLoc = program->getUniforms().findLocation("bufferWidth"); + _bufferHeightLoc = program->getUniforms().findLocation("bufferHeight"); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); @@ -209,7 +211,7 @@ const gpu::PipelinePointer& AmbientOcclusion::getOcclusionPipeline() { // Link the occlusion FBO to texture _occlusionBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, - DependencyManager::get()->getFrameBufferSize().width(), DependencyManager::get()->getFrameBufferSize().height())); + DependencyManager::get()->getFrameBufferSize().width(), DependencyManager::get()->getFrameBufferSize().height())); auto format = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); auto width = _occlusionBuffer->getWidth(); auto height = _occlusionBuffer->getHeight(); @@ -242,7 +244,7 @@ const gpu::PipelinePointer& AmbientOcclusion::getVBlurPipeline() { // Link the horizontal blur FBO to texture _vBlurBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, - DependencyManager::get()->getFrameBufferSize().width(), DependencyManager::get()->getFrameBufferSize().height())); + DependencyManager::get()->getFrameBufferSize().width(), DependencyManager::get()->getFrameBufferSize().height())); auto format = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); auto width = _vBlurBuffer->getWidth(); auto height = _vBlurBuffer->getHeight(); @@ -275,7 +277,7 @@ const gpu::PipelinePointer& AmbientOcclusion::getHBlurPipeline() { // Link the horizontal blur FBO to texture _hBlurBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, - DependencyManager::get()->getFrameBufferSize().width(), DependencyManager::get()->getFrameBufferSize().height())); + DependencyManager::get()->getFrameBufferSize().width(), DependencyManager::get()->getFrameBufferSize().height())); auto format = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); auto width = _hBlurBuffer->getWidth(); auto height = _hBlurBuffer->getHeight(); @@ -295,6 +297,9 @@ const gpu::PipelinePointer& AmbientOcclusion::getBlendPipeline() { gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vs, ps)); gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("blurredOcclusionTexture"), 0)); + slotBindings.insert(gpu::Shader::Binding(std::string("colorTexture"), 1)); + gpu::Shader::makeProgram(*program, slotBindings); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); @@ -303,17 +308,16 @@ const gpu::PipelinePointer& AmbientOcclusion::getBlendPipeline() { // Blend on transparent state->setBlendFunction(true, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::DEST_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ZERO); + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); - // Link the horizontal blur FBO to texture - _hBlurBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, - DependencyManager::get()->getFrameBufferSize().width(), DependencyManager::get()->getFrameBufferSize().height())); + // Link the blend FBO to texture + _blendBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, + DependencyManager::get()->getFrameBufferSize().width(), DependencyManager::get()->getFrameBufferSize().height())); auto format = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); - auto width = _hBlurBuffer->getWidth(); - auto height = _hBlurBuffer->getHeight(); + auto width = _blendBuffer->getWidth(); + auto height = _blendBuffer->getHeight(); auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _hBlurTexture = gpu::TexturePointer(gpu::Texture::create2D(format, width, height, defaultSampler)); + _blendTexture = gpu::TexturePointer(gpu::Texture::create2D(format, width, height, defaultSampler)); // Good to go add the brand new pipeline _blendPipeline.reset(gpu::Pipeline::create(program, state)); @@ -321,38 +325,67 @@ const gpu::PipelinePointer& AmbientOcclusion::getBlendPipeline() { return _blendPipeline; } +const gpu::PipelinePointer& AmbientOcclusion::getAOResultPipeline() { + if (!_AOResultPipeline) { + auto vs = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(ambient_occlusion_vert))); + auto ps = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(occlusion_result_frag))); + gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vs, ps)); + //gpu::ShaderPointer program = gpu::StandardShaderLib::getProgram(gpu::StandardShaderLib::getDrawTransformUnitQuadVS(), gpu::StandardShaderLib::getDrawTexturePS()); + + gpu::Shader::BindingSet slotBindings; + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + state->setDepthTest(false, false, gpu::LESS_EQUAL); + + // Blend on transparent + state->setBlendFunction(false, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); + + // Good to go add the brand new pipeline + _AOResultPipeline.reset(gpu::Pipeline::create(program, state)); + } + return _AOResultPipeline; +} + void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext) { - - // create a simple pipeline that does: - assert(renderContext->args); assert(renderContext->args->_viewFrustum); RenderArgs* args = renderContext->args; auto& scene = sceneContext->_scene; - // Allright, something to render let's do it gpu::Batch batch; glm::mat4 projMat; Transform viewMat; args->_viewFrustum->evalProjectionMatrix(projMat); args->_viewFrustum->evalViewTransform(viewMat); - if (args->_renderMode == RenderArgs::MIRROR_RENDER_MODE) { - viewMat.postScale(glm::vec3(-1.0f, 1.0f, 1.0f)); - } batch.setProjectionTransform(projMat); batch.setViewTransform(viewMat); batch.setModelTransform(Transform()); // Occlusion step getOcclusionPipeline(); - batch.setResourceTexture(0, DependencyManager::get()->getPrimaryDepthTexture()); - batch.setResourceTexture(1, DependencyManager::get()->getPrimaryNormalTexture()); + batch.setResourceTexture(0, DependencyManager::get()->getPrimaryDepthTexture()); + batch.setResourceTexture(1, DependencyManager::get()->getPrimaryNormalTexture()); _occlusionBuffer->setRenderBuffer(0, _occlusionTexture); batch.setFramebuffer(_occlusionBuffer); - // bind the first gpu::Pipeline we need - for calculating occlusion buffer + // Occlusion uniforms + g_scale = 1.0f; + g_bias = 1.0f; + g_sample_rad = 1.0f; + g_intensity = 1.0f; + + // Bind the first gpu::Pipeline we need - for calculating occlusion buffer batch.setPipeline(getOcclusionPipeline()); + batch._glUniform1f(_gScaleLoc, g_scale); + batch._glUniform1f(_gBiasLoc, g_bias); + batch._glUniform1f(_gSampleRadiusLoc, g_sample_rad); + batch._glUniform1f(_gIntensityLoc, g_intensity); + batch._glUniform1f(_bufferWidthLoc, DependencyManager::get()->getFrameBufferSize().width()); + batch._glUniform1f(_bufferHeightLoc, DependencyManager::get()->getFrameBufferSize().height()); glm::vec4 color(0.0f, 0.0f, 0.0f, 1.0f); glm::vec2 bottomLeft(-1.0f, -1.0f); @@ -367,7 +400,7 @@ void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, cons _vBlurBuffer->setRenderBuffer(0, _vBlurTexture); batch.setFramebuffer(_vBlurBuffer); - // bind the second gpu::Pipeline we need - for calculating blur buffer + // Bind the second gpu::Pipeline we need - for calculating blur buffer batch.setPipeline(getVBlurPipeline()); DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); @@ -378,29 +411,38 @@ void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, cons _hBlurBuffer->setRenderBuffer(0, _hBlurTexture); batch.setFramebuffer(_hBlurBuffer); - // bind the third gpu::Pipeline we need - for calculating blur buffer + // Bind the third gpu::Pipeline we need - for calculating blur buffer batch.setPipeline(getHBlurPipeline()); DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); - // "Blend" step - batch.setResourceTexture(0, _occlusionTexture); - batch.setFramebuffer(DependencyManager::get()->getPrimaryFramebuffer()); + // Blend step + getBlendPipeline(); + batch.setResourceTexture(0, _hBlurTexture); + batch.setResourceTexture(1, DependencyManager::get()->getPrimaryColorTexture()); + batch.setFramebuffer(_blendBuffer); - // bind the fourth gpu::Pipeline we need - for blending the primary framefuffer with blurred occlusion texture + // Bind the fourth gpu::Pipeline we need - for blending the primary color buffer with blurred occlusion texture batch.setPipeline(getBlendPipeline()); + DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); + + // Final AO result step + getAOResultPipeline(); + batch.setResourceTexture(0, _hBlurTexture); + batch.setFramebuffer(DependencyManager::get()->getPrimaryFramebuffer()); + + // Bind the fifth gpu::Pipeline we need - for displaying the blended texture + batch.setPipeline(getAOResultPipeline()); + glm::vec2 bottomLeftSmall(0.5f, -1.0f); glm::vec2 topRightSmall(1.0f, -0.5f); - DependencyManager::get()->renderQuad(batch, bottomLeftSmall, topRightSmall, texCoordTopLeft, texCoordBottomRight, color); + DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); // Ready to render args->_context->syncCache(); - renderContext->args->_context->syncCache(); args->_context->render((batch)); // need to fetch forom the z buffer and render something in a new render target a result that combine the z and produce a fake AO result - - } diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.h b/libraries/render-utils/src/AmbientOcclusionEffect.h index d38624f24c..ec0314016c 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.h +++ b/libraries/render-utils/src/AmbientOcclusionEffect.h @@ -28,21 +28,38 @@ public: const gpu::PipelinePointer& getHBlurPipeline(); const gpu::PipelinePointer& getVBlurPipeline(); const gpu::PipelinePointer& getBlendPipeline(); + const gpu::PipelinePointer& getAOResultPipeline(); private: + // Uniforms for AO + gpu::int32 _gScaleLoc; + gpu::int32 _gBiasLoc; + gpu::int32 _gSampleRadiusLoc; + gpu::int32 _gIntensityLoc; + gpu::int32 _bufferWidthLoc; + gpu::int32 _bufferHeightLoc; + float g_scale; + float g_bias; + float g_sample_rad; + float g_intensity; + gpu::PipelinePointer _occlusionPipeline; gpu::PipelinePointer _hBlurPipeline; gpu::PipelinePointer _vBlurPipeline; gpu::PipelinePointer _blendPipeline; + gpu::PipelinePointer _AOResultPipeline; gpu::FramebufferPointer _occlusionBuffer; gpu::FramebufferPointer _hBlurBuffer; gpu::FramebufferPointer _vBlurBuffer; + gpu::FramebufferPointer _blendBuffer; gpu::TexturePointer _occlusionTexture; gpu::TexturePointer _hBlurTexture; gpu::TexturePointer _vBlurTexture; + gpu::TexturePointer _blendTexture; + }; #endif // hifi_AmbientOcclusionEffect_h diff --git a/libraries/render-utils/src/ambient_occlusion.slf b/libraries/render-utils/src/ambient_occlusion.slf index 10b687ad1f..023fe2552c 100644 --- a/libraries/render-utils/src/ambient_occlusion.slf +++ b/libraries/render-utils/src/ambient_occlusion.slf @@ -14,36 +14,96 @@ <@include DeferredBufferWrite.slh@> +<@include gpu/Transform.slh@> + +<$declareStandardTransform()$> + varying vec2 varTexcoord; uniform sampler2D depthTexture; uniform sampler2D normalTexture; -float getRandom(vec2 co) { - return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); +uniform float g_scale; +uniform float g_bias; +uniform float g_sample_rad; +uniform float g_intensity; +uniform float bufferWidth; +uniform float bufferHeight; + +#define SAMPLE_COUNT 4 + +float getRandom(vec2 uv) { + return fract(sin(dot(uv.xy ,vec2(12.9898,78.233))) * 43758.5453); } -/* -float doAmbientOcclusion(vec2 tcoord, vec2 uv, vec3 p, vec3 cnorm) { - vec3 diff = getPosition(tcoord + uv) - p; - vec3 v = normalize(diff); - float d = length(diff) * g_scale; - return max(0.0, dot(cnorm, v) - g_bias) * (1.0/(1.0 + d)) * g_intensity; -} -*/ + void main(void) { + vec3 sampleKernel[4] = { vec3(0.2, 0.0, 0.0), + vec3(0.0, 0.2, 0.0), + vec3(0.0, 0.0, 0.2), + vec3(0.2, 0.2, 0.2) }; - vec4 depthColor = texture2D(depthTexture, varTexcoord.xy); - vec4 normalColor = texture2D(normalTexture, varTexcoord.xy); - float z = depthColor.r; // fetch the z-value from our depth texture - float n = 1.0; // the near plane - float f = 30.0; // the far plane - float c = (2.0 * n) / (f + n - z * (f - n)); // convert to linear values + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); - vec4 linearizedDepthColor = vec4(c, c, c, 1.0); - gl_FragColor = mix(linearizedDepthColor, normalColor, 0.5); - //gl_FragColor = linearizedDepthColor; + vec3 eyeDir = vec3(0.0); + vec3 cameraPositionWorldSpace; + <$transformEyeToWorldDir(cam, eyeDir, cameraPositionWorldSpace)$> - //vec3 p = getPosition(i.uv); - //vec3 n = getNormal(i.uv); - //vec2 rand = vec2(getRandom(varTexcoord.xy), getRandom(varTexcoord.yx)); + vec4 depthColor = texture2D(depthTexture, varTexcoord); + + // z in non linear range [0,1] + float depthVal = depthColor.r; + // conversion into NDC [-1,1] + float zNDC = depthVal * 2.0 - 1.0; + float n = 1.0; // the near plane + float f = 30.0; // the far plane + float l = -1.0; // left + float r = 1.0; // right + float b = -1.0; // bottom + float t = 1.0; // top + + // conversion into eye space + float zEye = 2*f*n / (zNDC*(f-n)-(f+n)); + // Converting from pixel coordinates to NDC + float xNDC = gl_FragCoord.x/bufferWidth * 2.0 - 1.0; + float yNDC = gl_FragCoord.y/bufferHeight * 2.0 - 1.0; + // Unprojecting X and Y from NDC to eye space + float xEye = -zEye*(xNDC*(r-l)+(r+l))/(2.0*n); + float yEye = -zEye*(yNDC*(t-b)+(t+b))/(2.0*n); + vec3 currentFragEyeSpace = vec3(xEye, yEye, zEye); + vec3 currentFragWorldSpace; + <$transformEyeToWorldDir(cam, currentFragEyeSpace, currentFragWorldSpace)$> + + vec3 cameraToPositionRay = normalize(currentFragWorldSpace - cameraPositionWorldSpace); + vec3 origin = cameraToPositionRay * depthVal + cameraPositionWorldSpace; + + vec3 normal = normalize(texture2D(normalTexture, varTexcoord).xyz); + //normal = normalize(normal * normalMatrix); + + vec3 rvec = vec3(getRandom(varTexcoord.xy), getRandom(varTexcoord.yx), getRandom(varTexcoord.xx)) * 2.0 - 1.0; + vec3 tangent = normalize(rvec - normal * dot(rvec, normal)); + vec3 bitangent = cross(normal, tangent); + mat3 tbn = mat3(tangent, bitangent, normal); + + float occlusion = 0.0; + + for (int i = 0; i < SAMPLE_COUNT; ++i) { + vec3 samplePos = origin + (tbn * sampleKernel[i]) * g_sample_rad; + vec4 offset = cam._projectionViewUntranslated * vec4(samplePos, 1.0); + + offset.xy = (offset.xy / offset.w) * 0.5 + 0.5; + float depth = length(samplePos - cameraPositionWorldSpace); + + float sampleDepthVal = texture2D(depthTexture, offset.xy).r; + + float rangeDelta = abs(depthVal - sampleDepthVal); + float rangeCheck = smoothstep(0.0, 1.0, g_sample_rad / rangeDelta); + + occlusion += rangeCheck * step(sampleDepthVal, depth); + } + + occlusion = 1.0 - occlusion / float(SAMPLE_COUNT); + occlusion = clamp(pow(occlusion, g_intensity), 0.0, 1.0); + gl_FragColor = vec4(occlusion, occlusion, occlusion, 1.0); } + diff --git a/libraries/render-utils/src/occlusion_blend.slf b/libraries/render-utils/src/occlusion_blend.slf index 30d3173f90..64103e38c9 100644 --- a/libraries/render-utils/src/occlusion_blend.slf +++ b/libraries/render-utils/src/occlusion_blend.slf @@ -17,8 +17,17 @@ varying vec2 varTexcoord; uniform sampler2D blurredOcclusionTexture; +uniform sampler2D colorTexture; void main(void) { - vec4 depthColor = texture2D(blurredOcclusionTexture, varTexcoord.xy); - gl_FragColor = depthColor; + vec4 occlusionColor = texture2D(blurredOcclusionTexture, varTexcoord); + vec4 currentColor = texture2D(colorTexture, varTexcoord); + + if(occlusionColor.r == 1.0) { + gl_FragColor = currentColor; + } + else { + gl_FragColor = mix(occlusionColor, currentColor, 0.5); + } + gl_FragColor = occlusionColor; } diff --git a/libraries/render-utils/src/occlusion_result.slf b/libraries/render-utils/src/occlusion_result.slf new file mode 100644 index 0000000000..3b7e895dc6 --- /dev/null +++ b/libraries/render-utils/src/occlusion_result.slf @@ -0,0 +1,23 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// occlusion_result.frag +// fragment shader +// +// Created by Niraj Venkat on 7/23/15. +// 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 DeferredBufferWrite.slh@> + +varying vec2 varTexcoord; + +uniform sampler2D resultTexture; + +void main(void) { + gl_FragColor = texture2D(resultTexture, varTexcoord); +} From 28e6a4ac6331f9810c300eb032b0ae44c4eb6d89 Mon Sep 17 00:00:00 2001 From: "Kevin M. Thomas" Date: Fri, 24 Jul 2015 09:36:35 -0400 Subject: [PATCH 066/242] Updating master as old job related to AudioClient.h was incorretly included with current job. --- libraries/audio-client/src/AudioClient.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 3a85adbc97..aeea7c07c1 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -55,9 +55,9 @@ static const int NUM_AUDIO_CHANNELS = 2; -static const int DEFAULT_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 20; +static const int DEFAULT_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 3; static const int MIN_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 1; -static const int MAX_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 40; +static const int MAX_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 20; #if defined(Q_OS_ANDROID) || defined(Q_OS_WIN) static const int DEFAULT_AUDIO_OUTPUT_STARVE_DETECTION_ENABLED = false; #else From 3893add1487143b6d79f1012f612a250242f29a1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 24 Jul 2015 10:43:24 -0700 Subject: [PATCH 067/242] Fix setting WebWindow's width and height Width and height parameters now set the WebWindow's width and height instead of its minimum size, when not part of a tool window. --- interface/src/scripting/WebWindowClass.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/scripting/WebWindowClass.cpp b/interface/src/scripting/WebWindowClass.cpp index 3bd7e390ec..f187de95d2 100644 --- a/interface/src/scripting/WebWindowClass.cpp +++ b/interface/src/scripting/WebWindowClass.cpp @@ -57,7 +57,7 @@ WebWindowClass::WebWindowClass(const QString& title, const QString& url, int wid } else { auto dialogWidget = new QDialog(Application::getInstance()->getWindow(), Qt::Window); dialogWidget->setWindowTitle(title); - dialogWidget->setMinimumSize(width, height); + dialogWidget->resize(width, height); connect(dialogWidget, &QDialog::finished, this, &WebWindowClass::hasClosed); auto layout = new QVBoxLayout(dialogWidget); From fc612ab8cd03997dc767df3e4e72a0240ab22ffe Mon Sep 17 00:00:00 2001 From: Niraj Venkat Date: Fri, 24 Jul 2015 11:29:52 -0700 Subject: [PATCH 068/242] Merge conflict fix --- interface/src/Application.cpp | 5 ----- libraries/render-utils/src/AmbientOcclusionEffect.cpp | 1 - 2 files changed, 6 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 4d2fcbce11..3ba66cf2a0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3287,11 +3287,6 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se sceneInterface->setEngineDrawnOverlay3DItems(engineRC->_numDrawnOverlay3DItems); } - //Render the sixense lasers - if (Menu::getInstance()->isOptionChecked(MenuOption::SixenseLasers)) { - _myAvatar->renderLaserPointers(*renderArgs->_batch); - } - if (!selfAvatarOnly) { // give external parties a change to hook in { diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index f46ae1719b..d8b56f5ba1 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -21,7 +21,6 @@ #include "gpu/StandardShaderLib.h" #include "AmbientOcclusionEffect.h" -#include "RenderUtil.h" #include "TextureCache.h" #include "FramebufferCache.h" #include "DependencyManager.h" From 67c9a33cc0a0b57d79d957adb176c56976dbe68e Mon Sep 17 00:00:00 2001 From: "Kevin M. Thomas" Date: Fri, 24 Jul 2015 15:16:02 -0400 Subject: [PATCH 069/242] Update Menu.cpp --- interface/src/Menu.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index d5f4d78f5b..91ae6a4d02 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -487,14 +487,10 @@ Menu::Menu() { #endif MenuWrapper* networkMenu = developerMenu->addMenu("Network"); -<<<<<<< HEAD addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); - addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableNackPackets, 0, false); -======= addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableNackPackets, 0, false, qApp->getEntityEditPacketSender(), SLOT(toggleNackPackets())); ->>>>>>> f3dc159e336b7b580bbd39b367802b0099e66ccb addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableActivityLogger, 0, From ddada17b08bfa12219eb7f04b5f60966b28c9c46 Mon Sep 17 00:00:00 2001 From: "Kevin M. Thomas" Date: Fri, 24 Jul 2015 15:27:16 -0400 Subject: [PATCH 070/242] move jsstreamplayer.html into example/html move jssstreamplayer.js script into example/example/audio --- examples/jsstreamplayer.js | 142 ------------------------------------- 1 file changed, 142 deletions(-) delete mode 100644 examples/jsstreamplayer.js diff --git a/examples/jsstreamplayer.js b/examples/jsstreamplayer.js deleted file mode 100644 index 6bd941f677..0000000000 --- a/examples/jsstreamplayer.js +++ /dev/null @@ -1,142 +0,0 @@ -// -// #20622: JS Stream Player -// ************************* -// -// Created by Kevin M. Thomas and Thoys 07/17/15. -// Copyright 2015 High Fidelity, Inc. -// kevintown.net -// -// JavaScript for the High Fidelity interface that creates a stream player with a UI and keyPressEvents for adding a stream URL in addition to play, stop and volume functionality. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - - -// Declare variables and set up new WebWindow. -var stream; -var volume = 1; -var streamWindow = new WebWindow('Stream', "https://dl.dropboxusercontent.com/u/17344741/jsstreamplayer/jsstreamplayer.html", 0, 0, false); - -// Set up toggleStreamURLButton overlay. -var toggleStreamURLButton = Overlays.addOverlay("text", { - x: 76, - y: 275, - width: 40, - height: 28, - backgroundColor: {red: 0, green: 0, blue: 0}, - color: {red: 255, green: 255, blue: 0}, - font: {size: 15}, - topMargin: 8, - text: " URL" -}); - -// Set up toggleStreamPlayButton overlay. -var toggleStreamPlayButton = Overlays.addOverlay("text", { - x: 122, - y: 275, - width: 38, - height: 28, - backgroundColor: { red: 0, green: 0, blue: 0}, - color: { red: 255, green: 255, blue: 0}, - font: {size: 15}, - topMargin: 8, - text: " Play" -}); - -// Set up toggleStreamStopButton overlay. -var toggleStreamStopButton = Overlays.addOverlay("text", { - x: 166, - y: 275, - width: 40, - height: 28, - backgroundColor: { red: 0, green: 0, blue: 0}, - color: { red: 255, green: 255, blue: 0}, - font: {size: 15}, - topMargin: 8, - text: " Stop" -}); - -// Set up increaseVolumeButton overlay. -var toggleIncreaseVolumeButton = Overlays.addOverlay("text", { - x: 211, - y: 275, - width: 18, - height: 28, - backgroundColor: { red: 0, green: 0, blue: 0}, - color: { red: 255, green: 255, blue: 0}, - font: {size: 15}, - topMargin: 8, - text: " +" -}); - -// Set up decreaseVolumeButton overlay. -var toggleDecreaseVolumeButton = Overlays.addOverlay("text", { - x: 234, - y: 275, - width: 15, - height: 28, - backgroundColor: { red: 0, green: 0, blue: 0}, - color: { red: 255, green: 255, blue: 0}, - font: {size: 15}, - topMargin: 8, - text: " -" -}); - -// Function that adds mousePressEvent functionality to connect UI to enter stream URL, play and stop stream. -function mousePressEvent(event) { - if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleStreamURLButton) { - stream = Window.prompt("Enter Stream: "); - var streamJSON = { - action: "changeStream", - stream: stream - } - streamWindow.eventBridge.emitScriptEvent(JSON.stringify(streamJSON)); - } - if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleStreamPlayButton) { - var streamJSON = { - action: "changeStream", - stream: stream - } - streamWindow.eventBridge.emitScriptEvent(JSON.stringify(streamJSON)); - } - if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleStreamStopButton) { - var streamJSON = { - action: "changeStream", - stream: "" - } - streamWindow.eventBridge.emitScriptEvent(JSON.stringify(streamJSON)); - } - if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleIncreaseVolumeButton) { - volume += 0.2; - var volumeJSON = { - action: "changeVolume", - volume: volume - } - streamWindow.eventBridge.emitScriptEvent(JSON.stringify(volumeJSON)); - } - if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleDecreaseVolumeButton) { - volume -= 0.2; - var volumeJSON = { - action: "changeVolume", - volume: volume - } - streamWindow.eventBridge.emitScriptEvent(JSON.stringify(volumeJSON)); - } -} - -// Call function. -Controller.mousePressEvent.connect(mousePressEvent); -streamWindow.setVisible(false); - -// Function to delete overlays upon exit. -function onScriptEnding() { - Overlays.deleteOverlay(toggleStreamURLButton); - Overlays.deleteOverlay(toggleStreamPlayButton); - Overlays.deleteOverlay(toggleStreamStopButton); - Overlays.deleteOverlay(toggleIncreaseVolumeButton); - Overlays.deleteOverlay(toggleDecreaseVolumeButton); -} - -// Call function. -Script.scriptEnding.connect(onScriptEnding); \ No newline at end of file From d4d5c9f9359729cf97bec92cd5a920f1730529b6 Mon Sep 17 00:00:00 2001 From: "Kevin M. Thomas" Date: Fri, 24 Jul 2015 15:43:50 -0400 Subject: [PATCH 071/242] Created a folder in examples called "zones" then moved files to location. --- examples/example/jsstreamplayerdomain-zone.js | 204 ------------------ examples/html/jsstreamplayerdomain-zone.html | 42 ---- 2 files changed, 246 deletions(-) delete mode 100644 examples/example/jsstreamplayerdomain-zone.js delete mode 100644 examples/html/jsstreamplayerdomain-zone.html diff --git a/examples/example/jsstreamplayerdomain-zone.js b/examples/example/jsstreamplayerdomain-zone.js deleted file mode 100644 index 543a95b839..0000000000 --- a/examples/example/jsstreamplayerdomain-zone.js +++ /dev/null @@ -1,204 +0,0 @@ -// -// #20628: JS Stream Player Domain-Zone -// ************************************* -// -// Created by Kevin M. Thomas and Thoys 07/20/15. -// Copyright 2015 High Fidelity, Inc. -// kevintown.net -// -// JavaScript for the High Fidelity interface that creates a stream player with a UI for playing a domain-zone specificed stream URL in addition to play, stop and volume functionality which is resident only in the domain-zone. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - - -// Declare variables and set up new WebWindow. -var zone = "Zone2"; -var stream = "http://listen.radionomy.com/80sMixTape"; -var volume; -var streamWindow = new WebWindow('Stream', "https://dl.dropboxusercontent.com/u/17344741/jsstreamplayer/jsstreamplayerdomain-zone.html", 0, 0, false); -var visible = false; - -// Set up toggleStreamPlayButton overlay. -var toggleStreamPlayButton = Overlays.addOverlay("text", { - x: 122, - y: 310, - width: 38, - height: 28, - backgroundColor: { red: 0, green: 0, blue: 0}, - color: { red: 255, green: 255, blue: 0}, - font: {size: 15}, - topMargin: 8, - visible: false, - text: " Play" -}); - -// Set up toggleStreamStopButton overlay. -var toggleStreamStopButton = Overlays.addOverlay("text", { - x: 166, - y: 310, - width: 40, - height: 28, - backgroundColor: { red: 0, green: 0, blue: 0}, - color: { red: 255, green: 255, blue: 0}, - font: {size: 15}, - topMargin: 8, - visible: false, - text: " Stop" -}); - -// Set up increaseVolumeButton overlay. -var toggleIncreaseVolumeButton = Overlays.addOverlay("text", { - x: 211, - y: 310, - width: 18, - height: 28, - backgroundColor: { red: 0, green: 0, blue: 0}, - color: { red: 255, green: 255, blue: 0}, - font: {size: 15}, - topMargin: 8, - visible: false, - text: " +" -}); - -// Set up decreaseVolumeButton overlay. -var toggleDecreaseVolumeButton = Overlays.addOverlay("text", { - x: 234, - y: 310, - width: 15, - height: 28, - backgroundColor: { red: 0, green: 0, blue: 0}, - color: { red: 255, green: 255, blue: 0}, - font: {size: 15}, - topMargin: 8, - visible: false, - text: " -" -}); - -// Function to change JSON object stream. -function changeStream(stream) { - var streamJSON = { - action: "changeStream", - stream: stream - } - streamWindow.eventBridge.emitScriptEvent(JSON.stringify(streamJSON)); -} - -// Function to change JSON object volume. -function changeVolume(volume) { - var volumeJSON = { - action: "changeVolume", - volume: volume - } - streamWindow.eventBridge.emitScriptEvent(JSON.stringify(volumeJSON)); -} - -// Function that adds mousePressEvent functionality to connect UI to enter stream URL, play and stop stream. -function mousePressEvent(event) { - if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleStreamPlayButton) { - changeStream(stream); - volume = 0.25; - changeVolume(volume); - } - if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleStreamStopButton) { - changeStream(""); - } - if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleIncreaseVolumeButton) { - volume += 0.25; - changeVolume(volume); - } - if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleDecreaseVolumeButton) { - volume -= 0.25; - changeVolume(volume); - } -} - -// Function checking bool if in proper zone. -function isOurZone(properties) { - return properties.type == "Zone" && properties.name == zone; -} - -// Function checking if avatar is in zone. -function isInZone() { - var entities = Entities.findEntities(MyAvatar.position, 10000); - - for (var i in entities) { - var properties = Entities.getEntityProperties(entities[i]); - - if (isOurZone(properties)) { - var minX = properties.position.x - (properties.dimensions.x / 2); - var maxX = properties.position.x + (properties.dimensions.x / 2); - var minY = properties.position.y - (properties.dimensions.y / 2); - var maxY = properties.position.y + (properties.dimensions.y / 2); - var minZ = properties.position.z - (properties.dimensions.z / 2); - var maxZ = properties.position.z + (properties.dimensions.z / 2); - - if (MyAvatar.position.x >= minX && MyAvatar.position.x <= maxX && MyAvatar.position.y >= minY && MyAvatar.position.y <= maxY && MyAvatar.position.z >= minZ && MyAvatar.position.z <= maxZ) { - return true; - } - } - } - return false; -} - -// Function to toggle visibile the overlay. -function toggleVisible(newVisibility) { - if (newVisibility != visible) { - visible = newVisibility; - Overlays.editOverlay(toggleStreamPlayButton, {visible: visible}); - Overlays.editOverlay(toggleStreamStopButton, {visible: visible}); - Overlays.editOverlay(toggleIncreaseVolumeButton, {visible: visible}); - Overlays.editOverlay(toggleDecreaseVolumeButton, {visible: visible}); - } -} - -// Function to check if avatar is in proper domain. -Window.domainChanged.connect(function() { - if (Window.location.hostname != "Music" && Window.location.hostname != "LiveMusic") { - Script.stop(); - } -}); - -// Function to check if avatar is within zone. -Entities.enterEntity.connect(function(entityID) { - print("Entered..." + JSON.stringify(entityID)); - var properties = Entities.getEntityProperties(entityID); - if (isOurZone(properties)) { - print("Entering Zone!"); - toggleVisible(true); - } -}) - -// Function to check if avatar is leaving zone. -Entities.leaveEntity.connect(function(entityID) { - print("Left..." + JSON.stringify(entityID)); - var properties = Entities.getEntityProperties(entityID); - if (isOurZone(properties)) { - print("Leaving Zone!"); - toggleVisible(false); - changeStream(""); - } -}) - -// Function to delete overlays upon exit. -function onScriptEnding() { - Overlays.deleteOverlay(toggleStreamPlayButton); - Overlays.deleteOverlay(toggleStreamStopButton); - Overlays.deleteOverlay(toggleIncreaseVolumeButton); - Overlays.deleteOverlay(toggleDecreaseVolumeButton); - changeStream(""); - streamWindow.deleteLater(); -} - -// Function call to ensure that if you log in to the zone visibility is true. -if (isInZone()) { - toggleVisible(true); -} - -// Connect mouse and hide WebWindow. -Controller.mousePressEvent.connect(mousePressEvent); -streamWindow.setVisible(false); - -// Call function upon ending script. -Script.scriptEnding.connect(onScriptEnding); \ No newline at end of file diff --git a/examples/html/jsstreamplayerdomain-zone.html b/examples/html/jsstreamplayerdomain-zone.html deleted file mode 100644 index 28b2202591..0000000000 --- a/examples/html/jsstreamplayerdomain-zone.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 4e298d815ddbb5ebcf7c81391454dffb1a33c13c Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 24 Jul 2015 14:07:32 -0700 Subject: [PATCH 072/242] Add the joint mapping necessary for playing animations in the rig. --- libraries/animation/src/AnimationHandle.cpp | 11 ++++++++++- libraries/animation/src/AnimationHandle.h | 4 ++-- libraries/animation/src/Rig.cpp | 11 +++++++++++ libraries/animation/src/Rig.h | 11 ++++++++++- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/libraries/animation/src/AnimationHandle.cpp b/libraries/animation/src/AnimationHandle.cpp index 51e8cb62d2..e8ac6e0a10 100644 --- a/libraries/animation/src/AnimationHandle.cpp +++ b/libraries/animation/src/AnimationHandle.cpp @@ -103,6 +103,15 @@ void AnimationHandle::setJointMappings(QVector jointMappings) { _jointMappings = jointMappings; } +QVector AnimationHandle::getJointMappings() { + if (_jointMappings.isEmpty()) { + QVector animationJoints = _animation->getGeometry().joints; + for (int i = 0; i < animationJoints.count(); i++) { + _jointMappings.append(_rig->indexOfJoint(animationJoints.at(i).name)); + } + } + return _jointMappings; +} void AnimationHandle::simulate(float deltaTime) { if (!_animation || !_animation->isLoaded()) { @@ -111,7 +120,7 @@ void AnimationHandle::simulate(float deltaTime) { _animationLoop.simulate(deltaTime); - if (_jointMappings.isEmpty()) { + if (getJointMappings().isEmpty()) { qDebug() << "AnimationHandle::simulate -- _jointMappings.isEmpty()"; return; } diff --git a/libraries/animation/src/AnimationHandle.h b/libraries/animation/src/AnimationHandle.h index 9075118f43..5d39682a0a 100644 --- a/libraries/animation/src/AnimationHandle.h +++ b/libraries/animation/src/AnimationHandle.h @@ -45,7 +45,7 @@ inline uint qHash(const std::weak_ptr& a, uint seed) { -/// Represents a handle to a model animation. +/// Represents a handle to a model animation. I.e., an Animation in use by a given Rig. class AnimationHandle : public QObject, public std::enable_shared_from_this { Q_OBJECT @@ -96,6 +96,7 @@ public: void setAnimationDetails(const AnimationDetails& details); void setJointMappings(QVector jointMappings); + QVector getJointMappings(); // computing if necessary void simulate(float deltaTime); void applyFrame(float frameIndex); void replaceMatchingPriorities(float newPriority); @@ -125,7 +126,6 @@ private: AnimationLoop _animationLoop; static QHash, QVector> _jointMappingsCache; - static QVector getJointMappings(const AnimationPointer& animation); }; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index e0b1625813..8f4047bc71 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -73,6 +73,17 @@ float Rig::initJointStates(QVector states, glm::mat4 parentTransform return radius; } +// We could build and cache a dictionary, too.... +// Should we be using .fst mapping instead/also? +int Rig::indexOfJoint(const QString& jointName) { + for (int i = 0; i < _jointStates.count(); i++) { + if (_jointStates[i].getFBXJoint().name == jointName) { + return i; + } + } + return -1; +} + void Rig::initJointTransforms(glm::mat4 parentTransform) { // compute model transforms diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 20110813fc..0af4073c96 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -10,7 +10,15 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* TBD: +/* + Things we want to be able to do, that I think we cannot do now: + * Stop an animation at a given time so that it can be examined visually or in a test harness. (I think we can already stop animation and set frame to a computed float? But does that move the bones?) + * Play two animations, blending between them. (Current structure just has one, under script control.) + * Fade in an animation over another. + * Apply IK, lean, head pointing or other overrides relative to previous position. + All of this depends on coordinated state. + + TBD: - What are responsibilities of Animation/AnimationPointer/AnimationCache/AnimationDetails/AnimationObject/AnimationLoop? Is there common/copied code (e.g., ScriptableAvatar::update)? - How do attachments interact with the physics of the attached entity? E.g., do hand joints need to reflect held object @@ -58,6 +66,7 @@ public: float initJointStates(QVector states, glm::mat4 parentTransform, int neckJointIndex); bool jointStatesEmpty() { return _jointStates.isEmpty(); }; int getJointStateCount() const { return _jointStates.size(); } + int indexOfJoint(const QString& jointName) ; void initJointTransforms(glm::mat4 parentTransform); void clearJointTransformTranslation(int jointIndex); From cff7bb703e8b397dc88c25efd363a440d21744b3 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 24 Jul 2015 14:08:30 -0700 Subject: [PATCH 073/242] Test evolution. --- tests/rig/src/RigTests.cpp | 48 ++++++++++++++++++++++++-------------- tests/rig/src/RigTests.h | 3 +-- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/tests/rig/src/RigTests.cpp b/tests/rig/src/RigTests.cpp index 95ddc86b3f..c5622ac501 100644 --- a/tests/rig/src/RigTests.cpp +++ b/tests/rig/src/RigTests.cpp @@ -61,25 +61,34 @@ void RigTests::initTestCase() { // So here we do neither. Using as much as we can from AvatarData (which is in the avatar and further requires network and audio), and // duplicating whatever other code we need from (My)Avatar. Ugh. We may refactor that later, but right now, cleaning this up is not on our critical path. - // Joint mapping from fst - auto avatar = std::make_shared(); + // Joint mapping from fst. FIXME: Do we need this??? + /*auto avatar = std::make_shared(); QEventLoop loop; // Create an event loop that will quit when we get the finished signal QObject::connect(avatar.get(), &AvatarData::jointMappingLoaded, &loop, &QEventLoop::quit); avatar->setSkeletonModelURL(QUrl("https://hifi-public.s3.amazonaws.com/marketplace/contents/4a690585-3fa3-499e-9f8b-fd1226e561b1/e47e6898027aa40f1beb6adecc6a7db5.fst")); // Zach fst - loop.exec(); // Blocking all further tests until signalled. + loop.exec();*/ // Blocking all further tests until signalled. // Joint geometry from fbx. +#define FROM_FILE "/Users/howardstearns/howardHiFi/Zack.fbx" +#ifdef FROM_FILE + QFile file(FROM_FILE); + QCOMPARE(file.open(QIODevice::ReadOnly), true); + FBXGeometry geometry = readFBX(file.readAll(), QVariantHash()); +#else QUrl fbxUrl("https://s3.amazonaws.com/hifi-public/models/skeletons/Zack/Zack.fbx"); - QNetworkReply* netReply = OBJReader().request(fbxUrl, false); // Just a convenience hack for synchronoud http request - QCOMPARE(netReply->isFinished() && (netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200), true); - FBXGeometry geometry = readFBX(netReply->readAll(), QVariantHash()); - QCOMPARE(geometry.joints.count(), avatar->getJointNames().count()); + QNetworkReply* reply = OBJReader().request(fbxUrl, false); // Just a convenience hack for synchronoud http request + auto fbxHttpCode = !reply->isFinished() ? -1 : reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + QCOMPARE(fbxHttpCode, 200); + FBXGeometry geometry = readFBX(reply->readAll(), QVariantHash()); +#endif + //QCOMPARE(geometry.joints.count(), avatar->getJointNames().count()); QVector jointStates; for (int i = 0; i < geometry.joints.size(); ++i) { - const FBXJoint& joint = geometry.joints[i]; + // Note that if the geometry is stack allocated and goes away, so will the joints. Hence the heap copy here. + FBXJoint* joint = new FBXJoint(geometry.joints[i]); JointState state; - state.setFBXJoint(&joint); + state.setFBXJoint(joint); jointStates.append(state); } @@ -88,14 +97,17 @@ void RigTests::initTestCase() { std::cout << "Rig is ready " << geometry.joints.count() << " joints " << std::endl; } -/*void RigTests::dummyPassTest() { - bool x = true; - std::cout << "dummyPassTest x=" << x << std::endl; - QCOMPARE(x, true); +void reportJoint(int index, JointState joint) { // Handy for debugging + std::cout << "\n"; + std::cout << index << " " << joint.getFBXJoint().name.toUtf8().data() << "\n"; + std::cout << " pos:" << joint.getPosition() << "/" << joint.getPositionInParentFrame() << " from " << joint.getParentIndex() << "\n"; + std::cout << " rot:" << safeEulerAngles(joint.getRotation()) << "/" << safeEulerAngles(joint.getRotationInParentFrame()) << "/" << safeEulerAngles(joint.getRotationInBindFrame()) << "\n"; + std::cout << "\n"; } -void RigTests::dummyFailTest() { - bool x = false; - std::cout << "dummyFailTest x=" << x << std::endl; - QCOMPARE(x, true); -}*/ +void RigTests::initialPoseArmsDown() { + for (int i = 0; i < _rig->getJointStateCount(); i++) { + JointState joint = _rig->getJointState(i); + reportJoint(i, joint); + } +} diff --git a/tests/rig/src/RigTests.h b/tests/rig/src/RigTests.h index 1ce692b858..4280c0a8fa 100644 --- a/tests/rig/src/RigTests.h +++ b/tests/rig/src/RigTests.h @@ -46,8 +46,7 @@ class RigTests : public QObject { private slots: void initTestCase(); - /*void dummyPassTest(); - void dummyFailTest();*/ + void initialPoseArmsDown(); private: RigPointer _rig; From 776d4747b2fc17fce67a8a8ef34c6e77f0adfb63 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Fri, 24 Jul 2015 14:47:44 -0700 Subject: [PATCH 074/242] Cleaning up the FBO cache and the output stage in general --- libraries/gpu/src/gpu/Batch.cpp | 59 ++++++++------- libraries/gpu/src/gpu/Batch.h | 25 +++---- libraries/gpu/src/gpu/GLBackend.cpp | 67 +---------------- libraries/gpu/src/gpu/GLBackend.h | 3 +- libraries/gpu/src/gpu/GLBackendOutput.cpp | 91 +++++++++++++++++++++-- 5 files changed, 127 insertions(+), 118 deletions(-) diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index 567ce66cd8..4ac33d8f14 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -106,36 +106,6 @@ void Batch::drawIndexedInstanced(uint32 nbInstances, Primitive primitiveType, ui _params.push_back(nbInstances); } -void Batch::clearFramebuffer(Framebuffer::Masks targets, const Vec4& color, float depth, int stencil, bool enableScissor) { - ADD_COMMAND(clearFramebuffer); - - _params.push_back(enableScissor); - _params.push_back(stencil); - _params.push_back(depth); - _params.push_back(color.w); - _params.push_back(color.z); - _params.push_back(color.y); - _params.push_back(color.x); - _params.push_back(targets); -} - -void Batch::clearColorFramebuffer(Framebuffer::Masks targets, const Vec4& color, bool enableScissor) { - clearFramebuffer(targets & Framebuffer::BUFFER_COLORS, color, 1.0f, 0, enableScissor); -} - -void Batch::clearDepthFramebuffer(float depth, bool enableScissor) { - clearFramebuffer(Framebuffer::BUFFER_DEPTH, Vec4(0.0f), depth, 0, enableScissor); -} - -void Batch::clearStencilFramebuffer(int stencil, bool enableScissor) { - clearFramebuffer(Framebuffer::BUFFER_STENCIL, Vec4(0.0f), 1.0f, stencil, enableScissor); -} - -void Batch::clearDepthStencilFramebuffer(float depth, int stencil, bool enableScissor) { - clearFramebuffer(Framebuffer::BUFFER_DEPTHSTENCIL, Vec4(0.0f), depth, stencil, enableScissor); -} - - void Batch::setInputFormat(const Stream::FormatPointer& format) { ADD_COMMAND(setInputFormat); @@ -255,6 +225,35 @@ void Batch::setFramebuffer(const FramebufferPointer& framebuffer) { } +void Batch::clearFramebuffer(Framebuffer::Masks targets, const Vec4& color, float depth, int stencil, bool enableScissor) { + ADD_COMMAND(clearFramebuffer); + + _params.push_back(enableScissor); + _params.push_back(stencil); + _params.push_back(depth); + _params.push_back(color.w); + _params.push_back(color.z); + _params.push_back(color.y); + _params.push_back(color.x); + _params.push_back(targets); +} + +void Batch::clearColorFramebuffer(Framebuffer::Masks targets, const Vec4& color, bool enableScissor) { + clearFramebuffer(targets & Framebuffer::BUFFER_COLORS, color, 1.0f, 0, enableScissor); +} + +void Batch::clearDepthFramebuffer(float depth, bool enableScissor) { + clearFramebuffer(Framebuffer::BUFFER_DEPTH, Vec4(0.0f), depth, 0, enableScissor); +} + +void Batch::clearStencilFramebuffer(int stencil, bool enableScissor) { + clearFramebuffer(Framebuffer::BUFFER_STENCIL, Vec4(0.0f), 1.0f, stencil, enableScissor); +} + +void Batch::clearDepthStencilFramebuffer(float depth, int stencil, bool enableScissor) { + clearFramebuffer(Framebuffer::BUFFER_DEPTHSTENCIL, Vec4(0.0f), depth, stencil, enableScissor); +} + void Batch::blit(const FramebufferPointer& src, const Vec4i& srcViewport, const FramebufferPointer& dst, const Vec4i& dstViewport) { ADD_COMMAND(blit); diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index acc1f6fdac..58fe03c9cf 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -54,15 +54,6 @@ public: void drawInstanced(uint32 nbInstances, Primitive primitiveType, uint32 nbVertices, uint32 startVertex = 0, uint32 startInstance = 0); void drawIndexedInstanced(uint32 nbInstances, Primitive primitiveType, uint32 nbIndices, uint32 startIndex = 0, uint32 startInstance = 0); - // Clear framebuffer layers - // Targets can be any of the render buffers contained in the Framebuffer - // Optionally the scissor test can be enabled locally for this command and to restrict the clearing command to the pixels contained in the scissor rectangle - void clearFramebuffer(Framebuffer::Masks targets, const Vec4& color, float depth, int stencil, bool enableScissor = false); - void clearColorFramebuffer(Framebuffer::Masks targets, const Vec4& color, bool enableScissor = false); // not a command, just a shortcut for clearFramebuffer, mask out targets to make sure it touches only color targets - void clearDepthFramebuffer(float depth, bool enableScissor = false); // not a command, just a shortcut for clearFramebuffer, it touches only depth target - void clearStencilFramebuffer(int stencil, bool enableScissor = false); // not a command, just a shortcut for clearFramebuffer, it touches only stencil target - void clearDepthStencilFramebuffer(float depth, int stencil, bool enableScissor = false); // not a command, just a shortcut for clearFramebuffer, it touches depth and stencil target - // Input Stage // InputFormat // InputBuffers @@ -105,8 +96,17 @@ public: // Framebuffer Stage void setFramebuffer(const FramebufferPointer& framebuffer); - void blit(const FramebufferPointer& src, const Vec4i& srcViewport, - const FramebufferPointer& dst, const Vec4i& dstViewport); + + // Clear framebuffer layers + // Targets can be any of the render buffers contained in the currnetly bound Framebuffer + // Optionally the scissor test can be enabled locally for this command and to restrict the clearing command to the pixels contained in the scissor rectangle + void clearFramebuffer(Framebuffer::Masks targets, const Vec4& color, float depth, int stencil, bool enableScissor = false); + void clearColorFramebuffer(Framebuffer::Masks targets, const Vec4& color, bool enableScissor = false); // not a command, just a shortcut for clearFramebuffer, mask out targets to make sure it touches only color targets + void clearDepthFramebuffer(float depth, bool enableScissor = false); // not a command, just a shortcut for clearFramebuffer, it touches only depth target + void clearStencilFramebuffer(int stencil, bool enableScissor = false); // not a command, just a shortcut for clearFramebuffer, it touches only stencil target + void clearDepthStencilFramebuffer(float depth, int stencil, bool enableScissor = false); // not a command, just a shortcut for clearFramebuffer, it touches depth and stencil target + + void blit(const FramebufferPointer& src, const Vec4i& srcViewport, const FramebufferPointer& dst, const Vec4i& dstViewport); // Query Section void beginQuery(const QueryPointer& query); @@ -162,8 +162,6 @@ public: COMMAND_drawInstanced, COMMAND_drawIndexedInstanced, - COMMAND_clearFramebuffer, - COMMAND_setInputFormat, COMMAND_setInputBuffer, COMMAND_setIndexBuffer, @@ -181,6 +179,7 @@ public: COMMAND_setResourceTexture, COMMAND_setFramebuffer, + COMMAND_clearFramebuffer, COMMAND_blit, COMMAND_beginQuery, diff --git a/libraries/gpu/src/gpu/GLBackend.cpp b/libraries/gpu/src/gpu/GLBackend.cpp index ae50b96bc5..6b1d552be9 100644 --- a/libraries/gpu/src/gpu/GLBackend.cpp +++ b/libraries/gpu/src/gpu/GLBackend.cpp @@ -21,7 +21,6 @@ GLBackend::CommandCall GLBackend::_commandCalls[Batch::NUM_COMMANDS] = (&::gpu::GLBackend::do_drawIndexed), (&::gpu::GLBackend::do_drawInstanced), (&::gpu::GLBackend::do_drawIndexedInstanced), - (&::gpu::GLBackend::do_clearFramebuffer), (&::gpu::GLBackend::do_setInputFormat), (&::gpu::GLBackend::do_setInputBuffer), @@ -40,6 +39,7 @@ GLBackend::CommandCall GLBackend::_commandCalls[Batch::NUM_COMMANDS] = (&::gpu::GLBackend::do_setResourceTexture), (&::gpu::GLBackend::do_setFramebuffer), + (&::gpu::GLBackend::do_clearFramebuffer), (&::gpu::GLBackend::do_blit), (&::gpu::GLBackend::do_beginQuery), @@ -246,71 +246,6 @@ void GLBackend::do_drawIndexedInstanced(Batch& batch, uint32 paramOffset) { (void) CHECK_GL_ERROR(); } -void GLBackend::do_clearFramebuffer(Batch& batch, uint32 paramOffset) { - - uint32 masks = batch._params[paramOffset + 7]._uint; - Vec4 color; - color.x = batch._params[paramOffset + 6]._float; - color.y = batch._params[paramOffset + 5]._float; - color.z = batch._params[paramOffset + 4]._float; - color.w = batch._params[paramOffset + 3]._float; - float depth = batch._params[paramOffset + 2]._float; - int stencil = batch._params[paramOffset + 1]._int; - int useScissor = batch._params[paramOffset + 0]._int; - - GLuint glmask = 0; - if (masks & Framebuffer::BUFFER_STENCIL) { - glClearStencil(stencil); - glmask |= GL_STENCIL_BUFFER_BIT; - } - - if (masks & Framebuffer::BUFFER_DEPTH) { - glClearDepth(depth); - glmask |= GL_DEPTH_BUFFER_BIT; - } - - std::vector drawBuffers; - if (masks & Framebuffer::BUFFER_COLORS) { - for (unsigned int i = 0; i < Framebuffer::MAX_NUM_RENDER_BUFFERS; i++) { - if (masks & (1 << i)) { - drawBuffers.push_back(GL_COLOR_ATTACHMENT0 + i); - } - } - - if (!drawBuffers.empty()) { - glDrawBuffers(drawBuffers.size(), drawBuffers.data()); - glClearColor(color.x, color.y, color.z, color.w); - glmask |= GL_COLOR_BUFFER_BIT; - } - - // Force the color mask cache to WRITE_ALL if not the case - do_setStateColorWriteMask(State::ColorMask::WRITE_ALL); - } - - // Apply scissor if needed and if not already on - bool doEnableScissor = (useScissor && (!_pipeline._stateCache.scissorEnable)); - if (doEnableScissor) { - glEnable(GL_SCISSOR_TEST); - } - - glClear(glmask); - - // Restore scissor if needed - if (doEnableScissor) { - glDisable(GL_SCISSOR_TEST); - } - - // Restore the color draw buffers only if a frmaebuffer is bound - if (_output._framebuffer && !drawBuffers.empty()) { - auto glFramebuffer = syncGPUObject(*_output._framebuffer); - if (glFramebuffer) { - glDrawBuffers(glFramebuffer->_colorBuffers.size(), glFramebuffer->_colorBuffers.data()); - } - } - - (void) CHECK_GL_ERROR(); -} - // TODO: As long as we have gl calls explicitely issued from interface // code, we need to be able to record and batch these calls. THe long // term strategy is to get rid of any GL calls in favor of the HIFI GPU API diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index 894e2c4548..9d8c9ef805 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -241,8 +241,6 @@ protected: void do_drawInstanced(Batch& batch, uint32 paramOffset); void do_drawIndexedInstanced(Batch& batch, uint32 paramOffset); - void do_clearFramebuffer(Batch& batch, uint32 paramOffset); - // Input Stage void do_setInputFormat(Batch& batch, uint32 paramOffset); void do_setInputBuffer(Batch& batch, uint32 paramOffset); @@ -385,6 +383,7 @@ protected: // Output stage void do_setFramebuffer(Batch& batch, uint32 paramOffset); + void do_clearFramebuffer(Batch& batch, uint32 paramOffset); void do_blit(Batch& batch, uint32 paramOffset); // Synchronize the state cache of this Backend with the actual real state of the GL Context diff --git a/libraries/gpu/src/gpu/GLBackendOutput.cpp b/libraries/gpu/src/gpu/GLBackendOutput.cpp index 1b22649ad6..b37def48e7 100755 --- a/libraries/gpu/src/gpu/GLBackendOutput.cpp +++ b/libraries/gpu/src/gpu/GLBackendOutput.cpp @@ -180,7 +180,6 @@ void GLBackend::syncOutputStateCache() { void GLBackend::do_setFramebuffer(Batch& batch, uint32 paramOffset) { auto framebuffer = batch._framebuffers.get(batch._params[paramOffset]._uint); - if (_output._framebuffer != framebuffer) { auto newFBO = getFramebufferID(framebuffer); if (_output._drawFBO != newFBO) { @@ -191,6 +190,72 @@ void GLBackend::do_setFramebuffer(Batch& batch, uint32 paramOffset) { } } +void GLBackend::do_clearFramebuffer(Batch& batch, uint32 paramOffset) { + + uint32 masks = batch._params[paramOffset + 7]._uint; + Vec4 color; + color.x = batch._params[paramOffset + 6]._float; + color.y = batch._params[paramOffset + 5]._float; + color.z = batch._params[paramOffset + 4]._float; + color.w = batch._params[paramOffset + 3]._float; + float depth = batch._params[paramOffset + 2]._float; + int stencil = batch._params[paramOffset + 1]._int; + int useScissor = batch._params[paramOffset + 0]._int; + + GLuint glmask = 0; + if (masks & Framebuffer::BUFFER_STENCIL) { + glClearStencil(stencil); + glmask |= GL_STENCIL_BUFFER_BIT; + } + + if (masks & Framebuffer::BUFFER_DEPTH) { + glClearDepth(depth); + glmask |= GL_DEPTH_BUFFER_BIT; + } + + std::vector drawBuffers; + if (masks & Framebuffer::BUFFER_COLORS) { + for (unsigned int i = 0; i < Framebuffer::MAX_NUM_RENDER_BUFFERS; i++) { + if (masks & (1 << i)) { + drawBuffers.push_back(GL_COLOR_ATTACHMENT0 + i); + } + } + + if (!drawBuffers.empty()) { + glDrawBuffers(drawBuffers.size(), drawBuffers.data()); + glClearColor(color.x, color.y, color.z, color.w); + glmask |= GL_COLOR_BUFFER_BIT; + } + + // Force the color mask cache to WRITE_ALL if not the case + do_setStateColorWriteMask(State::ColorMask::WRITE_ALL); + } + + // Apply scissor if needed and if not already on + bool doEnableScissor = (useScissor && (!_pipeline._stateCache.scissorEnable)); + if (doEnableScissor) { + glEnable(GL_SCISSOR_TEST); + } + + // Clear! + glClear(glmask); + + // Restore scissor if needed + if (doEnableScissor) { + glDisable(GL_SCISSOR_TEST); + } + + // Restore the color draw buffers only if a frmaebuffer is bound + if (_output._framebuffer && !drawBuffers.empty()) { + auto glFramebuffer = syncGPUObject(*_output._framebuffer); + if (glFramebuffer) { + glDrawBuffers(glFramebuffer->_colorBuffers.size(), glFramebuffer->_colorBuffers.data()); + } + } + + (void) CHECK_GL_ERROR(); +} + void GLBackend::do_blit(Batch& batch, uint32 paramOffset) { auto srcframebuffer = batch._framebuffers.get(batch._params[paramOffset]._uint); Vec4i srcvp; @@ -203,19 +268,31 @@ void GLBackend::do_blit(Batch& batch, uint32 paramOffset) { for (size_t i = 0; i < 4; ++i) { dstvp[i] = batch._params[paramOffset + 6 + i]._int; } - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, getFramebufferID(dstframebuffer)); + + // Assign dest framebuffer if not bound already + auto newDrawFBO = getFramebufferID(dstframebuffer); + if (_output._drawFBO != newDrawFBO) { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, newDrawFBO); + } + + // always bind the read fbo glBindFramebuffer(GL_READ_FRAMEBUFFER, getFramebufferID(srcframebuffer)); + + // Blit! glBlitFramebuffer(srcvp.x, srcvp.y, srcvp.z, srcvp.w, dstvp.x, dstvp.y, dstvp.z, dstvp.w, GL_COLOR_BUFFER_BIT, GL_LINEAR); - - (void) CHECK_GL_ERROR(); - if (_output._framebuffer) { - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, getFramebufferID(_output._framebuffer)); + // Always clean the read fbo to 0 + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + + // Restore draw fbo if changed + if (_output._drawFBO != newDrawFBO) { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _output._drawFBO); } -} + (void) CHECK_GL_ERROR(); +} void GLBackend::downloadFramebuffer(const FramebufferPointer& srcFramebuffer, const Vec4i& region, QImage& destImage) { auto readFBO = gpu::GLBackend::getFramebufferID(srcFramebuffer); From 5dd16d9f801cb79cbaa786d1582a3eeca16a443a Mon Sep 17 00:00:00 2001 From: Niraj Venkat Date: Fri, 24 Jul 2015 16:07:30 -0700 Subject: [PATCH 075/242] Blend function applied to reduce one FBO --- .../src/AmbientOcclusionEffect.cpp | 189 +----------------- .../render-utils/src/occlusion_blend.slf | 13 +- .../render-utils/src/occlusion_blend.slv | 24 --- .../render-utils/src/occlusion_result.slf | 23 --- 4 files changed, 8 insertions(+), 241 deletions(-) delete mode 100644 libraries/render-utils/src/occlusion_blend.slv delete mode 100644 libraries/render-utils/src/occlusion_result.slf diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index d8b56f5ba1..da081f7811 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -32,150 +32,8 @@ #include "gaussian_blur_vertical_vert.h" #include "gaussian_blur_horizontal_vert.h" #include "gaussian_blur_frag.h" -#include "occlusion_blend_vert.h" #include "occlusion_blend_frag.h" -//#include "occlusion_result_vert.h" -#include "occlusion_result_frag.h" -/* -void AmbientOcclusionEffect::init(AbstractViewStateInterface* viewState) { - _viewState = viewState; // we will use this for view state services - - _occlusionProgram = new ProgramObject(); - _occlusionProgram->addShaderFromSourceFile(QGLShader::Vertex, PathUtils::resourcesPath() - + "shaders/ambient_occlusion.vert"); - _occlusionProgram->addShaderFromSourceFile(QGLShader::Fragment, PathUtils::resourcesPath() - + "shaders/ambient_occlusion.frag"); - _occlusionProgram->link(); - - // create the sample kernel: an array of hemispherically distributed offset vectors - const int SAMPLE_KERNEL_SIZE = 16; - QVector3D sampleKernel[SAMPLE_KERNEL_SIZE]; - for (int i = 0; i < SAMPLE_KERNEL_SIZE; i++) { - // square the length in order to increase density towards the center - glm::vec3 vector = glm::sphericalRand(1.0f); - float scale = randFloat(); - const float MIN_VECTOR_LENGTH = 0.01f; - const float MAX_VECTOR_LENGTH = 1.0f; - vector *= glm::mix(MIN_VECTOR_LENGTH, MAX_VECTOR_LENGTH, scale * scale); - sampleKernel[i] = QVector3D(vector.x, vector.y, vector.z); - } - - _occlusionProgram->bind(); - _occlusionProgram->setUniformValue("depthTexture", 0); - _occlusionProgram->setUniformValue("rotationTexture", 1); - _occlusionProgram->setUniformValueArray("sampleKernel", sampleKernel, SAMPLE_KERNEL_SIZE); - _occlusionProgram->setUniformValue("radius", 0.1f); - _occlusionProgram->release(); - - _nearLocation = _occlusionProgram->uniformLocation("near"); - _farLocation = _occlusionProgram->uniformLocation("far"); - _leftBottomLocation = _occlusionProgram->uniformLocation("leftBottom"); - _rightTopLocation = _occlusionProgram->uniformLocation("rightTop"); - _noiseScaleLocation = _occlusionProgram->uniformLocation("noiseScale"); - _texCoordOffsetLocation = _occlusionProgram->uniformLocation("texCoordOffset"); - _texCoordScaleLocation = _occlusionProgram->uniformLocation("texCoordScale"); - - // generate the random rotation texture - glGenTextures(1, &_rotationTextureID); - glBindTexture(GL_TEXTURE_2D, _rotationTextureID); - const int ELEMENTS_PER_PIXEL = 3; - unsigned char rotationData[ROTATION_WIDTH * ROTATION_HEIGHT * ELEMENTS_PER_PIXEL]; - unsigned char* rotation = rotationData; - for (int i = 0; i < ROTATION_WIDTH * ROTATION_HEIGHT; i++) { - glm::vec3 vector = glm::sphericalRand(1.0f); - *rotation++ = ((vector.x + 1.0f) / 2.0f) * 255.0f; - *rotation++ = ((vector.y + 1.0f) / 2.0f) * 255.0f; - *rotation++ = ((vector.z + 1.0f) / 2.0f) * 255.0f; - } - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, ROTATION_WIDTH, ROTATION_HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, rotationData); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glBindTexture(GL_TEXTURE_2D, 0); - - _blurProgram = new ProgramObject(); - _blurProgram->addShaderFromSourceFile(QGLShader::Vertex, PathUtils::resourcesPath() + "shaders/ambient_occlusion.vert"); - _blurProgram->addShaderFromSourceFile(QGLShader::Fragment, PathUtils::resourcesPath() + "shaders/occlusion_blur.frag"); - _blurProgram->link(); - - _blurProgram->bind(); - _blurProgram->setUniformValue("originalTexture", 0); - _blurProgram->release(); - - _blurScaleLocation = _blurProgram->uniformLocation("blurScale"); -} - -void AmbientOcclusionEffect::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext){ - glDisable(GL_BLEND); - glDisable(GL_DEPTH_TEST); - glDepthMask(GL_FALSE); - - glBindTexture(GL_TEXTURE_2D, DependencyManager::get()->getPrimaryDepthTextureID()); - - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, _rotationTextureID); - - // render with the occlusion shader to the secondary/tertiary buffer - auto freeFramebuffer = nullptr; // DependencyManager::get()->getFreeFramebuffer(); // FIXME - glBindFramebuffer(GL_FRAMEBUFFER, gpu::GLBackend::getFramebufferID(freeFramebuffer)); - - float left, right, bottom, top, nearVal, farVal; - glm::vec4 nearClipPlane, farClipPlane; - AbstractViewStateInterface::instance()->computeOffAxisFrustum(left, right, bottom, top, nearVal, farVal, nearClipPlane, farClipPlane); - - int viewport[4]; - glGetIntegerv(GL_VIEWPORT, viewport); - const int VIEWPORT_X_INDEX = 0; - const int VIEWPORT_WIDTH_INDEX = 2; - - auto framebufferSize = DependencyManager::get()->getFrameBufferSize(); - float sMin = viewport[VIEWPORT_X_INDEX] / (float)framebufferSize.width(); - float sWidth = viewport[VIEWPORT_WIDTH_INDEX] / (float)framebufferSize.width(); - - _occlusionProgram->bind(); - _occlusionProgram->setUniformValue(_nearLocation, nearVal); - _occlusionProgram->setUniformValue(_farLocation, farVal); - _occlusionProgram->setUniformValue(_leftBottomLocation, left, bottom); - _occlusionProgram->setUniformValue(_rightTopLocation, right, top); - _occlusionProgram->setUniformValue(_noiseScaleLocation, viewport[VIEWPORT_WIDTH_INDEX] / (float)ROTATION_WIDTH, - framebufferSize.height() / (float)ROTATION_HEIGHT); - _occlusionProgram->setUniformValue(_texCoordOffsetLocation, sMin, 0.0f); - _occlusionProgram->setUniformValue(_texCoordScaleLocation, sWidth, 1.0f); - - renderFullscreenQuad(); - - _occlusionProgram->release(); - - glBindFramebuffer(GL_FRAMEBUFFER, 0); - - glBindTexture(GL_TEXTURE_2D, 0); - - glActiveTexture(GL_TEXTURE0); - - // now render secondary to primary with 4x4 blur - auto primaryFramebuffer = DependencyManager::get()->getPrimaryFramebuffer(); - glBindFramebuffer(GL_FRAMEBUFFER, gpu::GLBackend::getFramebufferID(primaryFramebuffer)); - - glEnable(GL_BLEND); - glBlendFuncSeparate(GL_ZERO, GL_SRC_COLOR, GL_ZERO, GL_ONE); - - auto freeFramebufferTexture = freeFramebuffer->getRenderBuffer(0); - glBindTexture(GL_TEXTURE_2D, gpu::GLBackend::getTextureID(freeFramebufferTexture)); - - _blurProgram->bind(); - _blurProgram->setUniformValue(_blurScaleLocation, 1.0f / framebufferSize.width(), 1.0f / framebufferSize.height()); - - renderFullscreenQuad(sMin, sMin + sWidth); - - _blurProgram->release(); - - glBindTexture(GL_TEXTURE_2D, 0); - - glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); - glEnable(GL_DEPTH_TEST); - glDepthMask(GL_TRUE) -} -*/ AmbientOcclusion::AmbientOcclusion() { } @@ -297,7 +155,6 @@ const gpu::PipelinePointer& AmbientOcclusion::getBlendPipeline() { gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("blurredOcclusionTexture"), 0)); - slotBindings.insert(gpu::Shader::Binding(std::string("colorTexture"), 1)); gpu::Shader::makeProgram(*program, slotBindings); @@ -306,8 +163,8 @@ const gpu::PipelinePointer& AmbientOcclusion::getBlendPipeline() { state->setDepthTest(false, false, gpu::LESS_EQUAL); // Blend on transparent - state->setBlendFunction(false, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); + state->setBlendFunction(true, + gpu::State::SRC_COLOR, gpu::State::BLEND_OP_ADD, gpu::State::DEST_COLOR); // Link the blend FBO to texture _blendBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, @@ -324,30 +181,6 @@ const gpu::PipelinePointer& AmbientOcclusion::getBlendPipeline() { return _blendPipeline; } -const gpu::PipelinePointer& AmbientOcclusion::getAOResultPipeline() { - if (!_AOResultPipeline) { - auto vs = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(ambient_occlusion_vert))); - auto ps = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(occlusion_result_frag))); - gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vs, ps)); - //gpu::ShaderPointer program = gpu::StandardShaderLib::getProgram(gpu::StandardShaderLib::getDrawTransformUnitQuadVS(), gpu::StandardShaderLib::getDrawTexturePS()); - - gpu::Shader::BindingSet slotBindings; - gpu::Shader::makeProgram(*program, slotBindings); - - gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - - state->setDepthTest(false, false, gpu::LESS_EQUAL); - - // Blend on transparent - state->setBlendFunction(false, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); - - // Good to go add the brand new pipeline - _AOResultPipeline.reset(gpu::Pipeline::create(program, state)); - } - return _AOResultPipeline; -} - void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext) { assert(renderContext->args); assert(renderContext->args->_viewFrustum); @@ -418,30 +251,14 @@ void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, cons // Blend step getBlendPipeline(); batch.setResourceTexture(0, _hBlurTexture); - batch.setResourceTexture(1, DependencyManager::get()->getPrimaryColorTexture()); - batch.setFramebuffer(_blendBuffer); + batch.setFramebuffer(DependencyManager::get()->getPrimaryFramebuffer()); // Bind the fourth gpu::Pipeline we need - for blending the primary color buffer with blurred occlusion texture batch.setPipeline(getBlendPipeline()); DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); - - // Final AO result step - getAOResultPipeline(); - batch.setResourceTexture(0, _blendTexture); - batch.setFramebuffer(DependencyManager::get()->getPrimaryFramebuffer()); - - // Bind the fifth gpu::Pipeline we need - for displaying the blended texture - batch.setPipeline(getAOResultPipeline()); - - glm::vec2 bottomLeftSmall(0.5f, -1.0f); - glm::vec2 topRightSmall(1.0f, -0.5f); - DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); // Ready to render args->_context->syncCache(); args->_context->render((batch)); - - // need to fetch forom the z buffer and render something in a new render target a result that combine the z and produce a fake AO result - } diff --git a/libraries/render-utils/src/occlusion_blend.slf b/libraries/render-utils/src/occlusion_blend.slf index 00b09b0cbd..6c0101eb6f 100644 --- a/libraries/render-utils/src/occlusion_blend.slf +++ b/libraries/render-utils/src/occlusion_blend.slf @@ -17,17 +17,14 @@ varying vec2 varTexcoord; uniform sampler2D blurredOcclusionTexture; -uniform sampler2D colorTexture; void main(void) { vec4 occlusionColor = texture2D(blurredOcclusionTexture, varTexcoord); - vec4 currentColor = texture2D(colorTexture, varTexcoord); - /* - if(occlusionColor.r == 1.0) { - gl_FragColor = currentColor; + + if(occlusionColor.r > 0.8 && occlusionColor.r <= 1.0) { + gl_FragColor = vec4(vec3(0.0), 1.0); } else { - gl_FragColor = mix(occlusionColor, currentColor, 0.5); - }*/ - gl_FragColor = currentColor; + gl_FragColor = vec4(vec3(0.2), 1.0); + } } diff --git a/libraries/render-utils/src/occlusion_blend.slv b/libraries/render-utils/src/occlusion_blend.slv deleted file mode 100644 index 53380a494f..0000000000 --- a/libraries/render-utils/src/occlusion_blend.slv +++ /dev/null @@ -1,24 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// occlusion_blend.vert -// vertex shader -// -// Created by Niraj Venkat on 7/20/15. -// 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 gpu/Transform.slh@> - -<$declareStandardTransform()$> - -varying vec2 varTexcoord; - -void main(void) { - varTexcoord = gl_MultiTexCoord0.xy; - gl_Position = gl_Vertex; -} diff --git a/libraries/render-utils/src/occlusion_result.slf b/libraries/render-utils/src/occlusion_result.slf deleted file mode 100644 index 3b7e895dc6..0000000000 --- a/libraries/render-utils/src/occlusion_result.slf +++ /dev/null @@ -1,23 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// occlusion_result.frag -// fragment shader -// -// Created by Niraj Venkat on 7/23/15. -// 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 DeferredBufferWrite.slh@> - -varying vec2 varTexcoord; - -uniform sampler2D resultTexture; - -void main(void) { - gl_FragColor = texture2D(resultTexture, varTexcoord); -} From 1d99057052c82b88764a83f575b7c94409aeb34f Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 24 Jul 2015 16:08:47 -0700 Subject: [PATCH 076/242] Debug printouts in tests. --- tests/rig/src/RigTests.cpp | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/rig/src/RigTests.cpp b/tests/rig/src/RigTests.cpp index c5622ac501..74047c0162 100644 --- a/tests/rig/src/RigTests.cpp +++ b/tests/rig/src/RigTests.cpp @@ -104,10 +104,24 @@ void reportJoint(int index, JointState joint) { // Handy for debugging std::cout << " rot:" << safeEulerAngles(joint.getRotation()) << "/" << safeEulerAngles(joint.getRotationInParentFrame()) << "/" << safeEulerAngles(joint.getRotationInBindFrame()) << "\n"; std::cout << "\n"; } - -void RigTests::initialPoseArmsDown() { - for (int i = 0; i < _rig->getJointStateCount(); i++) { - JointState joint = _rig->getJointState(i); +void reportByName(RigPointer rig, const QString& name) { + int jointIndex = rig->indexOfJoint(name); + reportJoint(jointIndex, rig->getJointState(jointIndex)); +} +void reportAll(RigPointer rig) { + for (int i = 0; i < rig->getJointStateCount(); i++) { + JointState joint = rig->getJointState(i); reportJoint(i, joint); } } +void reportSome(RigPointer rig) { + QString names[] = {"Head", "Neck", "RightShoulder", "RightArm", "RightForeArm", "RightHand", "Spine2", "Spine1", "Spine", "Hips", "RightUpLeg", "RightLeg", "RightFoot", "RightToeBase", "RightToe_End"}; + for (auto name : names) { + reportByName(rig, name); + } +} + +void RigTests::initialPoseArmsDown() { + //reportAll(_rig); + reportSome(_rig); +} From 6bc3d65bf4cfe58e3ba12a75979baa13ce3228ce Mon Sep 17 00:00:00 2001 From: Niraj Venkat Date: Fri, 24 Jul 2015 16:16:18 -0700 Subject: [PATCH 077/242] Fixing build errors --- libraries/render-utils/src/AmbientOcclusionEffect.cpp | 11 +---------- libraries/render-utils/src/AmbientOcclusionEffect.h | 4 ---- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index da081f7811..d7fa88b276 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -149,7 +149,7 @@ const gpu::PipelinePointer& AmbientOcclusion::getHBlurPipeline() { const gpu::PipelinePointer& AmbientOcclusion::getBlendPipeline() { if (!_blendPipeline) { - auto vs = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(occlusion_blend_vert))); + auto vs = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(ambient_occlusion_vert))); auto ps = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(occlusion_blend_frag))); gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vs, ps)); @@ -166,15 +166,6 @@ const gpu::PipelinePointer& AmbientOcclusion::getBlendPipeline() { state->setBlendFunction(true, gpu::State::SRC_COLOR, gpu::State::BLEND_OP_ADD, gpu::State::DEST_COLOR); - // Link the blend FBO to texture - _blendBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, - DependencyManager::get()->getFrameBufferSize().width(), DependencyManager::get()->getFrameBufferSize().height())); - auto format = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); - auto width = _blendBuffer->getWidth(); - auto height = _blendBuffer->getHeight(); - auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _blendTexture = gpu::TexturePointer(gpu::Texture::create2D(format, width, height, defaultSampler)); - // Good to go add the brand new pipeline _blendPipeline.reset(gpu::Pipeline::create(program, state)); } diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.h b/libraries/render-utils/src/AmbientOcclusionEffect.h index ec0314016c..76cb0b063d 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.h +++ b/libraries/render-utils/src/AmbientOcclusionEffect.h @@ -28,7 +28,6 @@ public: const gpu::PipelinePointer& getHBlurPipeline(); const gpu::PipelinePointer& getVBlurPipeline(); const gpu::PipelinePointer& getBlendPipeline(); - const gpu::PipelinePointer& getAOResultPipeline(); private: @@ -48,17 +47,14 @@ private: gpu::PipelinePointer _hBlurPipeline; gpu::PipelinePointer _vBlurPipeline; gpu::PipelinePointer _blendPipeline; - gpu::PipelinePointer _AOResultPipeline; gpu::FramebufferPointer _occlusionBuffer; gpu::FramebufferPointer _hBlurBuffer; gpu::FramebufferPointer _vBlurBuffer; - gpu::FramebufferPointer _blendBuffer; gpu::TexturePointer _occlusionTexture; gpu::TexturePointer _hBlurTexture; gpu::TexturePointer _vBlurTexture; - gpu::TexturePointer _blendTexture; }; From 05dda1220e54d856fc52ecb445331480aff9b888 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 24 Jul 2015 22:01:55 -0700 Subject: [PATCH 078/242] Add logging. --- libraries/animation/src/AnimationLogging.cpp | 14 ++++++++++++++ libraries/animation/src/AnimationLogging.h | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 libraries/animation/src/AnimationLogging.cpp create mode 100644 libraries/animation/src/AnimationLogging.h diff --git a/libraries/animation/src/AnimationLogging.cpp b/libraries/animation/src/AnimationLogging.cpp new file mode 100644 index 0000000000..11ed5ebd35 --- /dev/null +++ b/libraries/animation/src/AnimationLogging.cpp @@ -0,0 +1,14 @@ +// +// AnimationLogging.cpp +// libraries/audio/src +// +// Created by Howard Stearns on 7/24/15. +// 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 "AnimationLogging.h" + +Q_LOGGING_CATEGORY(animation, "hifi.animation") diff --git a/libraries/animation/src/AnimationLogging.h b/libraries/animation/src/AnimationLogging.h new file mode 100644 index 0000000000..6c56e2dbe4 --- /dev/null +++ b/libraries/animation/src/AnimationLogging.h @@ -0,0 +1,18 @@ +// +// AnimationLogging.h +// libraries/animation/src/ +// +// Created by Howard Stearns on 7/24/15. +// +// 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_AnimationLogging_h +#define hifi_AnimationLogging_h + +#include + +Q_DECLARE_LOGGING_CATEGORY(animation) + +#endif From 8b5f24e4df09515c5a58f9515d6c00fc3eb5cdd3 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 24 Jul 2015 22:02:39 -0700 Subject: [PATCH 079/242] Keep list of animations in Rig, not MyAvatar. --- interface/src/avatar/MyAvatar.cpp | 92 +++++++------------------------ interface/src/avatar/MyAvatar.h | 8 +-- libraries/animation/src/Rig.cpp | 87 +++++++++++++++++++++++++++-- libraries/animation/src/Rig.h | 13 ++++- 4 files changed, 118 insertions(+), 82 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 00fce3a1e7..655af63c55 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -491,17 +491,6 @@ void MyAvatar::loadLastRecording() { _player->loadRecording(_recorder->getRecording()); } -AnimationHandlePointer MyAvatar::addAnimationHandle() { - AnimationHandlePointer handle = _rig->createAnimationHandle(); - _animationHandles.append(handle); - return handle; -} - -void MyAvatar::removeAnimationHandle(const AnimationHandlePointer& handle) { - handle->stop(); - _animationHandles.removeOne(handle); -} - void MyAvatar::startAnimation(const QString& url, float fps, float priority, bool loop, bool hold, float firstFrame, float lastFrame, const QStringList& maskedJoints) { if (QThread::currentThread() != thread()) { @@ -510,16 +499,7 @@ void MyAvatar::startAnimation(const QString& url, float fps, float priority, Q_ARG(float, lastFrame), Q_ARG(const QStringList&, maskedJoints)); return; } - AnimationHandlePointer handle = _rig->createAnimationHandle(); - handle->setURL(url); - handle->setFPS(fps); - handle->setPriority(priority); - handle->setLoop(loop); - handle->setHold(hold); - handle->setFirstFrame(firstFrame); - handle->setLastFrame(lastFrame); - handle->setMaskedJoints(maskedJoints); - handle->start(); + _rig->startAnimation(url, fps, priority, loop, hold, firstFrame, lastFrame, maskedJoints); } void MyAvatar::startAnimationByRole(const QString& role, const QString& url, float fps, float priority, @@ -530,25 +510,7 @@ void MyAvatar::startAnimationByRole(const QString& role, const QString& url, flo Q_ARG(float, lastFrame), Q_ARG(const QStringList&, maskedJoints)); return; } - // check for a configured animation for the role - foreach (const AnimationHandlePointer& handle, _animationHandles) { - if (handle->getRole() == role) { - handle->start(); - return; - } - } - // no joy; use the parameters provided - AnimationHandlePointer handle = _rig->createAnimationHandle(); - handle->setRole(role); - handle->setURL(url); - handle->setFPS(fps); - handle->setPriority(priority); - handle->setLoop(loop); - handle->setHold(hold); - handle->setFirstFrame(firstFrame); - handle->setLastFrame(lastFrame); - handle->setMaskedJoints(maskedJoints); - handle->start(); + _rig->startAnimationByRole(role, url, fps, priority, loop, hold, firstFrame, lastFrame, maskedJoints); } void MyAvatar::stopAnimationByRole(const QString& role) { @@ -556,11 +518,7 @@ void MyAvatar::stopAnimationByRole(const QString& role) { QMetaObject::invokeMethod(this, "stopAnimationByRole", Q_ARG(const QString&, role)); return; } - foreach (const AnimationHandlePointer& handle, _skeletonModel.getRunningAnimations()) { - if (handle->getRole() == role) { - handle->stop(); - } - } + _rig->stopAnimationByRole(role); } void MyAvatar::stopAnimation(const QString& url) { @@ -568,11 +526,7 @@ void MyAvatar::stopAnimation(const QString& url) { QMetaObject::invokeMethod(this, "stopAnimation", Q_ARG(const QString&, url)); return; } - foreach (const AnimationHandlePointer& handle, _skeletonModel.getRunningAnimations()) { - if (handle->getURL() == url) { - handle->stop(); - } - } + _rig->stopAnimation(url); } AnimationDetails MyAvatar::getAnimationDetailsByRole(const QString& role) { @@ -583,7 +537,7 @@ AnimationDetails MyAvatar::getAnimationDetailsByRole(const QString& role) { Q_ARG(const QString&, role)); return result; } - foreach (const AnimationHandlePointer& handle, _skeletonModel.getRunningAnimations()) { + foreach (const AnimationHandlePointer& handle, _rig->getRunningAnimations()) { if (handle->getRole() == role) { result = handle->getAnimationDetails(); break; @@ -600,7 +554,7 @@ AnimationDetails MyAvatar::getAnimationDetails(const QString& url) { Q_ARG(const QString&, url)); return result; } - foreach (const AnimationHandlePointer& handle, _skeletonModel.getRunningAnimations()) { + foreach (const AnimationHandlePointer& handle, _rig->getRunningAnimations()) { if (handle->getURL() == url) { result = handle->getAnimationDetails(); break; @@ -646,9 +600,11 @@ void MyAvatar::saveData() { settings.endArray(); settings.beginWriteArray("animationHandles"); - for (int i = 0; i < _animationHandles.size(); i++) { + auto animationHandles = _rig->getAnimationHandles(); + for (int i = 0; i < animationHandles.size(); i++) { settings.setArrayIndex(i); - const AnimationHandlePointer& pointer = _animationHandles.at(i); + const AnimationHandlePointer& pointer = animationHandles.at(i); + qCDebug(interfaceapp) << "Save animation" << pointer->getURL().toString(); settings.setValue("role", pointer->getRole()); settings.setValue("url", pointer->getURL()); settings.setValue("fps", pointer->getFPS()); @@ -766,25 +722,19 @@ void MyAvatar::loadData() { setAttachmentData(attachmentData); int animationCount = settings.beginReadArray("animationHandles"); - while (_animationHandles.size() > animationCount) { - _animationHandles.takeLast()->stop(); - } - while (_animationHandles.size() < animationCount) { - addAnimationHandle(); - } + _rig->deleteAnimations(); for (int i = 0; i < animationCount; i++) { settings.setArrayIndex(i); - const AnimationHandlePointer& handle = _animationHandles.at(i); - handle->setRole(settings.value("role", "idle").toString()); - handle->setURL(settings.value("url").toUrl()); - handle->setFPS(loadSetting(settings, "fps", 30.0f)); - handle->setPriority(loadSetting(settings, "priority", 1.0f)); - handle->setLoop(settings.value("loop", true).toBool()); - handle->setHold(settings.value("hold", false).toBool()); - handle->setFirstFrame(settings.value("firstFrame", 0.0f).toFloat()); - handle->setLastFrame(settings.value("lastFrame", INT_MAX).toFloat()); - handle->setMaskedJoints(settings.value("maskedJoints").toStringList()); - handle->setStartAutomatically(settings.value("startAutomatically", true).toBool()); + _rig->addAnimationByRole(settings.value("role", "idle").toString(), + settings.value("url").toString(), + loadSetting(settings, "fps", 30.0f), + loadSetting(settings, "priority", 1.0f), + settings.value("loop", true).toBool(), + settings.value("hold", false).toBool(), + settings.value("firstFrame", 0.0f).toFloat(), + settings.value("lastFrame", INT_MAX).toFloat(), + settings.value("maskedJoints").toStringList(), + settings.value("startAutomatically", true).toBool()); } settings.endArray(); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index c6cb48878f..802c92ec2c 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -61,9 +61,9 @@ public: bool getShouldRenderLocally() const { return _shouldRender; } float getRealWorldFieldOfView() { return _realWorldFieldOfView.get(); } - const QList& getAnimationHandles() const { return _animationHandles; } - AnimationHandlePointer addAnimationHandle(); - void removeAnimationHandle(const AnimationHandlePointer& handle); + const QList& getAnimationHandles() const { return _rig->getAnimationHandles(); } + AnimationHandlePointer addAnimationHandle() { return _rig->createAnimationHandle(); } + void removeAnimationHandle(const AnimationHandlePointer& handle) { _rig->removeAnimationHandle(handle); } /// Allows scripts to run animations. Q_INVOKABLE void startAnimation(const QString& url, float fps = 30.0f, float priority = 1.0f, bool loop = false, @@ -256,8 +256,6 @@ private: bool _shouldRender; bool _billboardValid; float _oculusYawOffset; - - QList _animationHandles; bool _feetTouchFloor; eyeContactTarget _eyeContactTarget; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 8f4047bc71..7d520e7930 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -12,7 +12,7 @@ #include #include "AnimationHandle.h" - +#include "AnimationLogging.h" #include "Rig.h" void insertSorted(QList& handles, const AnimationHandlePointer& handle) { @@ -27,9 +27,86 @@ void insertSorted(QList& handles, const AnimationHandleP AnimationHandlePointer Rig::createAnimationHandle() { AnimationHandlePointer handle(new AnimationHandle(getRigPointer())); - _animationHandles.insert(handle); + _animationHandles.append(handle); return handle; } +void Rig::removeAnimationHandle(const AnimationHandlePointer& handle) { + handle->stop(); + // FIXME? Do we need to also animationHandle->clearJoints()? deleteAnimations(), below, was first written to do so, but did not first stop it. + _animationHandles.removeOne(handle); +} + +void Rig::startAnimation(const QString& url, float fps, float priority, + bool loop, bool hold, float firstFrame, float lastFrame, const QStringList& maskedJoints) { + qCDebug(animation) << "startAnimation" << url << fps << priority << loop << hold << firstFrame << lastFrame << maskedJoints; + AnimationHandlePointer handle = nullptr; + foreach (const AnimationHandlePointer& candidate, _animationHandles) { + if (candidate->getURL() == url) { + handle = candidate; + break; + } + } + if (!handle) { + handle = createAnimationHandle(); + handle->setURL(url); + } + handle->setFPS(fps); + handle->setPriority(priority); + handle->setLoop(loop); + handle->setHold(hold); + handle->setFirstFrame(firstFrame); + handle->setLastFrame(lastFrame); + handle->setMaskedJoints(maskedJoints); + handle->start(); +} + +void Rig::addAnimationByRole(const QString& role, const QString& url, float fps, float priority, + bool loop, bool hold, float firstFrame, float lastFrame, const QStringList& maskedJoints, bool startAutomatically) { + // check for a configured animation for the role + qCDebug(animation) << "addAnimationByRole" << role << url << fps << priority << loop << hold << firstFrame << lastFrame << maskedJoints << startAutomatically; + AnimationHandlePointer handle = nullptr; + foreach (const AnimationHandlePointer& candidate, _animationHandles) { + if (candidate->getRole() == role) { + handle = candidate; + break; + } + } + if (!handle) { + handle = createAnimationHandle(); + handle->setRole(role); + } + handle->setURL(url); + handle->setFPS(fps); + handle->setPriority(priority); + handle->setLoop(loop); + handle->setHold(hold); + handle->setFirstFrame(firstFrame); + handle->setLastFrame(lastFrame); + handle->setMaskedJoints(maskedJoints); + if (startAutomatically) { + handle->start(); + } +} +void Rig::startAnimationByRole(const QString& role, const QString& url, float fps, float priority, + bool loop, bool hold, float firstFrame, float lastFrame, const QStringList& maskedJoints) { + addAnimationByRole(role, url, fps, priority, loop, hold, firstFrame, lastFrame, maskedJoints, true); +} + +void Rig::stopAnimationByRole(const QString& role) { + foreach (const AnimationHandlePointer& handle, getRunningAnimations()) { + if (handle->getRole() == role) { + handle->stop(); + } + } +} + +void Rig::stopAnimation(const QString& url) { + foreach (const AnimationHandlePointer& handle, getRunningAnimations()) { + if (handle->getURL() == url) { + handle->stop(); + } + } +} bool Rig::removeRunningAnimation(AnimationHandlePointer animationHandle) { return _runningAnimations.removeOne(animationHandle); @@ -44,10 +121,10 @@ bool Rig::isRunningAnimation(AnimationHandlePointer animationHandle) { } void Rig::deleteAnimations() { - for (QSet::iterator it = _animationHandles.begin(); it != _animationHandles.end(); ) { - (*it)->clearJoints(); - it = _animationHandles.erase(it); + for (auto animation : _animationHandles) { + removeAnimationHandle(animation); } + _animationHandles.clear(); } float Rig::initJointStates(QVector states, glm::mat4 parentTransform, int neckJointIndex) { diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 0af4073c96..2087ad6800 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -57,11 +57,22 @@ public: RigPointer getRigPointer() { return shared_from_this(); } AnimationHandlePointer createAnimationHandle(); + void removeAnimationHandle(const AnimationHandlePointer& handle); bool removeRunningAnimation(AnimationHandlePointer animationHandle); void addRunningAnimation(AnimationHandlePointer animationHandle); bool isRunningAnimation(AnimationHandlePointer animationHandle); const QList& getRunningAnimations() const { return _runningAnimations; } void deleteAnimations(); + const QList& getAnimationHandles() const { return _animationHandles; } + void startAnimation(const QString& url, float fps = 30.0f, float priority = 1.0f, bool loop = false, + bool hold = false, float firstFrame = 0.0f, float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList()); + void stopAnimation(const QString& url); + void startAnimationByRole(const QString& role, const QString& url = QString(), float fps = 30.0f, + float priority = 1.0f, bool loop = false, bool hold = false, float firstFrame = 0.0f, + float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList()); + void stopAnimationByRole(const QString& role); + void addAnimationByRole(const QString& role, const QString& url, float fps, float priority, + bool loop, bool hold, float firstFrame, float lastFrame, const QStringList& maskedJoints, bool startAutomatically); float initJointStates(QVector states, glm::mat4 parentTransform, int neckJointIndex); bool jointStatesEmpty() { return _jointStates.isEmpty(); }; @@ -124,7 +135,7 @@ public: protected: QVector _jointStates; - QSet _animationHandles; + QList _animationHandles; QList _runningAnimations; JointState maybeCauterizeHead(int jointIndex) const; From ca8d0d542012f93dea2be32d2ea066d7bd1b1eca Mon Sep 17 00:00:00 2001 From: "Kevin M. Thomas" Date: Sat, 25 Jul 2015 07:56:57 -0400 Subject: [PATCH 080/242] Added files to zone folder and made change to jstreamplayerdomain-zone.js to allow for zone entity data field to load a stream url rather than have it hard coded in the js file. This allows for many zones to be created and simply having to put a new stream url in each data field in each zone to work. --- .../zones/jsstreamplayerdomain-zone-entity.js | 33 ++++ examples/zones/jsstreamplayerdomain-zone.html | 42 +++++ examples/zones/jsstreamplayerdomain-zone.js | 176 ++++++++++++++++++ 3 files changed, 251 insertions(+) create mode 100644 examples/zones/jsstreamplayerdomain-zone-entity.js create mode 100644 examples/zones/jsstreamplayerdomain-zone.html create mode 100644 examples/zones/jsstreamplayerdomain-zone.js diff --git a/examples/zones/jsstreamplayerdomain-zone-entity.js b/examples/zones/jsstreamplayerdomain-zone-entity.js new file mode 100644 index 0000000000..9a8cb8b8b4 --- /dev/null +++ b/examples/zones/jsstreamplayerdomain-zone-entity.js @@ -0,0 +1,33 @@ +// +// #20628: JS Stream Player Domain-Zone-Entity +// ******************************************** +// +// Created by Kevin M. Thomas and Thoys 07/20/15. +// Copyright 2015 High Fidelity, Inc. +// kevintown.net +// +// JavaScript for the High Fidelity interface that is an entity script to be placed in a chosen entity inside a domain-zone. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +// Function which exists inside of an entity which triggers as a user approches it. +(function() { + const SCRIPT_NAME = "https://dl.dropboxusercontent.com/u/17344741/jsstreamplayer/jsstreamplayerdomain-zone.js"; + function isScriptRunning(script) { + script = script.toLowerCase().trim(); + var runningScripts = ScriptDiscoveryService.getRunning(); + for (i in runningScripts) { + if (runningScripts[i].url.toLowerCase().trim() == script) { + return true; + } + } + return false; + }; + + if (!isScriptRunning(SCRIPT_NAME)) { + Script.load(SCRIPT_NAME); + } +}) \ No newline at end of file diff --git a/examples/zones/jsstreamplayerdomain-zone.html b/examples/zones/jsstreamplayerdomain-zone.html new file mode 100644 index 0000000000..28b2202591 --- /dev/null +++ b/examples/zones/jsstreamplayerdomain-zone.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/zones/jsstreamplayerdomain-zone.js b/examples/zones/jsstreamplayerdomain-zone.js new file mode 100644 index 0000000000..33d7364709 --- /dev/null +++ b/examples/zones/jsstreamplayerdomain-zone.js @@ -0,0 +1,176 @@ +// +// #20628: JS Stream Player Domain-Zone +// ************************************* +// +// Created by Kevin M. Thomas, Thoys and Konstantin 07/24/15. +// Copyright 2015 High Fidelity, Inc. +// kevintown.net +// +// JavaScript for the High Fidelity interface that creates a stream player with a UI for playing a domain-zone specificed stream URL in addition to play, stop and volume functionality which is resident only in the domain-zone. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +// Declare variables and set up new WebWindow. +var lastZone = ""; +var volume = 0.5; +var stream = ""; +var streamWindow = new WebWindow('Stream', "https://dl.dropboxusercontent.com/u/17344741/jsstreamplayer/jsstreamplayerdomain-zone.html", 0, 0, false); +var visible = false; + +// Set up toggleStreamPlayButton overlay. +var toggleStreamPlayButton = Overlays.addOverlay("text", { + x: 122, + y: 310, + width: 38, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + visible: false, + text: " Play" +}); + +// Set up toggleStreamStopButton overlay. +var toggleStreamStopButton = Overlays.addOverlay("text", { + x: 166, + y: 310, + width: 40, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + visible: false, + text: " Stop" +}); + +// Set up increaseVolumeButton overlay. +var toggleIncreaseVolumeButton = Overlays.addOverlay("text", { + x: 211, + y: 310, + width: 18, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + visible: false, + text: " +" +}); + +// Set up decreaseVolumeButton overlay. +var toggleDecreaseVolumeButton = Overlays.addOverlay("text", { + x: 234, + y: 310, + width: 15, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + visible: false, + text: " -" +}); + +// Function to change JSON object stream. +function changeStream(stream) { + var streamJSON = { + action: "changeStream", + stream: stream + } + streamWindow.eventBridge.emitScriptEvent(JSON.stringify(streamJSON)); +} + +// Function to change JSON object volume. +function changeVolume(volume) { + var volumeJSON = { + action: "changeVolume", + volume: volume + } + streamWindow.eventBridge.emitScriptEvent(JSON.stringify(volumeJSON)); +} + +// Function that adds mousePressEvent functionality to connect UI to enter stream URL, play and stop stream. +function mousePressEvent(event) { + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleStreamPlayButton) { + changeStream(stream); + volume = 0.25; + changeVolume(volume); + } + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleStreamStopButton) { + changeStream(""); + } + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleIncreaseVolumeButton) { + volume += 0.25; + changeVolume(volume); + } + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleDecreaseVolumeButton) { + volume -= 0.25; + changeVolume(volume); + } +} + +// Function checking bool if in proper zone. +function isOurZone(properties) { + return stream != "" && properties.type == "Zone"; +} + +// Function to toggle visibile the overlay. +function toggleVisible(newVisibility) { + if (newVisibility != visible) { + visible = newVisibility; + Overlays.editOverlay(toggleStreamPlayButton, {visible: visible}); + Overlays.editOverlay(toggleStreamStopButton, {visible: visible}); + Overlays.editOverlay(toggleIncreaseVolumeButton, {visible: visible}); + Overlays.editOverlay(toggleDecreaseVolumeButton, {visible: visible}); + } +} + +// Function to check if avatar is in proper domain. +Window.domainChanged.connect(function() { + Script.stop(); +}); + +// Function to check if avatar is within zone. +Entities.enterEntity.connect(function(entityID) { + print("Entered..." + JSON.stringify(entityID)); + var properties = Entities.getEntityProperties(entityID); + stream = properties.userData; + if(isOurZone(properties)) + { + lastZone = properties.name; + toggleVisible(true); + } +}) + +// Function to check if avatar is leaving zone. +Entities.leaveEntity.connect(function(entityID) { + print("Left..." + JSON.stringify(entityID)); + var properties = Entities.getEntityProperties(entityID); + if (properties.name == lastZone && properties.type == "Zone") { + print("Leaving Zone!"); + toggleVisible(false); + changeStream(""); + } +}) + +// Function to delete overlays upon exit. +function onScriptEnding() { + Overlays.deleteOverlay(toggleStreamPlayButton); + Overlays.deleteOverlay(toggleStreamStopButton); + Overlays.deleteOverlay(toggleIncreaseVolumeButton); + Overlays.deleteOverlay(toggleDecreaseVolumeButton); + changeStream(""); + streamWindow.deleteLater(); +} + +// Connect mouse and hide WebWindow. +Controller.mousePressEvent.connect(mousePressEvent); +streamWindow.setVisible(false); + +// Call function upon ending script. +Script.scriptEnding.connect(onScriptEnding); \ No newline at end of file From 7be33dcb5845908254950206ce6cff794ff1edde Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Sat, 25 Jul 2015 11:40:58 -0400 Subject: [PATCH 081/242] Limit the amount of time consumed by rendering QML --- .../render-utils/src/OffscreenQmlSurface.cpp | 21 +++++++++++++------ .../render-utils/src/OffscreenQmlSurface.h | 1 + 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/libraries/render-utils/src/OffscreenQmlSurface.cpp b/libraries/render-utils/src/OffscreenQmlSurface.cpp index 3ebc7704a8..056f9dbc6d 100644 --- a/libraries/render-utils/src/OffscreenQmlSurface.cpp +++ b/libraries/render-utils/src/OffscreenQmlSurface.cpp @@ -19,6 +19,7 @@ #include "FboCache.h" #include +#include class QMyQuickRenderControl : public QQuickRenderControl { protected: @@ -44,7 +45,10 @@ Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus") // Time between receiving a request to render the offscreen UI actually triggering // the render. Could possibly be increased depending on the framerate we expect to // achieve. -static const int SMALL_INTERVAL = 5; +static const int MAX_QML_FRAMERATE = 10; +static const int MIN_RENDER_INTERVAL_US = USECS_PER_SECOND / MAX_QML_FRAMERATE; +static const int MIN_TIMER_MS = 5; + OffscreenQmlSurface::OffscreenQmlSurface() : _renderControl(new QMyQuickRenderControl), _fboCache(new FboCache) { @@ -90,7 +94,6 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { // When Quick says there is a need to render, we will not render immediately. Instead, // a timer with a small interval is used to get better performance. _updateTimer.setSingleShot(true); - _updateTimer.setInterval(SMALL_INTERVAL); connect(&_updateTimer, &QTimer::timeout, this, &OffscreenQmlSurface::updateQuick); // Now hook up the signals. For simplicy we don't differentiate between @@ -170,13 +173,18 @@ QObject* OffscreenQmlSurface::load(const QUrl& qmlSource, std::function MIN_RENDER_INTERVAL_US) { + _updateTimer.setInterval(MIN_TIMER_MS); + } else { + _updateTimer.setInterval((MIN_RENDER_INTERVAL_US - lastInterval) / USECS_PER_MSEC); + } _updateTimer.start(); } } @@ -243,6 +251,7 @@ void OffscreenQmlSurface::updateQuick() { if (_paused) { return; } + if (!makeCurrent()) { return; } @@ -270,11 +279,11 @@ void OffscreenQmlSurface::updateQuick() { // Need a debug context with sync logging to figure out why. // for now just clear the errors glGetError(); -// Q_ASSERT(!glGetError()); _quickWindow->resetOpenGLState(); QOpenGLFramebufferObject::bindDefault(); + _lastRenderTime = usecTimestampNow(); // Force completion of all the operations before we emit the texture as being ready for use glFinish(); diff --git a/libraries/render-utils/src/OffscreenQmlSurface.h b/libraries/render-utils/src/OffscreenQmlSurface.h index b892806c44..1fbf69ef4d 100644 --- a/libraries/render-utils/src/OffscreenQmlSurface.h +++ b/libraries/render-utils/src/OffscreenQmlSurface.h @@ -86,6 +86,7 @@ private: QQuickItem* _rootItem{ nullptr }; QTimer _updateTimer; FboCache* _fboCache; + quint64 _lastRenderTime{ 0 }; bool _polish{ true }; bool _paused{ true }; MouseTranslator _mouseTranslator{ [](const QPointF& p) { return p; } }; From d25e09af74544db662a2e92637439386b00cb284 Mon Sep 17 00:00:00 2001 From: Marcel Verhagen Date: Sun, 26 Jul 2015 00:48:31 +0200 Subject: [PATCH 082/242] removed the last 2 filename lines, they should not be there. --- libraries/fbx/src/FBXReader.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 554d56ee5a..69482a8c81 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1463,11 +1463,9 @@ QByteArray fileOnUrl(const QByteArray filenameString,const QString url) { if (checkFile.exists() && checkFile.isFile()) { filename = filename.replace('\\', '/'); } else { - // there is not texture at the filename. Assume it is in the root dir. + // there is no texture at the fbx dir with the filename added. Assume it is in the fbx dir. filename = filename.mid(qMax(filename.lastIndexOf('\\'), filename.lastIndexOf('/')) + 1); } - filename = filename.mid(qMax(filename.lastIndexOf('\\'), filename.lastIndexOf('/')) + 1); - filename = filename.replace('\\', '/'); return filename; } From 24fff719c57d2a6daf565b9a5410b29fb8c431f1 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 25 Jul 2015 21:11:23 -0700 Subject: [PATCH 083/242] quiet compiler --- interface/src/Application.cpp | 4 ---- interface/src/avatar/Avatar.cpp | 4 ++-- interface/src/avatar/MyAvatar.cpp | 1 - interface/src/devices/DdeFaceTracker.h | 5 ++--- interface/src/ui/ApplicationCompositor.cpp | 2 +- interface/src/ui/AudioStatsDialog.cpp | 6 ++++-- interface/src/ui/overlays/Cube3DOverlay.cpp | 1 - 7 files changed, 9 insertions(+), 14 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index c2be31cfb3..fa2a82ecb4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3389,14 +3389,10 @@ void Application::renderRearViewMirror(RenderArgs* renderArgs, const QRect& regi // set the bounds of rear mirror view gpu::Vec4i viewport; if (billboard) { - QSize size = DependencyManager::get()->getFrameBufferSize(); viewport = gpu::Vec4i(0, 0, region.width(), region.height()); } else { // if not rendering the billboard, the region is in device independent coordinates; must convert to device - QSize size = DependencyManager::get()->getFrameBufferSize(); float ratio = (float)QApplication::desktop()->windowHandle()->devicePixelRatio() * getRenderResolutionScale(); - int x = region.x() * ratio; - int y = region.y() * ratio; int width = region.width() * ratio; int height = region.height() * ratio; viewport = gpu::Vec4i(0, 0, width, height); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 9f457e558d..095a225952 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -462,8 +462,8 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { const float LOOKING_AT_ME_ALPHA_START = 0.8f; const float LOOKING_AT_ME_DURATION = 0.5f; // seconds quint64 now = usecTimestampNow(); - float alpha = LOOKING_AT_ME_ALPHA_START - * (1.0f - ((float)(usecTimestampNow() - getHead()->getLookingAtMeStarted())) + float alpha = LOOKING_AT_ME_ALPHA_START + * (1.0f - ((float)(now - getHead()->getLookingAtMeStarted())) / (LOOKING_AT_ME_DURATION * (float)USECS_PER_SECOND)); if (alpha > 0.0f) { QSharedPointer geometry = getHead()->getFaceModel().getGeometry(); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index c6c6919325..f332173568 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1290,7 +1290,6 @@ bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const { void MyAvatar::updateOrientation(float deltaTime) { // Smoothly rotate body with arrow keys - float driveLeft = _driveKeys[ROT_LEFT] - _driveKeys[ROT_RIGHT]; float targetSpeed = (_driveKeys[ROT_LEFT] - _driveKeys[ROT_RIGHT]) * YAW_SPEED; if (targetSpeed != 0.0f) { const float ROTATION_RAMP_TIMESCALE = 0.1f; diff --git a/interface/src/devices/DdeFaceTracker.h b/interface/src/devices/DdeFaceTracker.h index 5536fa14bd..9673f541d2 100644 --- a/interface/src/devices/DdeFaceTracker.h +++ b/interface/src/devices/DdeFaceTracker.h @@ -91,13 +91,12 @@ private: int _leftBlinkIndex; int _rightBlinkIndex; - int _leftEyeOpenIndex; - int _rightEyeOpenIndex; - int _leftEyeDownIndex; int _rightEyeDownIndex; int _leftEyeInIndex; int _rightEyeInIndex; + int _leftEyeOpenIndex; + int _rightEyeOpenIndex; int _browDownLeftIndex; int _browDownRightIndex; diff --git a/interface/src/ui/ApplicationCompositor.cpp b/interface/src/ui/ApplicationCompositor.cpp index 54fb4fbd1f..9bda88b3bf 100644 --- a/interface/src/ui/ApplicationCompositor.cpp +++ b/interface/src/ui/ApplicationCompositor.cpp @@ -495,7 +495,7 @@ void ApplicationCompositor::renderControllerPointers(gpu::Batch& batch) { glm::vec3 direction = glm::inverse(myAvatar->getOrientation()) * palmData->getFingerDirection(); // Get the angles, scaled between (-0.5,0.5) - float xAngle = (atan2(direction.z, direction.x) + PI_OVER_TWO); + float xAngle = (atan2f(direction.z, direction.x) + PI_OVER_TWO); float yAngle = 0.5f - ((atan2f(direction.z, direction.y) + (float)PI_OVER_TWO)); // Get the pixel range over which the xAngle and yAngle are scaled diff --git a/interface/src/ui/AudioStatsDialog.cpp b/interface/src/ui/AudioStatsDialog.cpp index 116cc60b5e..e57182e251 100644 --- a/interface/src/ui/AudioStatsDialog.cpp +++ b/interface/src/ui/AudioStatsDialog.cpp @@ -125,8 +125,10 @@ void AudioStatsDialog::renderStats() { audioInputBufferLatency = (double)_stats->getAudioInputMsecsReadStats().getWindowAverage(); inputRingBufferLatency = (double)_stats->getInputRungBufferMsecsAvailableStats().getWindowAverage(); networkRoundtripLatency = (double) audioMixerNodePointer->getPingMs(); - mixerRingBufferLatency = (double)_stats->getMixerAvatarStreamStats()._framesAvailableAverage * AudioConstants::NETWORK_FRAME_MSECS; - outputRingBufferLatency = (double)downstreamAudioStreamStats._framesAvailableAverage * AudioConstants::NETWORK_FRAME_MSECS; + mixerRingBufferLatency = (double)_stats->getMixerAvatarStreamStats()._framesAvailableAverage * + (double)AudioConstants::NETWORK_FRAME_MSECS; + outputRingBufferLatency = (double)downstreamAudioStreamStats._framesAvailableAverage * + (double)AudioConstants::NETWORK_FRAME_MSECS; audioOutputBufferLatency = (double)_stats->getAudioOutputMsecsUnplayedStats().getWindowAverage(); } diff --git a/interface/src/ui/overlays/Cube3DOverlay.cpp b/interface/src/ui/overlays/Cube3DOverlay.cpp index 961d7f765b..200a1a328f 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.cpp +++ b/interface/src/ui/overlays/Cube3DOverlay.cpp @@ -36,7 +36,6 @@ void Cube3DOverlay::render(RenderArgs* args) { // TODO: handle registration point?? glm::vec3 position = getPosition(); - glm::vec3 center = getCenter(); glm::vec3 dimensions = getDimensions(); glm::quat rotation = getRotation(); From 5cc0b45850fa489dc0439a2a1c40c4f5a3089bc8 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 15 Jul 2015 18:27:39 -0700 Subject: [PATCH 084/242] Improved ParticleEffectEntityItem rendering and updates * Created custom pipelines and shaders for untextured and textured particle rendering. * Created custom render payload for particles * Moved all particle updates into simulation rather then render. * Uses pendingChanges.updateItem lambda to update the playload with new data for rendering. * ParticleEffectEntityItem now updates its dimensions properly, based on emitter properties. * Bug fix for dt not accumulating properly, during gaps between updates. now we just update all the time. (super cheap tho, if there are no particles animating) --- examples/particles.js | 7 +- libraries/entities-renderer/CMakeLists.txt | 2 + .../RenderableParticleEffectEntityItem.cpp | 327 ++++++++++++++---- .../src/RenderableParticleEffectEntityItem.h | 25 +- .../src/textured_particle.slf | 20 ++ .../src/textured_particle.slv | 28 ++ .../src/untextured_particle.slf | 16 + .../src/untextured_particle.slv | 24 ++ .../entities/src/ParticleEffectEntityItem.cpp | 80 ++++- .../entities/src/ParticleEffectEntityItem.h | 14 +- 10 files changed, 457 insertions(+), 86 deletions(-) create mode 100644 libraries/entities-renderer/src/textured_particle.slf create mode 100644 libraries/entities-renderer/src/textured_particle.slv create mode 100644 libraries/entities-renderer/src/untextured_particle.slf create mode 100644 libraries/entities-renderer/src/untextured_particle.slv diff --git a/examples/particles.js b/examples/particles.js index c26458a9af..deb6228fff 100644 --- a/examples/particles.js +++ b/examples/particles.js @@ -44,6 +44,7 @@ emitStrength: emitStrength, emitDirection: emitDirection, color: color, + lifespan: 1.0, visible: true, locked: false }); @@ -67,13 +68,13 @@ var objs = []; function Init() { objs.push(new TestBox()); - objs.push(new TestFx({ red: 255, blue: 0, green: 0 }, + objs.push(new TestFx({ red: 255, green: 0, blue: 0 }, { x: 0.5, y: 1.0, z: 0.0 }, 100, 3, 1)); - objs.push(new TestFx({ red: 0, blue: 255, green: 0 }, + objs.push(new TestFx({ red: 0, green: 255, blue: 0 }, { x: 0, y: 1, z: 0 }, 1000, 5, 0.5)); - objs.push(new TestFx({ red: 0, blue: 0, green: 255 }, + objs.push(new TestFx({ red: 0, green: 0, blue: 255 }, { x: -0.5, y: 1, z: 0 }, 100, 3, 1)); } diff --git a/libraries/entities-renderer/CMakeLists.txt b/libraries/entities-renderer/CMakeLists.txt index 810d5f5843..e9adf2b750 100644 --- a/libraries/entities-renderer/CMakeLists.txt +++ b/libraries/entities-renderer/CMakeLists.txt @@ -1,5 +1,7 @@ set(TARGET_NAME entities-renderer) +AUTOSCRIBE_SHADER_LIB(gpu model render) + # use setup_hifi_library macro to setup our project and link appropriate Qt modules setup_hifi_library(Widgets OpenGL Network Script) diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index 48ac83dfc2..738a150dc5 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -14,22 +14,141 @@ #include #include #include +#include #include "EntitiesRendererLogging.h" #include "RenderableParticleEffectEntityItem.h" +#include "untextured_particle_vert.h" +#include "untextured_particle_frag.h" +#include "textured_particle_vert.h" +#include "textured_particle_frag.h" + +class ParticlePayload { +public: + typedef render::Payload Payload; + typedef Payload::DataPointer Pointer; + typedef RenderableParticleEffectEntityItem::Vertex Vertex; + + ParticlePayload() : _vertexFormat(std::make_shared()), + _vertexBuffer(std::make_shared()), + _indexBuffer(std::make_shared()) { + _vertexFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element::VEC3F_XYZ, 0); + _vertexFormat->setAttribute(gpu::Stream::TEXCOORD, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV), offsetof(Vertex, uv)); + _vertexFormat->setAttribute(gpu::Stream::COLOR, 0, gpu::Element::COLOR_RGBA_32, offsetof(Vertex, rgba)); + } + + void setPipeline(gpu::PipelinePointer pipeline) { _pipeline = pipeline; } + const gpu::PipelinePointer& getPipeline() const { return _pipeline; } + + const Transform& getModelTransform() const { return _modelTransform; } + void setModelTransform(const Transform& modelTransform) { _modelTransform = modelTransform; } + + const AABox& getBound() const { return _bound; } + void setBound(AABox& bound) { _bound = bound; } + + gpu::BufferPointer getVertexBuffer() { return _vertexBuffer; } + const gpu::BufferPointer& getVertexBuffer() const { return _vertexBuffer; } + + gpu::BufferPointer getIndexBuffer() { return _indexBuffer; } + const gpu::BufferPointer& getIndexBuffer() const { return _indexBuffer; } + + void setTexture(gpu::TexturePointer texture) { _texture = texture; } + const gpu::TexturePointer& getTexture() const { return _texture; } + + bool getVisibleFlag() const { return _visibleFlag; } + void setVisibleFlag(bool visibleFlag) { _visibleFlag = visibleFlag; } + + void render(RenderArgs* args) const { + assert(_pipeline); + + gpu::Batch& batch = *args->_batch; + batch.setPipeline(_pipeline); + + if (_texture) { + batch.setResourceTexture(0, _texture); + } + + batch.setModelTransform(_modelTransform); + batch.setInputFormat(_vertexFormat); + batch.setInputBuffer(0, _vertexBuffer, 0, sizeof(Vertex)); + batch.setIndexBuffer(gpu::UINT16, _indexBuffer, 0); + + auto numIndices = _indexBuffer->getSize() / sizeof(uint16_t); + batch.drawIndexed(gpu::TRIANGLES, numIndices); + } + +protected: + Transform _modelTransform; + AABox _bound; + gpu::PipelinePointer _pipeline; + gpu::Stream::FormatPointer _vertexFormat; + gpu::BufferPointer _vertexBuffer; + gpu::BufferPointer _indexBuffer; + gpu::TexturePointer _texture; + bool _visibleFlag = true; +}; + +namespace render { + template <> + const ItemKey payloadGetKey(const ParticlePayload::Pointer& payload) { + if (payload->getVisibleFlag()) { + return ItemKey::Builder::transparentShape(); + } else { + return ItemKey::Builder().withInvisible().build(); + } + } + + template <> + const Item::Bound payloadGetBound(const ParticlePayload::Pointer& payload) { + return payload->getBound(); + } + + template <> + void payloadRender(const ParticlePayload::Pointer& payload, RenderArgs* args) { + payload->render(args); + } +} + +gpu::PipelinePointer RenderableParticleEffectEntityItem::_texturedPipeline; +gpu::PipelinePointer RenderableParticleEffectEntityItem::_untexturedPipeline; + EntityItemPointer RenderableParticleEffectEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { return std::make_shared(entityID, properties); } RenderableParticleEffectEntityItem::RenderableParticleEffectEntityItem(const EntityItemID& entityItemID, const EntityItemProperties& properties) : ParticleEffectEntityItem(entityItemID, properties) { - _cacheID = DependencyManager::get()->allocateID(); + + // lazy creation of particle system pipeline + if (!_untexturedPipeline && !_texturedPipeline) { + createPipelines(); + } } -void RenderableParticleEffectEntityItem::render(RenderArgs* args) { - Q_ASSERT(getType() == EntityTypes::ParticleEffect); - PerformanceTimer perfTimer("RenderableParticleEffectEntityItem::render"); +bool RenderableParticleEffectEntityItem::addToScene(EntityItemPointer self, + render::ScenePointer scene, + render::PendingChanges& pendingChanges) { + + auto particlePayload = std::shared_ptr(new ParticlePayload()); + particlePayload->setPipeline(_untexturedPipeline); + _renderItemId = scene->allocateID(); + auto renderData = ParticlePayload::Pointer(particlePayload); + auto renderPayload = render::PayloadPointer(new ParticlePayload::Payload(renderData)); + pendingChanges.resetItem(_renderItemId, renderPayload); + _scene = scene; + return true; +} + +void RenderableParticleEffectEntityItem::removeFromScene(EntityItemPointer self, + render::ScenePointer scene, + render::PendingChanges& pendingChanges) { + pendingChanges.removeItem(_renderItemId); + _scene = nullptr; +}; + +void RenderableParticleEffectEntityItem::update(const quint64& now) { + ParticleEffectEntityItem::update(now); if (_texturesChangedFlag) { if (_textures.isEmpty()) { @@ -42,71 +161,151 @@ void RenderableParticleEffectEntityItem::render(RenderArgs* args) { _texturesChangedFlag = false; } - bool textured = _texture && _texture->isLoaded(); - updateQuads(args, textured); - - Q_ASSERT(args->_batch); - gpu::Batch& batch = *args->_batch; - if (textured) { - batch.setResourceTexture(0, _texture->getGPUTexture()); - } - batch.setModelTransform(getTransformToCenter()); - DependencyManager::get()->bindSimpleProgram(batch, textured); - DependencyManager::get()->renderVertices(batch, gpu::QUADS, _cacheID); -}; + updateRenderItem(); +} static glm::vec3 zSortAxis; static bool zSort(const glm::vec3& rhs, const glm::vec3& lhs) { return glm::dot(rhs, ::zSortAxis) > glm::dot(lhs, ::zSortAxis); } -void RenderableParticleEffectEntityItem::updateQuads(RenderArgs* args, bool textured) { - float particleRadius = getParticleRadius(); - glm::vec4 particleColor(toGlm(getXColor()), getLocalRenderAlpha()); - - glm::vec3 upOffset = args->_viewFrustum->getUp() * particleRadius; - glm::vec3 rightOffset = args->_viewFrustum->getRight() * particleRadius; - - QVector vertices; - QVector positions; - QVector textureCoords; - vertices.reserve(getLivingParticleCount() * VERTS_PER_PARTICLE); - - if (textured) { - textureCoords.reserve(getLivingParticleCount() * VERTS_PER_PARTICLE); - } - positions.reserve(getLivingParticleCount()); - - - for (quint32 i = _particleHeadIndex; i != _particleTailIndex; i = (i + 1) % _maxParticles) { - positions.append(_particlePositions[i]); - if (textured) { - textureCoords.append(glm::vec2(0, 1)); - textureCoords.append(glm::vec2(1, 1)); - textureCoords.append(glm::vec2(1, 0)); - textureCoords.append(glm::vec2(0, 0)); - } - } - - // sort particles back to front - ::zSortAxis = args->_viewFrustum->getDirection(); - qSort(positions.begin(), positions.end(), zSort); - - for (int i = 0; i < positions.size(); i++) { - glm::vec3 pos = (textured) ? positions[i] : _particlePositions[i]; - - // generate corners of quad aligned to face the camera. - vertices.append(pos + rightOffset + upOffset); - vertices.append(pos - rightOffset + upOffset); - vertices.append(pos - rightOffset - upOffset); - vertices.append(pos + rightOffset - upOffset); - - } - - if (textured) { - DependencyManager::get()->updateVertices(_cacheID, vertices, textureCoords, particleColor); - } else { - DependencyManager::get()->updateVertices(_cacheID, vertices, particleColor); - } +uint32_t toRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { + return ((uint32_t)r | (uint32_t)g << 8 | (uint32_t)b << 16 | (uint32_t)a << 24); } +void RenderableParticleEffectEntityItem::updateRenderItem() { + + if (!_scene) + return; + + float particleRadius = getParticleRadius(); + auto xcolor = getXColor(); + auto alpha = (uint8_t)(glm::clamp(getLocalRenderAlpha(), 0.0f, 1.0f) * 255.0f); + auto rgba = toRGBA(xcolor.red, xcolor.green, xcolor.blue, alpha); + + // make a copy of each particle position + std::vector positions; + positions.reserve(getLivingParticleCount()); + for (quint32 i = _particleHeadIndex; i != _particleTailIndex; i = (i + 1) % _maxParticles) { + positions.push_back(_particlePositions[i]); + } + + // sort particles back to front + // NOTE: this is view frustum might be one frame out of date. + auto frustum = AbstractViewStateInterface::instance()->getCurrentViewFrustum(); + ::zSortAxis = frustum->getDirection(); + qSort(positions.begin(), positions.end(), zSort); + + // allocate vertices + _vertices.clear(); + + // build vertices from particle positions + const glm::vec3 upOffset = frustum->getUp() * particleRadius; + const glm::vec3 rightOffset = frustum->getRight() * particleRadius; + for (auto&& pos : positions) { + // generate corners of quad aligned to face the camera. + _vertices.emplace_back(pos + rightOffset + upOffset, glm::vec2(1.0f, 1.0f), rgba); + _vertices.emplace_back(pos - rightOffset + upOffset, glm::vec2(0.0f, 1.0f), rgba); + _vertices.emplace_back(pos - rightOffset - upOffset, glm::vec2(0.0f, 0.0f), rgba); + _vertices.emplace_back(pos + rightOffset - upOffset, glm::vec2(1.0f, 0.0f), rgba); + } + + render::PendingChanges pendingChanges; + pendingChanges.updateItem(_renderItemId, [&](ParticlePayload& payload) { + + // update vertex buffer + auto vertexBuffer = payload.getVertexBuffer(); + size_t numBytes = sizeof(Vertex) * _vertices.size(); + vertexBuffer->resize(numBytes); + gpu::Byte* data = vertexBuffer->editData(); + memcpy(data, &(_vertices[0]), numBytes); + + // FIXME, don't update index buffer if num particles has not changed. + // update index buffer + auto indexBuffer = payload.getIndexBuffer(); + auto numQuads = (_vertices.size() / 4); + numBytes = sizeof(uint16_t) * numQuads * 6; + indexBuffer->resize(numBytes); + data = indexBuffer->editData(); + auto indexPtr = reinterpret_cast(data); + for (size_t i = 0; i < numQuads; ++i) { + indexPtr[i * 6 + 0] = i * 4 + 0; + indexPtr[i * 6 + 1] = i * 4 + 1; + indexPtr[i * 6 + 2] = i * 4 + 3; + indexPtr[i * 6 + 3] = i * 4 + 1; + indexPtr[i * 6 + 4] = i * 4 + 2; + indexPtr[i * 6 + 5] = i * 4 + 3; + } + + // update transform + glm::quat rot = _transform.getRotation(); + glm::vec3 pos = _transform.getTranslation(); + Transform t; + t.setRotation(rot); + t.setTranslation(pos); + payload.setModelTransform(t); + + // transform _particleMinBound and _particleMaxBound corners into world coords + glm::vec3 d = _particleMaxBound - _particleMinBound; + glm::vec3 corners[8] = { + pos + rot * (_particleMinBound + glm::vec3(0.0f, 0.0f, 0.0f)), + pos + rot * (_particleMinBound + glm::vec3(d.x, 0.0f, 0.0f)), + pos + rot * (_particleMinBound + glm::vec3(0.0f, d.y, 0.0f)), + pos + rot * (_particleMinBound + glm::vec3(d.x, d.y, 0.0f)), + pos + rot * (_particleMinBound + glm::vec3(0.0f, 0.0f, d.z)), + pos + rot * (_particleMinBound + glm::vec3(d.x, 0.0f, d.z)), + pos + rot * (_particleMinBound + glm::vec3(0.0f, d.y, d.z)), + pos + rot * (_particleMinBound + glm::vec3(d.x, d.y, d.z)) + }; + glm::vec3 min(FLT_MAX, FLT_MAX, FLT_MAX); + glm::vec3 max = -min; + for (int i = 0; i < 8; i++) { + min.x = std::min(min.x, corners[i].x); + min.y = std::min(min.y, corners[i].y); + min.z = std::min(min.z, corners[i].z); + max.x = std::max(max.x, corners[i].x); + max.y = std::max(max.y, corners[i].y); + max.z = std::max(max.z, corners[i].z); + } + AABox bound(min, max - min); + payload.setBound(bound); + + bool textured = _texture && _texture->isLoaded(); + if (textured) { + payload.setTexture(_texture->getGPUTexture()); + payload.setPipeline(_texturedPipeline); + } else { + payload.setTexture(nullptr); + payload.setPipeline(_untexturedPipeline); + } + }); + + _scene->enqueuePendingChanges(pendingChanges); +} + +void RenderableParticleEffectEntityItem::createPipelines() { + if (!_untexturedPipeline) { + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, + gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, + gpu::State::BLEND_OP_ADD, gpu::State::ONE); + auto vertShader = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(untextured_particle_vert))); + auto fragShader = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(untextured_particle_frag))); + auto program = gpu::ShaderPointer(gpu::Shader::createProgram(vertShader, fragShader)); + _untexturedPipeline = gpu::PipelinePointer(gpu::Pipeline::create(program, state)); + } + if (!_texturedPipeline) { + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, + gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, + gpu::State::BLEND_OP_ADD, gpu::State::ONE); + auto vertShader = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(textured_particle_vert))); + auto fragShader = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(textured_particle_frag))); + auto program = gpu::ShaderPointer(gpu::Shader::createProgram(vertShader, fragShader)); + _texturedPipeline = gpu::PipelinePointer(gpu::Pipeline::create(program, state)); + } +} diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h index 4ecea45ad0..9581c43ca5 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h @@ -16,20 +16,35 @@ #include "RenderableEntityItem.h" class RenderableParticleEffectEntityItem : public ParticleEffectEntityItem { +friend class ParticlePayload; public: static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); RenderableParticleEffectEntityItem(const EntityItemID& entityItemID, const EntityItemProperties& properties); - virtual void render(RenderArgs* args); - void updateQuads(RenderArgs* args, bool textured); + virtual void update(const quint64& now) override; - SIMPLE_RENDERABLE(); + void updateRenderItem(); + + virtual bool addToScene(EntityItemPointer self, render::ScenePointer scene, render::PendingChanges& pendingChanges); + virtual void removeFromScene(EntityItemPointer self, render::ScenePointer scene, render::PendingChanges& pendingChanges); protected: + render::ItemID _renderItemId; - int _cacheID; - const int VERTS_PER_PARTICLE = 4; + struct Vertex { + Vertex(glm::vec3 xyzIn, glm::vec2 uvIn, uint32_t rgbaIn) : xyz(xyzIn), uv(uvIn), rgba(rgbaIn) {} + glm::vec3 xyz; + glm::vec2 uv; + uint32_t rgba; + }; + static void createPipelines(); + + std::vector _vertices; + static gpu::PipelinePointer _untexturedPipeline; + static gpu::PipelinePointer _texturedPipeline; + + render::ScenePointer _scene; NetworkTexturePointer _texture; }; diff --git a/libraries/entities-renderer/src/textured_particle.slf b/libraries/entities-renderer/src/textured_particle.slf new file mode 100644 index 0000000000..543aa643aa --- /dev/null +++ b/libraries/entities-renderer/src/textured_particle.slf @@ -0,0 +1,20 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// fragment shader +// +// 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 +// + +uniform sampler2D colorMap; + +varying vec4 varColor; +varying vec2 varTexCoord; + +void main(void) { + vec4 color = texture2D(colorMap, varTexCoord); + gl_FragColor = color * varColor; +} diff --git a/libraries/entities-renderer/src/textured_particle.slv b/libraries/entities-renderer/src/textured_particle.slv new file mode 100644 index 0000000000..7564feb1ce --- /dev/null +++ b/libraries/entities-renderer/src/textured_particle.slv @@ -0,0 +1,28 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// particle vertex shader +// +// 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 gpu/Transform.slh@> + +<$declareStandardTransform()$> + +varying vec4 varColor; +varying vec2 varTexCoord; + +void main(void) { + // pass along the color & uvs to fragment shader + varColor = gl_Color; + varTexCoord = gl_MultiTexCoord0.xy; + + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformModelToClipPos(cam, obj, gl_Vertex, gl_Position)$> +} diff --git a/libraries/entities-renderer/src/untextured_particle.slf b/libraries/entities-renderer/src/untextured_particle.slf new file mode 100644 index 0000000000..bb3ed77e3f --- /dev/null +++ b/libraries/entities-renderer/src/untextured_particle.slf @@ -0,0 +1,16 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// fragment shader +// +// 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 +// + +varying vec4 varColor; + +void main(void) { + gl_FragColor = varColor; +} diff --git a/libraries/entities-renderer/src/untextured_particle.slv b/libraries/entities-renderer/src/untextured_particle.slv new file mode 100644 index 0000000000..2975dab046 --- /dev/null +++ b/libraries/entities-renderer/src/untextured_particle.slv @@ -0,0 +1,24 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// particle vertex shader +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Transform.slh@> + +<$declareStandardTransform()$> + +varying vec4 varColor; + +void main(void) { + // pass along the diffuse color + varColor = gl_Color; + + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformModelToClipPos(cam, obj, gl_Vertex, gl_Position)$> +} \ No newline at end of file diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index 95effa2980..a687e2be7a 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -39,6 +39,7 @@ #include "EntityTree.h" #include "EntityTreeElement.h" #include "EntitiesLogging.h" +#include "EntityScriptingInterface.h" #include "ParticleEffectEntityItem.h" const xColor ParticleEffectEntityItem::DEFAULT_COLOR = { 255, 255, 255 }; @@ -92,6 +93,74 @@ ParticleEffectEntityItem::ParticleEffectEntityItem(const EntityItemID& entityIte ParticleEffectEntityItem::~ParticleEffectEntityItem() { } +void ParticleEffectEntityItem::setDimensions(const glm::vec3& value) { + computeAndUpdateDimensions(); +} + +void ParticleEffectEntityItem::setLifespan(float lifespan) { + _lifespan = lifespan; + computeAndUpdateDimensions(); +} + +void ParticleEffectEntityItem::setEmitDirection(glm::vec3 emitDirection) { + _emitDirection = glm::normalize(emitDirection); + computeAndUpdateDimensions(); +} + +void ParticleEffectEntityItem::setEmitStrength(float emitStrength) { + _emitStrength = emitStrength; + computeAndUpdateDimensions(); +} + +void ParticleEffectEntityItem::setLocalGravity(float localGravity) { + _localGravity = localGravity; + computeAndUpdateDimensions(); +} + +void ParticleEffectEntityItem::setParticleRadius(float particleRadius) { + _particleRadius = particleRadius; + computeAndUpdateDimensions(); +} + +void ParticleEffectEntityItem::computeAndUpdateDimensions() { + + const float t = _lifespan * 1.1f; // add 10% extra time, to account for incremental timer accumulation error. + const float maxOffset = (0.5f * 0.25f * _emitStrength) + _particleRadius; + + // bounds for x and z is easy to compute because there is no at^2 term. + float xMax = (_emitDirection.x * _emitStrength + maxOffset) * t; + float xMin = (_emitDirection.x * _emitStrength - maxOffset) * t; + + float zMax = (_emitDirection.z * _emitStrength + maxOffset) * t; + float zMin = (_emitDirection.z * _emitStrength - maxOffset) * t; + + // yEnd is where the particle will end. + float a = _localGravity; + float atSquared = a * t * t; + float v = _emitDirection.y * _emitStrength + maxOffset; + float vt = v * t; + float yEnd = 0.5f * atSquared + vt; + + // yApex is where the particle is at it's apex. + float yApexT = (-v / a); + float yApex = 0.0f; + + // only set apex if it's within the lifespan of the particle. + if (yApexT >= 0.0f && yApexT <= t) { + yApex = -(v * v) / (2.0f * a); + } + + float yMax = std::max(yApex, yEnd); + float yMin = std::min(yApex, yEnd); + + // times 2 because dimensions are diameters not radii. + glm::vec3 dims(2.0f * std::max(fabs(xMin), fabs(xMax)), + 2.0f * std::max(fabs(yMin), fabs(yMax)), + 2.0f * std::max(fabs(zMin), fabs(zMax))); + + EntityItem::setDimensions(dims); +} + EntityItemProperties ParticleEffectEntityItem::getProperties() const { EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class @@ -245,7 +314,7 @@ bool ParticleEffectEntityItem::isAnimatingSomething() const { } bool ParticleEffectEntityItem::needsToCallUpdate() const { - return isAnimatingSomething() ? true : EntityItem::needsToCallUpdate(); + return true; } void ParticleEffectEntityItem::update(const quint64& now) { @@ -260,13 +329,6 @@ void ParticleEffectEntityItem::update(const quint64& now) { if (isAnimatingSomething()) { stepSimulation(deltaTime); - - // update the dimensions - glm::vec3 dims; - dims.x = glm::max(glm::abs(_particleMinBound.x), glm::abs(_particleMaxBound.x)) * 2.0f; - dims.y = glm::max(glm::abs(_particleMinBound.y), glm::abs(_particleMaxBound.y)) * 2.0f; - dims.z = glm::max(glm::abs(_particleMinBound.z), glm::abs(_particleMaxBound.z)) * 2.0f; - setDimensions(dims); } EntityItem::update(now); // let our base class handle it's updates... @@ -319,7 +381,7 @@ void ParticleEffectEntityItem::setAnimationSettings(const QString& value) { qCDebug(entities) << "ParticleEffectEntityItem::setAnimationSettings() calling setAnimationFrameIndex()..."; qCDebug(entities) << " settings:" << value; qCDebug(entities) << " settingsMap[frameIndex]:" << settingsMap["frameIndex"]; - qCDebug(entities" frameIndex: %20.5f", frameIndex); + qCDebug(entities, " frameIndex: %20.5f", frameIndex); } #endif diff --git a/libraries/entities/src/ParticleEffectEntityItem.h b/libraries/entities/src/ParticleEffectEntityItem.h index 3136ab6c7c..994c609f0f 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.h +++ b/libraries/entities/src/ParticleEffectEntityItem.h @@ -86,12 +86,14 @@ public: void setAnimationLastFrame(float lastFrame) { _animationLoop.setLastFrame(lastFrame); } float getAnimationLastFrame() const { return _animationLoop.getLastFrame(); } + virtual void setDimensions(const glm::vec3& value) override; + static const quint32 DEFAULT_MAX_PARTICLES; void setMaxParticles(quint32 maxParticles); quint32 getMaxParticles() const { return _maxParticles; } static const float DEFAULT_LIFESPAN; - void setLifespan(float lifespan) { _lifespan = lifespan; } + void setLifespan(float lifespan); float getLifespan() const { return _lifespan; } static const float DEFAULT_EMIT_RATE; @@ -99,21 +101,23 @@ public: float getEmitRate() const { return _emitRate; } static const glm::vec3 DEFAULT_EMIT_DIRECTION; - void setEmitDirection(glm::vec3 emitDirection) { _emitDirection = glm::normalize(emitDirection); } + void setEmitDirection(glm::vec3 emitDirection); const glm::vec3& getEmitDirection() const { return _emitDirection; } static const float DEFAULT_EMIT_STRENGTH; - void setEmitStrength(float emitStrength) { _emitStrength = emitStrength; } + void setEmitStrength(float emitStrength); float getEmitStrength() const { return _emitStrength; } static const float DEFAULT_LOCAL_GRAVITY; - void setLocalGravity(float localGravity) { _localGravity = localGravity; } + void setLocalGravity(float localGravity); float getLocalGravity() const { return _localGravity; } static const float DEFAULT_PARTICLE_RADIUS; - void setParticleRadius(float particleRadius) { _particleRadius = particleRadius; } + void setParticleRadius(float particleRadius); float getParticleRadius() const { return _particleRadius; } + void computeAndUpdateDimensions(); + bool getAnimationIsPlaying() const { return _animationLoop.isRunning(); } float getAnimationFrameIndex() const { return _animationLoop.getFrameIndex(); } float getAnimationFPS() const { return _animationLoop.getFPS(); } From 8ac06c4059f696dcac5fec67a1241fbb52c2c079 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 27 Jul 2015 08:48:16 -0700 Subject: [PATCH 085/242] remove friend class from Avatar --- interface/src/avatar/Avatar.h | 13 +++++++------ interface/src/avatar/AvatarManager.cpp | 8 ++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index a587e69642..8ae4b9fb4d 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -186,9 +186,10 @@ public: virtual void computeShapeInfo(ShapeInfo& shapeInfo); - friend class AvatarManager; + void setMotionState(AvatarMotionState* motionState) { _motionState = motionState; } + AvatarMotionState* getMotionState() { return _motionState; } -signals: + signals: void collisionWithAvatar(const QUuid& myUUID, const QUuid& theirUUID, const CollisionInfo& collision); protected: @@ -218,7 +219,7 @@ protected: glm::vec3 _worldUpDirection; float _stringLength; bool _moving; ///< set when position is changing - + // protected methods... glm::vec3 getBodyRightDirection() const { return getOrientation() * IDENTITY_RIGHT; } glm::vec3 getBodyUpDirection() const { return getOrientation() * IDENTITY_UP; } @@ -243,7 +244,7 @@ protected: virtual void updateJointMappings(); render::ItemID _renderItemID; - + private: bool _initialized; NetworkTexturePointer _billboardTexture; @@ -251,9 +252,9 @@ private: bool _isLookAtTarget; void renderBillboard(RenderArgs* renderArgs); - + float getBillboardSize() const; - + static int _jointConesID; int _voiceSphereID; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index ead330a41f..ee59a01e07 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -179,11 +179,11 @@ AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWe // protected void AvatarManager::removeAvatarMotionState(AvatarSharedPointer avatar) { auto rawPointer = std::static_pointer_cast(avatar); - AvatarMotionState* motionState= rawPointer->_motionState; + AvatarMotionState* motionState = rawPointer->getMotionState(); if (motionState) { // clean up physics stuff motionState->clearObjectBackPointer(); - rawPointer->_motionState = nullptr; + rawPointer->setMotionState(nullptr); _avatarMotionStates.remove(motionState); _motionStatesToAdd.remove(motionState); _motionStatesToDelete.push_back(motionState); @@ -307,7 +307,7 @@ void AvatarManager::updateAvatarPhysicsShape(const QUuid& id) { AvatarHash::iterator avatarItr = _avatarHash.find(id); if (avatarItr != _avatarHash.end()) { auto avatar = std::static_pointer_cast(avatarItr.value()); - AvatarMotionState* motionState = avatar->_motionState; + AvatarMotionState* motionState = avatar->getMotionState(); if (motionState) { motionState->addDirtyFlags(EntityItem::DIRTY_SHAPE); } else { @@ -316,7 +316,7 @@ void AvatarManager::updateAvatarPhysicsShape(const QUuid& id) { btCollisionShape* shape = ObjectMotionState::getShapeManager()->getShape(shapeInfo); if (shape) { AvatarMotionState* motionState = new AvatarMotionState(avatar.get(), shape); - avatar->_motionState = motionState; + avatar->setMotionState(motionState); _motionStatesToAdd.insert(motionState); _avatarMotionStates.insert(motionState); } From 7e582830415b351d40e3366ab4f77bf6b53b7194 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 27 Jul 2015 08:54:08 -0700 Subject: [PATCH 086/242] change the methods that aren't called from outside the class to be private --- interface/src/avatar/MyAvatar.h | 131 +++++++++++++++----------------- 1 file changed, 62 insertions(+), 69 deletions(-) diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 802c92ec2c..4fd66c0347 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -39,51 +39,40 @@ public: MyAvatar(RigPointer rig); ~MyAvatar(); - QByteArray toByteArray(); + void reset(); void update(float deltaTime); - void simulate(float deltaTime); void preRender(RenderArgs* renderArgs); - void updateFromTrackers(float deltaTime); - virtual void render(RenderArgs* renderArgs, const glm::vec3& cameraPositio) override; - virtual void renderBody(RenderArgs* renderArgs, ViewFrustum* renderFrustum, float glowLevel = 0.0f) override; - virtual bool shouldRenderHead(const RenderArgs* renderArgs) const override; - - // setters void setLeanScale(float scale) { _leanScale = scale; } - void setShouldRenderLocally(bool shouldRender) { _shouldRender = shouldRender; } void setRealWorldFieldOfView(float realWorldFov) { _realWorldFieldOfView.set(realWorldFov); } - // getters float getLeanScale() const { return _leanScale; } Q_INVOKABLE glm::vec3 getDefaultEyePosition() const; - bool getShouldRenderLocally() const { return _shouldRender; } + float getRealWorldFieldOfView() { return _realWorldFieldOfView.get(); } - + const QList& getAnimationHandles() const { return _rig->getAnimationHandles(); } AnimationHandlePointer addAnimationHandle() { return _rig->createAnimationHandle(); } void removeAnimationHandle(const AnimationHandlePointer& handle) { _rig->removeAnimationHandle(handle); } - /// Allows scripts to run animations. Q_INVOKABLE void startAnimation(const QString& url, float fps = 30.0f, float priority = 1.0f, bool loop = false, - bool hold = false, float firstFrame = 0.0f, float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList()); - + bool hold = false, float firstFrame = 0.0f, + float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList()); /// Stops an animation as identified by a URL. Q_INVOKABLE void stopAnimation(const QString& url); - + /// Starts an animation by its role, using the provided URL and parameters if the avatar doesn't have a custom /// animation for the role. Q_INVOKABLE void startAnimationByRole(const QString& role, const QString& url = QString(), float fps = 30.0f, - float priority = 1.0f, bool loop = false, bool hold = false, float firstFrame = 0.0f, - float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList()); - + float priority = 1.0f, bool loop = false, bool hold = false, float firstFrame = 0.0f, + float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList()); /// Stops an animation identified by its role. Q_INVOKABLE void stopAnimationByRole(const QString& role); - Q_INVOKABLE AnimationDetails getAnimationDetailsByRole(const QString& role); Q_INVOKABLE AnimationDetails getAnimationDetails(const QString& url); - + void clearJointAnimationPriorities(); + // get/set avatar data void saveData(); void loadData(); @@ -94,41 +83,32 @@ public: // Set what driving keys are being pressed to control thrust levels void clearDriveKeys(); void setDriveKeys(int key, float val) { _driveKeys[key] = val; }; - bool getDriveKeys(int key) { return _driveKeys[key] != 0.0f; }; - void relayDriveKeysToCharacterController(); - bool isMyAvatar() const { return true; } - eyeContactTarget getEyeContactTarget(); - virtual int parseDataFromBuffer(const QByteArray& buffer); - static void sendKillAvatar(); - + Q_INVOKABLE glm::vec3 getTrackedHeadPosition() const { return _trackedHeadPosition; } Q_INVOKABLE glm::vec3 getHeadPosition() const { return getHead()->getPosition(); } Q_INVOKABLE float getHeadFinalYaw() const { return getHead()->getFinalYaw(); } Q_INVOKABLE float getHeadFinalRoll() const { return getHead()->getFinalRoll(); } Q_INVOKABLE float getHeadFinalPitch() const { return getHead()->getFinalPitch(); } Q_INVOKABLE float getHeadDeltaPitch() const { return getHead()->getDeltaPitch(); } - + Q_INVOKABLE glm::vec3 getEyePosition() const { return getHead()->getEyePosition(); } - + Q_INVOKABLE glm::vec3 getTargetAvatarPosition() const { return _targetAvatarPosition; } + AvatarWeakPointer getLookAtTargetAvatar() const { return _lookAtTargetAvatar; } void updateLookAtTargetAvatar(); void clearLookAtTargetAvatar(); - - virtual void setJointRotations(QVector jointRotations); - virtual void setJointData(int index, const glm::quat& rotation); - virtual void clearJointData(int index); - virtual void clearJointsData(); Q_INVOKABLE void useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName = QString()); Q_INVOKABLE void useHeadURL(const QUrl& headURL, const QString& modelName = QString()); Q_INVOKABLE void useBodyURL(const QUrl& bodyURL, const QString& modelName = QString()); - Q_INVOKABLE void useHeadAndBodyURLs(const QUrl& headURL, const QUrl& bodyURL, const QString& headName = QString(), const QString& bodyName = QString()); + Q_INVOKABLE void useHeadAndBodyURLs(const QUrl& headURL, const QUrl& bodyURL, + const QString& headName = QString(), const QString& bodyName = QString()); Q_INVOKABLE bool getUseFullAvatar() const { return _useFullAvatar; } Q_INVOKABLE const QUrl& getFullAvatarURLFromPreferences() const { return _fullAvatarURLFromPreferences; } @@ -143,48 +123,30 @@ public: virtual void setAttachmentData(const QVector& attachmentData); - virtual glm::vec3 getSkeletonPosition() const; - void updateLocalAABox(); DynamicCharacterController* getCharacterController() { return &_characterController; } - - void clearJointAnimationPriorities(); - glm::vec3 getScriptedMotorVelocity() const { return _scriptedMotorVelocity; } - float getScriptedMotorTimescale() const { return _scriptedMotorTimescale; } - QString getScriptedMotorFrame() const; - - void setScriptedMotorVelocity(const glm::vec3& velocity); - void setScriptedMotorTimescale(float timescale); - void setScriptedMotorFrame(QString frame); const QString& getCollisionSoundURL() {return _collisionSoundURL; } void setCollisionSoundURL(const QString& url); void clearScriptableSettings(); - virtual void attach(const QString& modelURL, const QString& jointName = QString(), - const glm::vec3& translation = glm::vec3(), const glm::quat& rotation = glm::quat(), float scale = 1.0f, - bool allowDuplicates = false, bool useSaved = true); - /// Renders a laser pointer for UI picking - void renderLaserPointers(gpu::Batch& batch); + glm::vec3 getLaserPointerTipPosition(const PalmData* palm); - - const RecorderPointer getRecorder() const { return _recorder; } - const PlayerPointer getPlayer() const { return _player; } - + float getBoomLength() const { return _boomLength; } void setBoomLength(float boomLength) { _boomLength = boomLength; } - + static const float ZOOM_MIN; static const float ZOOM_MAX; static const float ZOOM_DEFAULT; - + public slots: void increaseSize(); void decreaseSize(); void resetSize(); - + void goToLocation(const glm::vec3& newPosition, bool hasOrientation = false, const glm::quat& newOrientation = glm::quat(), bool shouldFaceLocation = false); @@ -204,7 +166,7 @@ public slots: void clearReferential(); bool setModelReferential(const QUuid& id); bool setJointReferential(const QUuid& id, int jointIndex); - + bool isRecording(); qint64 recorderElapsed(); void startRecording(); @@ -213,7 +175,7 @@ public slots: void loadLastRecording(); virtual void rebuildSkeletonBody(); - + signals: void transformChanged(); void newCollisionSoundURL(const QUrl& url); @@ -221,6 +183,37 @@ signals: private: + QByteArray toByteArray(); + void simulate(float deltaTime); + void updateFromTrackers(float deltaTime); + virtual void render(RenderArgs* renderArgs, const glm::vec3& cameraPositio) override; + virtual void renderBody(RenderArgs* renderArgs, ViewFrustum* renderFrustum, float glowLevel = 0.0f) override; + virtual bool shouldRenderHead(const RenderArgs* renderArgs) const override; + void setShouldRenderLocally(bool shouldRender) { _shouldRender = shouldRender; } + bool getShouldRenderLocally() const { return _shouldRender; } + bool getDriveKeys(int key) { return _driveKeys[key] != 0.0f; }; + bool isMyAvatar() const { return true; } + virtual int parseDataFromBuffer(const QByteArray& buffer); + virtual void setJointRotations(QVector jointRotations); + virtual void setJointData(int index, const glm::quat& rotation); + virtual void clearJointData(int index); + virtual void clearJointsData(); + virtual glm::vec3 getSkeletonPosition() const; + void updateLocalAABox(); + glm::vec3 getScriptedMotorVelocity() const { return _scriptedMotorVelocity; } + float getScriptedMotorTimescale() const { return _scriptedMotorTimescale; } + QString getScriptedMotorFrame() const; + void setScriptedMotorVelocity(const glm::vec3& velocity); + void setScriptedMotorTimescale(float timescale); + void setScriptedMotorFrame(QString frame); + virtual void attach(const QString& modelURL, const QString& jointName = QString(), + const glm::vec3& translation = glm::vec3(), const glm::quat& rotation = glm::quat(), float scale = 1.0f, + bool allowDuplicates = false, bool useSaved = true); + + void renderLaserPointers(gpu::Batch& batch); + const RecorderPointer getRecorder() const { return _recorder; } + const PlayerPointer getPlayer() const { return _player; } + bool cameraInsideHead() const; // These are made private for MyAvatar so that you will use the "use" methods instead @@ -235,7 +228,7 @@ private: bool _wasPushing; bool _isPushing; bool _isBraking; - + float _boomLength; float _trapDuration; // seconds that avatar has been trapped by collisions @@ -256,30 +249,30 @@ private: bool _shouldRender; bool _billboardValid; float _oculusYawOffset; - + bool _feetTouchFloor; eyeContactTarget _eyeContactTarget; RecorderPointer _recorder; - + glm::vec3 _trackedHeadPosition; - + Setting::Handle _realWorldFieldOfView; - - // private methods + + // private methods void updateOrientation(float deltaTime); glm::vec3 applyKeyboardMotor(float deltaTime, const glm::vec3& velocity, bool isHovering); glm::vec3 applyScriptedMotor(float deltaTime, const glm::vec3& velocity); void updatePosition(float deltaTime); void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency); void maybeUpdateBillboard(); - + // Avatar Preferences bool _useFullAvatar = false; QUrl _fullAvatarURLFromPreferences; QUrl _headURLFromPreferences; QUrl _skeletonURLFromPreferences; - + QString _headModelName; QString _bodyModelName; QString _fullAvatarModelName; From 81375e47e07e02f9dd6798536e629d51242c53ce Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 27 Jul 2015 09:07:26 -0700 Subject: [PATCH 087/242] remove methods from MyAvatar: setJointRotations setJointData clearJointData clearJointsData --- interface/src/avatar/MyAvatar.cpp | 27 --------------------------- interface/src/avatar/MyAvatar.h | 4 ---- 2 files changed, 31 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 0dceb79402..fe4d4bc3cb 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -900,33 +900,6 @@ glm::vec3 MyAvatar::getDefaultEyePosition() const { const float SCRIPT_PRIORITY = DEFAULT_PRIORITY + 1.0f; const float RECORDER_PRIORITY = SCRIPT_PRIORITY + 1.0f; -void MyAvatar::setJointRotations(QVector jointRotations) { - int numStates = glm::min(_skeletonModel.getJointStateCount(), jointRotations.size()); - for (int i = 0; i < numStates; ++i) { - // HACK: ATM only Recorder calls setJointRotations() so we hardcode its priority here - _skeletonModel.setJointState(i, true, jointRotations[i], RECORDER_PRIORITY); - } -} - -void MyAvatar::setJointData(int index, const glm::quat& rotation) { - if (QThread::currentThread() == thread()) { - // HACK: ATM only JS scripts call setJointData() on MyAvatar so we hardcode the priority - _skeletonModel.setJointState(index, true, rotation, SCRIPT_PRIORITY); - } -} - -void MyAvatar::clearJointData(int index) { - if (QThread::currentThread() == thread()) { - // HACK: ATM only JS scripts call clearJointData() on MyAvatar so we hardcode the priority - _skeletonModel.setJointState(index, false, glm::quat(), 0.0f); - _skeletonModel.clearJointAnimationPriority(index); - } -} - -void MyAvatar::clearJointsData() { - clearJointAnimationPriorities(); -} - void MyAvatar::clearJointAnimationPriorities() { int numStates = _skeletonModel.getJointStateCount(); for (int i = 0; i < numStates; ++i) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 4fd66c0347..f208b68520 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -194,10 +194,6 @@ private: bool getDriveKeys(int key) { return _driveKeys[key] != 0.0f; }; bool isMyAvatar() const { return true; } virtual int parseDataFromBuffer(const QByteArray& buffer); - virtual void setJointRotations(QVector jointRotations); - virtual void setJointData(int index, const glm::quat& rotation); - virtual void clearJointData(int index); - virtual void clearJointsData(); virtual glm::vec3 getSkeletonPosition() const; void updateLocalAABox(); glm::vec3 getScriptedMotorVelocity() const { return _scriptedMotorVelocity; } From d17ddae53787a3ad094f82f65fc5491c75774a4f Mon Sep 17 00:00:00 2001 From: "Kevin M. Thomas" Date: Mon, 27 Jul 2015 12:14:50 -0400 Subject: [PATCH 088/242] Added .js file to examples/example/audio and added public bucket url functionality. --- examples/example/audio/jsstreamplayer.js | 145 +++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 examples/example/audio/jsstreamplayer.js diff --git a/examples/example/audio/jsstreamplayer.js b/examples/example/audio/jsstreamplayer.js new file mode 100644 index 0000000000..27ed32e3b5 --- /dev/null +++ b/examples/example/audio/jsstreamplayer.js @@ -0,0 +1,145 @@ +// +// #20622: JS Stream Player +// ************************* +// +// Created by Kevin M. Thomas and Thoys 07/17/15. +// Copyright 2015 High Fidelity, Inc. +// kevintown.net +// +// JavaScript for the High Fidelity interface that creates a stream player with a UI and keyPressEvents for adding a stream URL in addition to play, stop and volume functionality. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +// Declare HiFi public bucket. +HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; + +// Declare variables and set up new WebWindow. +var stream; +var volume = 1; +var streamWindow = new WebWindow('Stream', HIFI_PUBLIC_BUCKET + "examples/html/jsstreamplayer.html", 0, 0, false); + +// Set up toggleStreamURLButton overlay. +var toggleStreamURLButton = Overlays.addOverlay("text", { + x: 76, + y: 275, + width: 40, + height: 28, + backgroundColor: {red: 0, green: 0, blue: 0}, + color: {red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + text: " URL" +}); + +// Set up toggleStreamPlayButton overlay. +var toggleStreamPlayButton = Overlays.addOverlay("text", { + x: 122, + y: 275, + width: 38, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + text: " Play" +}); + +// Set up toggleStreamStopButton overlay. +var toggleStreamStopButton = Overlays.addOverlay("text", { + x: 166, + y: 275, + width: 40, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + text: " Stop" +}); + +// Set up increaseVolumeButton overlay. +var toggleIncreaseVolumeButton = Overlays.addOverlay("text", { + x: 211, + y: 275, + width: 18, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + text: " +" +}); + +// Set up decreaseVolumeButton overlay. +var toggleDecreaseVolumeButton = Overlays.addOverlay("text", { + x: 234, + y: 275, + width: 15, + height: 28, + backgroundColor: { red: 0, green: 0, blue: 0}, + color: { red: 255, green: 255, blue: 0}, + font: {size: 15}, + topMargin: 8, + text: " -" +}); + +// Function that adds mousePressEvent functionality to connect UI to enter stream URL, play and stop stream. +function mousePressEvent(event) { + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleStreamURLButton) { + stream = Window.prompt("Enter Stream: "); + var streamJSON = { + action: "changeStream", + stream: stream + } + streamWindow.eventBridge.emitScriptEvent(JSON.stringify(streamJSON)); + } + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleStreamPlayButton) { + var streamJSON = { + action: "changeStream", + stream: stream + } + streamWindow.eventBridge.emitScriptEvent(JSON.stringify(streamJSON)); + } + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleStreamStopButton) { + var streamJSON = { + action: "changeStream", + stream: "" + } + streamWindow.eventBridge.emitScriptEvent(JSON.stringify(streamJSON)); + } + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleIncreaseVolumeButton) { + volume += 0.2; + var volumeJSON = { + action: "changeVolume", + volume: volume + } + streamWindow.eventBridge.emitScriptEvent(JSON.stringify(volumeJSON)); + } + if (Overlays.getOverlayAtPoint({x: event.x, y: event.y}) == toggleDecreaseVolumeButton) { + volume -= 0.2; + var volumeJSON = { + action: "changeVolume", + volume: volume + } + streamWindow.eventBridge.emitScriptEvent(JSON.stringify(volumeJSON)); + } +} + +// Call function. +Controller.mousePressEvent.connect(mousePressEvent); +streamWindow.setVisible(false); + +// Function to delete overlays upon exit. +function onScriptEnding() { + Overlays.deleteOverlay(toggleStreamURLButton); + Overlays.deleteOverlay(toggleStreamPlayButton); + Overlays.deleteOverlay(toggleStreamStopButton); + Overlays.deleteOverlay(toggleIncreaseVolumeButton); + Overlays.deleteOverlay(toggleDecreaseVolumeButton); +} + +// Call function. +Script.scriptEnding.connect(onScriptEnding); \ No newline at end of file From dac6667ee6dff76f2ba031de6c58d35e891bbaa8 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 27 Jul 2015 09:21:24 -0700 Subject: [PATCH 089/242] Start of state. --- libraries/animation/src/Rig.cpp | 69 +++++++++++++++++++++------- libraries/animation/src/Rig.h | 19 ++++++-- libraries/render-utils/src/Model.cpp | 2 +- 3 files changed, 68 insertions(+), 22 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 7d520e7930..c0602dc870 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -9,6 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include #include "AnimationHandle.h" @@ -38,18 +39,15 @@ void Rig::removeAnimationHandle(const AnimationHandlePointer& handle) { void Rig::startAnimation(const QString& url, float fps, float priority, bool loop, bool hold, float firstFrame, float lastFrame, const QStringList& maskedJoints) { - qCDebug(animation) << "startAnimation" << url << fps << priority << loop << hold << firstFrame << lastFrame << maskedJoints; - AnimationHandlePointer handle = nullptr; + //qCDebug(animation) << "startAnimation" << url << fps << priority << loop << hold << firstFrame << lastFrame << maskedJoints; foreach (const AnimationHandlePointer& candidate, _animationHandles) { if (candidate->getURL() == url) { - handle = candidate; - break; + candidate->start(); + return; } } - if (!handle) { - handle = createAnimationHandle(); - handle->setURL(url); - } + AnimationHandlePointer handle = createAnimationHandle(); + handle->setURL(url); handle->setFPS(fps); handle->setPriority(priority); handle->setLoop(loop); @@ -63,18 +61,17 @@ void Rig::startAnimation(const QString& url, float fps, float priority, void Rig::addAnimationByRole(const QString& role, const QString& url, float fps, float priority, bool loop, bool hold, float firstFrame, float lastFrame, const QStringList& maskedJoints, bool startAutomatically) { // check for a configured animation for the role - qCDebug(animation) << "addAnimationByRole" << role << url << fps << priority << loop << hold << firstFrame << lastFrame << maskedJoints << startAutomatically; - AnimationHandlePointer handle = nullptr; + //qCDebug(animation) << "addAnimationByRole" << role << url << fps << priority << loop << hold << firstFrame << lastFrame << maskedJoints << startAutomatically; foreach (const AnimationHandlePointer& candidate, _animationHandles) { if (candidate->getRole() == role) { - handle = candidate; - break; + if (startAutomatically) { + candidate->start(); + } + return; } } - if (!handle) { - handle = createAnimationHandle(); - handle->setRole(role); - } + AnimationHandlePointer handle = createAnimationHandle(); + handle->setRole(role); handle->setURL(url); handle->setFPS(fps); handle->setPriority(priority); @@ -344,7 +341,45 @@ glm::mat4 Rig::getJointVisibleTransform(int jointIndex) const { return maybeCauterizeHead(jointIndex).getVisibleTransform(); } -void Rig::simulateInternal(float deltaTime, glm::mat4 parentTransform) { +void Rig::simulateInternal(float deltaTime, glm::mat4 parentTransform, const glm::vec3& worldPosition, const glm::quat& worldRotation) { + glm::vec3 front = worldRotation * IDENTITY_FRONT; + glm::vec3 delta = worldPosition - _lastPosition ; + float forwardSpeed = glm::dot(delta, front) / deltaTime; + float rotationalSpeed = glm::angle(front, _lastFront) / deltaTime; + bool isWalking = std::abs(forwardSpeed) > 0.01; + bool isTurning = std::abs(rotationalSpeed) > 0.5; + + // Crude, until we have blending: + const float EXPECTED_INTERVAL = 1.0f / 60.0f; + if (deltaTime >= EXPECTED_INTERVAL) { + isTurning = isTurning && !isWalking; // Only one of walk/turn, walk wins. + isTurning = false; // FIXME + bool isIdle = !isWalking && !isTurning; + auto singleRole = [](bool walking, bool turning, bool idling) { + return walking ? "walk" : (turning ? "turn" : (idling ? "idle" : "")); + }; + QString toStop = singleRole(_isWalking && !isWalking, _isTurning && !isTurning, _isIdle && !isIdle); + if (!toStop.isEmpty()) { + //qCDebug(animation) << "isTurning" << isTurning << "fronts" << front << _lastFront << glm::angle(front, _lastFront) << rotationalSpeed; + //stopAnimationByRole(toStop); + } + QString newRole = singleRole(isWalking && !_isWalking, isTurning && !_isTurning, isIdle && !_isIdle); + if (!newRole.isEmpty()) { + //startAnimationByRole(newRole); + qCDebug(animation) << deltaTime << ":" /*<< _lastPosition << worldPosition << "=>" */<< delta << "." << front << "=> " << forwardSpeed << newRole; + /*if (newRole == "idle") { + qCDebug(animation) << deltaTime << ":" << _lastPosition << worldPosition << "=>" << delta; + }*/ + } + + _lastPosition = worldPosition; + _positions[(++_positionIndex) % _positions.count()] = worldPosition; // exp. alt. to above line + _lastFront = front; + _isWalking = isWalking; + _isTurning = isTurning; + _isIdle = isIdle; + } + // update animations foreach (const AnimationHandlePointer& handle, _runningAnimations) { handle->simulate(deltaTime); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 2087ad6800..b56151b5be 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -71,8 +71,9 @@ public: float priority = 1.0f, bool loop = false, bool hold = false, float firstFrame = 0.0f, float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList()); void stopAnimationByRole(const QString& role); - void addAnimationByRole(const QString& role, const QString& url, float fps, float priority, - bool loop, bool hold, float firstFrame, float lastFrame, const QStringList& maskedJoints, bool startAutomatically); + void addAnimationByRole(const QString& role, const QString& url = QString(), float fps = 30.0f, + float priority = 1.0f, bool loop = false, bool hold = false, float firstFrame = 0.0f, + float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList(), bool startAutomatically = false); float initJointStates(QVector states, glm::mat4 parentTransform, int neckJointIndex); bool jointStatesEmpty() { return _jointStates.isEmpty(); }; @@ -107,7 +108,7 @@ public: void setJointTransform(int jointIndex, glm::mat4 newTransform); glm::mat4 getJointVisibleTransform(int jointIndex) const; void setJointVisibleTransform(int jointIndex, glm::mat4 newTransform); - void simulateInternal(float deltaTime, glm::mat4 parentTransform); + void simulateInternal(float deltaTime, glm::mat4 parentTransform, const glm::vec3& worldPosition, const glm::quat& worldRotation); bool setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, bool useRotation, int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, float priority, const QVector& freeLineage, glm::mat4 parentTransform); @@ -144,6 +145,16 @@ public: std::vector _headBones; bool _jointsAreDirty = false; int _neckJointIndex = -1; -}; + + bool _isWalking; + bool _isTurning; + bool _isIdle; + glm::vec3 _lastFront; + glm::vec3 _lastPosition; + // or, experimentally... + QVector _positions = QVector(4); + QVector _timeIntervals = QVector(4); + int _positionIndex; + }; #endif /* defined(__hifi__Rig__) */ diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index ddf18f6ef8..a316da0f99 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1346,7 +1346,7 @@ void Model::simulateInternal(float deltaTime) { const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - _rig->simulateInternal(deltaTime, parentTransform); + _rig->simulateInternal(deltaTime, parentTransform, getTranslation(), getRotation()); _shapesAreDirty = !_shapes.isEmpty(); From 5844b594dcfcdee259c9c67d50994cda801cb663 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 27 Jul 2015 09:27:16 -0700 Subject: [PATCH 090/242] Converted magic numbers to constants. --- .../RenderableParticleEffectEntityItem.cpp | 26 +++++++++++-------- .../entities/src/ParticleEffectEntityItem.cpp | 3 ++- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index 738a150dc5..2b4626c2c3 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -175,8 +175,9 @@ uint32_t toRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { void RenderableParticleEffectEntityItem::updateRenderItem() { - if (!_scene) + if (!_scene) { return; + } float particleRadius = getParticleRadius(); auto xcolor = getXColor(); @@ -223,18 +224,20 @@ void RenderableParticleEffectEntityItem::updateRenderItem() { // FIXME, don't update index buffer if num particles has not changed. // update index buffer auto indexBuffer = payload.getIndexBuffer(); - auto numQuads = (_vertices.size() / 4); - numBytes = sizeof(uint16_t) * numQuads * 6; + const size_t NUM_VERTS_PER_PARTICLE = 4; + const size_t NUM_INDICES_PER_PARTICLE = 6; + auto numQuads = (_vertices.size() / NUM_VERTS_PER_PARTICLE); + numBytes = sizeof(uint16_t) * numQuads * NUM_INDICES_PER_PARTICLE; indexBuffer->resize(numBytes); data = indexBuffer->editData(); auto indexPtr = reinterpret_cast(data); for (size_t i = 0; i < numQuads; ++i) { - indexPtr[i * 6 + 0] = i * 4 + 0; - indexPtr[i * 6 + 1] = i * 4 + 1; - indexPtr[i * 6 + 2] = i * 4 + 3; - indexPtr[i * 6 + 3] = i * 4 + 1; - indexPtr[i * 6 + 4] = i * 4 + 2; - indexPtr[i * 6 + 5] = i * 4 + 3; + indexPtr[i * NUM_INDICES_PER_PARTICLE + 0] = i * NUM_VERTS_PER_PARTICLE + 0; + indexPtr[i * NUM_INDICES_PER_PARTICLE + 1] = i * NUM_VERTS_PER_PARTICLE + 1; + indexPtr[i * NUM_INDICES_PER_PARTICLE + 2] = i * NUM_VERTS_PER_PARTICLE + 3; + indexPtr[i * NUM_INDICES_PER_PARTICLE + 3] = i * NUM_VERTS_PER_PARTICLE + 1; + indexPtr[i * NUM_INDICES_PER_PARTICLE + 4] = i * NUM_VERTS_PER_PARTICLE + 2; + indexPtr[i * NUM_INDICES_PER_PARTICLE + 5] = i * NUM_VERTS_PER_PARTICLE + 3; } // update transform @@ -247,7 +250,8 @@ void RenderableParticleEffectEntityItem::updateRenderItem() { // transform _particleMinBound and _particleMaxBound corners into world coords glm::vec3 d = _particleMaxBound - _particleMinBound; - glm::vec3 corners[8] = { + const size_t NUM_BOX_CORNERS = 8; + glm::vec3 corners[NUM_BOX_CORNERS] = { pos + rot * (_particleMinBound + glm::vec3(0.0f, 0.0f, 0.0f)), pos + rot * (_particleMinBound + glm::vec3(d.x, 0.0f, 0.0f)), pos + rot * (_particleMinBound + glm::vec3(0.0f, d.y, 0.0f)), @@ -259,7 +263,7 @@ void RenderableParticleEffectEntityItem::updateRenderItem() { }; glm::vec3 min(FLT_MAX, FLT_MAX, FLT_MAX); glm::vec3 max = -min; - for (int i = 0; i < 8; i++) { + for (size_t i = 0; i < NUM_BOX_CORNERS; i++) { min.x = std::min(min.x, corners[i].x); min.y = std::min(min.y, corners[i].y); min.z = std::min(min.z, corners[i].z); diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index a687e2be7a..71a5f87eb1 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -125,7 +125,8 @@ void ParticleEffectEntityItem::setParticleRadius(float particleRadius) { void ParticleEffectEntityItem::computeAndUpdateDimensions() { const float t = _lifespan * 1.1f; // add 10% extra time, to account for incremental timer accumulation error. - const float maxOffset = (0.5f * 0.25f * _emitStrength) + _particleRadius; + const float MAX_RANDOM_FACTOR = (0.5f * 0.25); + const float maxOffset = (MAX_RANDOM_FACTOR * _emitStrength) + _particleRadius; // bounds for x and z is easy to compute because there is no at^2 term. float xMax = (_emitDirection.x * _emitStrength + maxOffset) * t; From 604ef5dc71311982e25a90bb80eba499b46524c9 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 27 Jul 2015 09:28:56 -0700 Subject: [PATCH 091/242] Fixed float constant. --- libraries/entities/src/ParticleEffectEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index 71a5f87eb1..4dfc9dd436 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -125,7 +125,7 @@ void ParticleEffectEntityItem::setParticleRadius(float particleRadius) { void ParticleEffectEntityItem::computeAndUpdateDimensions() { const float t = _lifespan * 1.1f; // add 10% extra time, to account for incremental timer accumulation error. - const float MAX_RANDOM_FACTOR = (0.5f * 0.25); + const float MAX_RANDOM_FACTOR = (0.5f * 0.25f); const float maxOffset = (MAX_RANDOM_FACTOR * _emitStrength) + _particleRadius; // bounds for x and z is easy to compute because there is no at^2 term. From 9c57d1544fdc237fd4476972795e91050cdffa86 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 27 Jul 2015 09:53:27 -0700 Subject: [PATCH 092/242] fix OctreeSceneStat unpacking in Application --- interface/src/Application.cpp | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index fa2a82ecb4..c213d7629c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3596,19 +3596,13 @@ int Application::processOctreeStats(NLPacket& packet, SharedNodePointer sendingN int statsMessageLength = 0; const QUuid& nodeUUID = sendingNode->getUUID(); - OctreeSceneStats* octreeStats; - + // now that we know the node ID, let's add these stats to the stats for that node... _octreeSceneStatsLock.lockForWrite(); - auto it = _octreeServerSceneStats.find(nodeUUID); - if (it != _octreeServerSceneStats.end()) { - octreeStats = &it->second; - statsMessageLength = octreeStats->unpackFromPacket(packet); - } else { - OctreeSceneStats temp; - statsMessageLength = temp.unpackFromPacket(packet); - octreeStats = &temp; - } + + OctreeSceneStats* octreeStats = &_octreeServerSceneStats[nodeUUID]; + statsMessageLength = octreeStats->unpackFromPacket(packet); + _octreeSceneStatsLock.unlock(); VoxelPositionSize rootDetails; From 615218c77dc7c809a01da4265159805a81101da5 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 27 Jul 2015 09:58:58 -0700 Subject: [PATCH 093/242] use a ref in stats unpacking --- interface/src/Application.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index c213d7629c..69c10fc0ee 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3600,13 +3600,13 @@ int Application::processOctreeStats(NLPacket& packet, SharedNodePointer sendingN // now that we know the node ID, let's add these stats to the stats for that node... _octreeSceneStatsLock.lockForWrite(); - OctreeSceneStats* octreeStats = &_octreeServerSceneStats[nodeUUID]; - statsMessageLength = octreeStats->unpackFromPacket(packet); + OctreeSceneStats& octreeStats = _octreeServerSceneStats[nodeUUID]; + statsMessageLength = octreeStats.unpackFromPacket(packet); _octreeSceneStatsLock.unlock(); VoxelPositionSize rootDetails; - voxelDetailsForCode(octreeStats->getJurisdictionRoot(), rootDetails); + voxelDetailsForCode(octreeStats.getJurisdictionRoot(), rootDetails); // see if this is the first we've heard of this node... NodeToJurisdictionMap* jurisdiction = NULL; @@ -3631,7 +3631,7 @@ int Application::processOctreeStats(NLPacket& packet, SharedNodePointer sendingN // but OctreeSceneStats thinks it's just returning a reference to its contents. So we need to make a copy of the // details from the OctreeSceneStats to construct the JurisdictionMap JurisdictionMap jurisdictionMap; - jurisdictionMap.copyContents(octreeStats->getJurisdictionRoot(), octreeStats->getJurisdictionEndNodes()); + jurisdictionMap.copyContents(octreeStats.getJurisdictionRoot(), octreeStats.getJurisdictionEndNodes()); jurisdiction->lockForWrite(); (*jurisdiction)[nodeUUID] = jurisdictionMap; jurisdiction->unlock(); From 32d051396262b35956d1cf8b545744ac49861266 Mon Sep 17 00:00:00 2001 From: Marcel Verhagen Date: Mon, 27 Jul 2015 19:04:49 +0200 Subject: [PATCH 094/242] The 3Dconnextion files from https://github.com/highfidelity/hifi/pull/5351 For now without a merge conflict. Updated the menu name. Still have to look at the fast zooming and yaw on windows, probably have to add a var to prevent the button changes to be pushed to fast. Not sure why the yaw thing does not always work, could be that the position is also send at the same time and the input mapper does not not process all those synchronical. Probably will have to do something with masking the postion when the rotation is set for yaw. --- cmake/modules/FindconnexionClient.cmake | 38 + interface/CMakeLists.txt | 2 +- .../connexionclient/Inc/I3dMouseParams.h | 79 ++ interface/external/connexionclient/readme.txt | 4 + interface/src/Application.cpp | 6 + interface/src/Menu.cpp | 6 + interface/src/Menu.h | 1 + interface/src/devices/3Dconnexion.cpp | 1013 +++++++++++++++++ interface/src/devices/3Dconnexion.h | 244 ++++ tests/ui/src/main.cpp | 1 + 10 files changed, 1393 insertions(+), 1 deletion(-) create mode 100644 cmake/modules/FindconnexionClient.cmake create mode 100644 interface/external/connexionclient/Inc/I3dMouseParams.h create mode 100644 interface/external/connexionclient/readme.txt create mode 100755 interface/src/devices/3Dconnexion.cpp create mode 100755 interface/src/devices/3Dconnexion.h diff --git a/cmake/modules/FindconnexionClient.cmake b/cmake/modules/FindconnexionClient.cmake new file mode 100644 index 0000000000..1d6d7d4514 --- /dev/null +++ b/cmake/modules/FindconnexionClient.cmake @@ -0,0 +1,38 @@ +# +# FindconnexionClient.cmake +# +# Once done this will define +# +# 3DCONNEXIONCLIENT_INCLUDE_DIRS +# +# Created on 10/06/2015 by Marcel Verhagen +# 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 +# + +# setup hints for 3DCONNEXIONCLIENT search +include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") +hifi_library_search_hints("connexionclient") + +if (APPLE) + find_library(3DconnexionClient 3DconnexionClient) + if(EXISTS ${3DconnexionClient}) + set(CONNEXIONCLIENT_FOUND true) + set(CONNEXIONCLIENT_INCLUDE_DIR ${3DconnexionClient}) + set(CONNEXIONCLIENT_LIBRARY ${3DconnexionClient}) + set_target_properties(${TARGET_NAME} PROPERTIES LINK_FLAGS "-weak_framework 3DconnexionClient") + message(STATUS "Found 3Dconnexion") + mark_as_advanced(CONNEXIONCLIENT_INCLUDE_DIR CONNEXIONCLIENT_LIBRARY) + endif() +endif() + +if (WIN32) + find_path(CONNEXIONCLIENT_INCLUDE_DIRS I3dMouseParams.h PATH_SUFFIXES Inc HINTS ${CONNEXIONCLIENT_SEARCH_DIRS}) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(connexionClient DEFAULT_MSG CONNEXIONCLIENT_INCLUDE_DIRS) + + mark_as_advanced(CONNEXIONCLIENT_INCLUDE_DIRS CONNEXIONCLIENT_SEARCH_DIRS) +endif() diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 0c44ac801f..4ee3709f4a 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -2,7 +2,7 @@ set(TARGET_NAME interface) project(${TARGET_NAME}) # set a default root dir for each of our optional externals if it was not passed -set(OPTIONAL_EXTERNALS "Faceshift" "Sixense" "LeapMotion" "RtMidi" "SDL2" "RSSDK") +set(OPTIONAL_EXTERNALS "Faceshift" "Sixense" "LeapMotion" "RtMidi" "SDL2" "RSSDK" "connexionClient") foreach(EXTERNAL ${OPTIONAL_EXTERNALS}) string(TOUPPER ${EXTERNAL} ${EXTERNAL}_UPPERCASE) if (NOT ${${EXTERNAL}_UPPERCASE}_ROOT_DIR) diff --git a/interface/external/connexionclient/Inc/I3dMouseParams.h b/interface/external/connexionclient/Inc/I3dMouseParams.h new file mode 100644 index 0000000000..f250efe74f --- /dev/null +++ b/interface/external/connexionclient/Inc/I3dMouseParams.h @@ -0,0 +1,79 @@ +// +// 3DConnexion.cpp +// hifi +// +// Created by MarcelEdward Verhagen on 09-06-15. +// 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 I3D_MOUSE_PARAMS_H +#define I3D_MOUSE_PARAMS_H + +// Parameters for the 3D mouse based on the SDK from 3Dconnexion + +class I3dMouseSensor { +public: + enum Speed { + SPEED_LOW = 0, + SPEED_MID, + SPEED_HIGH + }; + + virtual bool IsPanZoom() const = 0; + virtual bool IsRotate() const = 0; + virtual Speed GetSpeed() const = 0; + + virtual void SetPanZoom(bool isPanZoom) = 0; + virtual void SetRotate(bool isRotate) = 0; + virtual void SetSpeed(Speed speed) = 0; + +protected: + virtual ~I3dMouseSensor() {} +}; + +class I3dMouseNavigation { +public: + enum Pivot { + PIVOT_MANUAL = 0, + PIVOT_AUTO, + PIVOT_AUTO_OVERRIDE + }; + + enum Navigation { + NAVIGATION_OBJECT_MODE = 0, + NAVIGATION_CAMERA_MODE, + NAVIGATION_FLY_MODE, + NAVIGATION_WALK_MODE, + NAVIGATION_HELICOPTER_MODE + }; + + enum PivotVisibility { + PIVOT_HIDE = 0, + PIVOT_SHOW, + PIVOT_SHOW_MOVING + }; + + virtual Navigation GetNavigationMode() const = 0; + virtual Pivot GetPivotMode() const = 0; + virtual PivotVisibility GetPivotVisibility() const = 0; + virtual bool IsLockHorizon() const = 0; + + virtual void SetLockHorizon(bool bOn) = 0; + virtual void SetNavigationMode(Navigation navigation) = 0; + virtual void SetPivotMode(Pivot pivot) = 0; + virtual void SetPivotVisibility(PivotVisibility visibility) = 0; + +protected: + virtual ~I3dMouseNavigation(){} +}; + +class I3dMouseParam : public I3dMouseSensor, public I3dMouseNavigation { +public: + virtual ~I3dMouseParam() {} +}; + +#endif diff --git a/interface/external/connexionclient/readme.txt b/interface/external/connexionclient/readme.txt new file mode 100644 index 0000000000..dd67a29449 --- /dev/null +++ b/interface/external/connexionclient/readme.txt @@ -0,0 +1,4 @@ +The mac version does not require any files here. 3D connexion should be installed from +http://www.3dconnexion.eu/service/drivers.html + +For windows a header file is required Inc/I3dMouseParams.h diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index fa2a82ecb4..33d88923eb 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -115,6 +115,7 @@ #include "devices/MIDIManager.h" #include "devices/OculusManager.h" #include "devices/TV3DManager.h" +#include "devices/3Dconnexion.h" #include "scripting/AccountScriptingInterface.h" #include "scripting/AudioDeviceScriptingInterface.h" @@ -639,6 +640,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : connect(applicationUpdater.data(), &AutoUpdater::newVersionIsAvailable, dialogsManager.data(), &DialogsManager::showUpdateDialog); applicationUpdater->checkForUpdate(); + // the 3Dconnexion device wants to be initiliazed after a window is displayed. + ConnexionClient::init(); + auto& packetReceiver = nodeList->getPacketReceiver(); packetReceiver.registerListener(PacketType::DomainConnectionDenied, this, "handleDomainConnectionDeniedPacket"); } @@ -750,6 +754,7 @@ Application::~Application() { Leapmotion::destroy(); RealSense::destroy(); + ConnexionClient::destroy(); qInstallMessageHandler(NULL); // NOTE: Do this as late as possible so we continue to get our log messages } @@ -1480,6 +1485,7 @@ void Application::focusOutEvent(QFocusEvent* event) { _keyboardMouseDevice.focusOutEvent(event); SixenseManager::getInstance().focusOutEvent(); SDL2Manager::getInstance()->focusOutEvent(); + ConnexionData::getInstance().focusOutEvent(); // synthesize events for keys currently pressed, since we may not get their release events foreach (int key, _keysPressed) { diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 91ae6a4d02..c347fdac67 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -29,6 +29,7 @@ #include "devices/Faceshift.h" #include "devices/RealSense.h" #include "devices/SixenseManager.h" +#include "devices/3Dconnexion.h" #include "MainWindow.h" #include "scripting/MenuScriptingInterface.h" #if defined(Q_OS_MAC) || defined(Q_OS_WIN) @@ -447,6 +448,11 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderLookAtVectors, 0, false); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderFocusIndicator, 0, false); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowWhosLookingAtMe, 0, false); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, + MenuOption::Connexion, + 0, false, + &ConnexionClient::getInstance(), + SLOT(toggleConnexion(bool))); MenuWrapper* handOptionsMenu = developerMenu->addMenu("Hands"); addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::AlignForearmsWithWrists, 0, false); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index bf0f89abb5..0edd93c5a6 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -161,6 +161,7 @@ namespace MenuOption { const QString CenterPlayerInView = "Center Player In View"; const QString Chat = "Chat..."; const QString Collisions = "Collisions"; + const QString Connexion = "Activate 3D Connexion Devices""; const QString Console = "Console..."; const QString ControlWithSpeech = "Control With Speech"; const QString CopyAddress = "Copy Address to Clipboard"; diff --git a/interface/src/devices/3Dconnexion.cpp b/interface/src/devices/3Dconnexion.cpp new file mode 100755 index 0000000000..111e2d5991 --- /dev/null +++ b/interface/src/devices/3Dconnexion.cpp @@ -0,0 +1,1013 @@ +// +// 3DConnexion.cpp +// hifi +// +// Created by MarcelEdward Verhagen on 09-06-15. +// 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 "3Dconnexion.h" +#include "UserActivityLogger.h" + +const float MAX_AXIS = 75.0f; // max forward = 2x speed + +void ConnexionData::focusOutEvent() { + _axisStateMap.clear(); + _buttonPressedMap.clear(); +}; + +ConnexionData& ConnexionData::getInstance() { + static ConnexionData sharedInstance; + return sharedInstance; +} + +ConnexionData::ConnexionData() { +} + +void ConnexionData::handleAxisEvent() { + //qCWarning(interfaceapp) << "pos state x = " << cc_position.x << " y = " << cc_position.y << " z = " << cc_position.z << " Rotation x = " << cc_rotation.x << " y = " << cc_rotation.y << " z = " << cc_rotation.z; + _axisStateMap[makeInput(ROTATION_AXIS_Y_POS).getChannel()] = (cc_rotation.y > 0.0f) ? cc_rotation.y / MAX_AXIS : 0.0f; + _axisStateMap[makeInput(ROTATION_AXIS_Y_NEG).getChannel()] = (cc_rotation.y < 0.0f) ? -cc_rotation.y / MAX_AXIS : 0.0f; + _axisStateMap[makeInput(POSITION_AXIS_X_POS).getChannel()] = (cc_position.x > 0.0f) ? cc_position.x / MAX_AXIS : 0.0f; + _axisStateMap[makeInput(POSITION_AXIS_X_NEG).getChannel()] = (cc_position.x < 0.0f) ? -cc_position.x / MAX_AXIS : 0.0f; + _axisStateMap[makeInput(POSITION_AXIS_Y_POS).getChannel()] = (cc_position.y > 0.0f) ? cc_position.y / MAX_AXIS : 0.0f; + _axisStateMap[makeInput(POSITION_AXIS_Y_NEG).getChannel()] = (cc_position.y < 0.0f) ? -cc_position.y / MAX_AXIS : 0.0f; + _axisStateMap[makeInput(POSITION_AXIS_Z_POS).getChannel()] = (cc_position.z > 0.0f) ? cc_position.z / MAX_AXIS : 0.0f; + _axisStateMap[makeInput(POSITION_AXIS_Z_NEG).getChannel()] = (cc_position.z < 0.0f) ? -cc_position.z / MAX_AXIS : 0.0f; + _axisStateMap[makeInput(ROTATION_AXIS_X_POS).getChannel()] = (cc_rotation.x > 0.0f) ? cc_rotation.x / MAX_AXIS : 0.0f; + _axisStateMap[makeInput(ROTATION_AXIS_X_NEG).getChannel()] = (cc_rotation.x < 0.0f) ? -cc_rotation.x / MAX_AXIS : 0.0f; + _axisStateMap[makeInput(ROTATION_AXIS_Z_POS).getChannel()] = (cc_rotation.z > 0.0f) ? cc_rotation.z / MAX_AXIS : 0.0f; + _axisStateMap[makeInput(ROTATION_AXIS_Z_NEG).getChannel()] = (cc_rotation.z < 0.0f) ? -cc_rotation.z / MAX_AXIS : 0.0f; +} + +void ConnexionData::setButton(int lastButtonState) { + _buttonPressedMap.clear(); + _buttonPressedMap.insert(lastButtonState); +} + +void ConnexionData::registerToUserInputMapper(UserInputMapper& mapper) { + // Grab the current free device ID + _deviceID = mapper.getFreeDeviceID(); + + auto proxy = UserInputMapper::DeviceProxy::Pointer(new UserInputMapper::DeviceProxy("ConnexionClient")); + proxy->getButton = [this](const UserInputMapper::Input& input, int timestamp) -> bool { return this->getButton(input.getChannel()); }; + proxy->getAxis = [this](const UserInputMapper::Input& input, int timestamp) -> float { return this->getAxis(input.getChannel()); }; + proxy->getAvailabeInputs = [this]() -> QVector { + QVector availableInputs; + + availableInputs.append(UserInputMapper::InputPair(makeInput(BUTTON_1), "Left button")); + availableInputs.append(UserInputMapper::InputPair(makeInput(BUTTON_2), "Right button")); + availableInputs.append(UserInputMapper::InputPair(makeInput(BUTTON_3), "Both buttons")); + + availableInputs.append(UserInputMapper::InputPair(makeInput(POSITION_AXIS_Y_NEG), "Move backward")); + availableInputs.append(UserInputMapper::InputPair(makeInput(POSITION_AXIS_Y_POS), "Move forward")); + availableInputs.append(UserInputMapper::InputPair(makeInput(POSITION_AXIS_X_POS), "Move right")); + availableInputs.append(UserInputMapper::InputPair(makeInput(POSITION_AXIS_X_NEG), "Move Left")); + availableInputs.append(UserInputMapper::InputPair(makeInput(POSITION_AXIS_Z_POS), "Move up")); + availableInputs.append(UserInputMapper::InputPair(makeInput(POSITION_AXIS_Z_NEG), "Move down")); + availableInputs.append(UserInputMapper::InputPair(makeInput(ROTATION_AXIS_Y_NEG), "Rotate backward")); + availableInputs.append(UserInputMapper::InputPair(makeInput(ROTATION_AXIS_Y_POS), "Rotate forward")); + availableInputs.append(UserInputMapper::InputPair(makeInput(ROTATION_AXIS_X_POS), "Rotate right")); + availableInputs.append(UserInputMapper::InputPair(makeInput(ROTATION_AXIS_X_NEG), "Rotate left")); + availableInputs.append(UserInputMapper::InputPair(makeInput(ROTATION_AXIS_Z_POS), "Rotate up")); + availableInputs.append(UserInputMapper::InputPair(makeInput(ROTATION_AXIS_Z_NEG), "Rotate down")); + + return availableInputs; + }; + proxy->resetDeviceBindings = [this, &mapper]() -> bool { + mapper.removeAllInputChannelsForDevice(_deviceID); + this->assignDefaultInputMapping(mapper); + return true; + }; + mapper.registerDevice(_deviceID, proxy); +} + +void ConnexionData::assignDefaultInputMapping(UserInputMapper& mapper) { + const float JOYSTICK_MOVE_SPEED = 1.0f; + //const float DPAD_MOVE_SPEED = 0.5f; + const float JOYSTICK_YAW_SPEED = 0.5f; + const float JOYSTICK_PITCH_SPEED = 0.25f; + const float BOOM_SPEED = 0.1f; + + // Y axes are flipped (up is negative) + // postion: Movement, strafing + mapper.addInputChannel(UserInputMapper::LONGITUDINAL_FORWARD, makeInput(POSITION_AXIS_Y_NEG), JOYSTICK_MOVE_SPEED); + mapper.addInputChannel(UserInputMapper::LONGITUDINAL_BACKWARD, makeInput(POSITION_AXIS_Y_POS), JOYSTICK_MOVE_SPEED); + mapper.addInputChannel(UserInputMapper::LATERAL_RIGHT, makeInput(POSITION_AXIS_X_POS), JOYSTICK_MOVE_SPEED); + mapper.addInputChannel(UserInputMapper::LATERAL_LEFT, makeInput(POSITION_AXIS_X_NEG), JOYSTICK_MOVE_SPEED); + mapper.addInputChannel(UserInputMapper::VERTICAL_UP, makeInput(POSITION_AXIS_Z_NEG), JOYSTICK_MOVE_SPEED); + mapper.addInputChannel(UserInputMapper::VERTICAL_DOWN, makeInput(POSITION_AXIS_Z_POS), JOYSTICK_MOVE_SPEED); + + // Rotation: Camera orientation with button 1 + mapper.addInputChannel(UserInputMapper::YAW_RIGHT, makeInput(ROTATION_AXIS_Z_POS), JOYSTICK_YAW_SPEED); + mapper.addInputChannel(UserInputMapper::YAW_LEFT, makeInput(ROTATION_AXIS_Z_NEG), JOYSTICK_YAW_SPEED); + mapper.addInputChannel(UserInputMapper::PITCH_DOWN, makeInput(ROTATION_AXIS_Y_NEG), JOYSTICK_PITCH_SPEED); + mapper.addInputChannel(UserInputMapper::PITCH_UP, makeInput(ROTATION_AXIS_Y_POS), JOYSTICK_PITCH_SPEED); + + // Button controls + // Zoom + mapper.addInputChannel(UserInputMapper::BOOM_IN, makeInput(BUTTON_1), BOOM_SPEED); + mapper.addInputChannel(UserInputMapper::BOOM_OUT, makeInput(BUTTON_2), BOOM_SPEED); + + // Zoom + // mapper.addInputChannel(UserInputMapper::BOOM_IN, makeInput(ROTATION_AXIS_Z_NEG), BOOM_SPEED); + // mapper.addInputChannel(UserInputMapper::BOOM_OUT, makeInput(ROTATION_AXIS_Z_POS), BOOM_SPEED); + +} + +float ConnexionData::getButton(int channel) const { + if (!_buttonPressedMap.empty()) { + if (_buttonPressedMap.find(channel) != _buttonPressedMap.end()) { + return 1.0f; + } else { + return 0.0f; + } + } + return 0.0f; +} + +float ConnexionData::getAxis(int channel) const { + auto axis = _axisStateMap.find(channel); + if (axis != _axisStateMap.end()) { + return (*axis).second; + } else { + return 0.0f; + } +} + +UserInputMapper::Input ConnexionData::makeInput(ConnexionData::ButtonChannel button) { + return UserInputMapper::Input(_deviceID, button, UserInputMapper::ChannelType::BUTTON); +} + +UserInputMapper::Input ConnexionData::makeInput(ConnexionData::PositionChannel axis) { + return UserInputMapper::Input(_deviceID, axis, UserInputMapper::ChannelType::AXIS); +} + +void ConnexionData::update() { + // the update is done in the ConnexionClient class. + // for windows in the nativeEventFilter the inputmapper is connected or registed or removed when an 3Dconnnexion device is attached or deteched + // for osx the api will call DeviceAddedHandler or DeviceRemoveHandler when a 3Dconnexion device is attached or detached +} + +ConnexionClient& ConnexionClient::getInstance() { + static ConnexionClient sharedInstance; + return sharedInstance; +} + +#ifdef HAVE_CONNEXIONCLIENT + +#ifdef _WIN32 + +static ConnexionClient* gMouseInput = 0; + +void ConnexionClient::toggleConnexion(bool shouldEnable) +{ + ConnexionData& connexiondata = ConnexionData::getInstance(); + if (shouldEnable && connexiondata.getDeviceID() == 0) { + ConnexionClient::init(); + } + if (!shouldEnable && connexiondata.getDeviceID() != 0) { + ConnexionClient::destroy(); + } + +} + +void ConnexionClient::init() { + if (Menu::getInstance()->isOptionChecked(MenuOption::Connexion)) { + ConnexionClient& cclient = ConnexionClient::getInstance(); + cclient.fLast3dmouseInputTime = 0; + + cclient.InitializeRawInput(GetActiveWindow()); + + gMouseInput = &cclient; + + QAbstractEventDispatcher::instance()->installNativeEventFilter(&cclient); + } +} + +void ConnexionClient::destroy() { + ConnexionClient& cclient = ConnexionClient::getInstance(); + QAbstractEventDispatcher::instance()->removeNativeEventFilter(&cclient); + ConnexionData& connexiondata = ConnexionData::getInstance(); + int deviceid = connexiondata.getDeviceID(); + connexiondata.setDeviceID(0); + Application::getUserInputMapper()->removeDevice(deviceid); +} + +#define LOGITECH_VENDOR_ID 0x46d + +#ifndef RIDEV_DEVNOTIFY +#define RIDEV_DEVNOTIFY 0x00002000 +#endif + +const int TRACE_RIDI_DEVICENAME = 0; +const int TRACE_RIDI_DEVICEINFO = 0; + +#ifdef _WIN64 +typedef unsigned __int64 QWORD; +#endif + +// object angular velocity per mouse tick 0.008 milliradians per second per count +static const double k3dmouseAngularVelocity = 8.0e-6; // radians per second per count + +static const int kTimeToLive = 5; + +enum ConnexionPid { + CONNEXIONPID_SPACEPILOT = 0xc625, + CONNEXIONPID_SPACENAVIGATOR = 0xc626, + CONNEXIONPID_SPACEEXPLORER = 0xc627, + CONNEXIONPID_SPACENAVIGATORFORNOTEBOOKS = 0xc628, + CONNEXIONPID_SPACEPILOTPRO = 0xc629 +}; + +// e3dmouse_virtual_key +enum V3dk { + V3DK_INVALID = 0 + , V3DK_MENU = 1, V3DK_FIT + , V3DK_TOP, V3DK_LEFT, V3DK_RIGHT, V3DK_FRONT, V3DK_BOTTOM, V3DK_BACK + , V3DK_CW, V3DK_CCW + , V3DK_ISO1, V3DK_ISO2 + , V3DK_1, V3DK_2, V3DK_3, V3DK_4, V3DK_5, V3DK_6, V3DK_7, V3DK_8, V3DK_9, V3DK_10 + , V3DK_ESC, V3DK_ALT, V3DK_SHIFT, V3DK_CTRL + , V3DK_ROTATE, V3DK_PANZOOM, V3DK_DOMINANT + , V3DK_PLUS, V3DK_MINUS +}; + +struct tag_VirtualKeys { + ConnexionPid pid; + size_t nKeys; + V3dk *vkeys; +}; + +// e3dmouse_virtual_key +static const V3dk SpaceExplorerKeys[] = { + V3DK_INVALID // there is no button 0 + , V3DK_1, V3DK_2 + , V3DK_TOP, V3DK_LEFT, V3DK_RIGHT, V3DK_FRONT + , V3DK_ESC, V3DK_ALT, V3DK_SHIFT, V3DK_CTRL + , V3DK_FIT, V3DK_MENU + , V3DK_PLUS, V3DK_MINUS + , V3DK_ROTATE +}; + +//e3dmouse_virtual_key +static const V3dk SpacePilotKeys[] = { + V3DK_INVALID + , V3DK_1, V3DK_2, V3DK_3, V3DK_4, V3DK_5, V3DK_6 + , V3DK_TOP, V3DK_LEFT, V3DK_RIGHT, V3DK_FRONT + , V3DK_ESC, V3DK_ALT, V3DK_SHIFT, V3DK_CTRL + , V3DK_FIT, V3DK_MENU + , V3DK_PLUS, V3DK_MINUS + , V3DK_DOMINANT, V3DK_ROTATE +}; + +static const struct tag_VirtualKeys _3dmouseVirtualKeys[] = { + CONNEXIONPID_SPACEPILOT + , sizeof(SpacePilotKeys) / sizeof(SpacePilotKeys[0]) + , const_cast(SpacePilotKeys), + CONNEXIONPID_SPACEEXPLORER + , sizeof(SpaceExplorerKeys) / sizeof(SpaceExplorerKeys[0]) + , const_cast(SpaceExplorerKeys) +}; + +// Converts a hid device keycode (button identifier) of a pre-2009 3Dconnexion USB device to the standard 3d mouse virtual key definition. +// pid USB Product ID (PID) of 3D mouse device +// hidKeyCode Hid keycode as retrieved from a Raw Input packet +// return The standard 3d mouse virtual key (button identifier) or zero if an error occurs. + +// Converts a hid device keycode (button identifier) of a pre-2009 3Dconnexion USB device +// to the standard 3d mouse virtual key definition. +unsigned short HidToVirtualKey(unsigned long pid, unsigned short hidKeyCode) { + unsigned short virtualkey = hidKeyCode; + for (size_t i = 0; iremoveDevice(deviceid); + } + + if (!ConnexionClient::Is3dmouseAttached()) { + return false; + } + + MSG* message = (MSG*)(msg); + + if (message->message == WM_INPUT) { + HRAWINPUT hRawInput = reinterpret_cast(message->lParam); + gMouseInput->OnRawInput(RIM_INPUT, hRawInput); + if (result != 0) { + result = 0; + } + return true; + } + return false; +} + +ConnexionClient::ConnexionClient() { + +} + +ConnexionClient::~ConnexionClient() { + ConnexionClient& cclient = ConnexionClient::getInstance(); + QAbstractEventDispatcher::instance()->removeNativeEventFilter(&cclient); +} + +// Access the mouse parameters structure +I3dMouseParam& ConnexionClient::MouseParams() { + return f3dMouseParams; +} + +// Access the mouse parameters structure +const I3dMouseParam& ConnexionClient::MouseParams() const { + return f3dMouseParams; +} + +//Called with the processed motion data when a 3D mouse event is received +void ConnexionClient::Move3d(HANDLE device, std::vector& motionData) { + Q_UNUSED(device); + ConnexionData& connexiondata = ConnexionData::getInstance(); + connexiondata.cc_position = { motionData[0] * 1000, motionData[1] * 1000, motionData[2] * 1000 }; + connexiondata.cc_rotation = { motionData[3] * 1500, motionData[4] * 1500, motionData[5] * 1500 }; + connexiondata.handleAxisEvent(); +} + +//Called when a 3D mouse key is pressed +void ConnexionClient::On3dmouseKeyDown(HANDLE device, int virtualKeyCode) { + Q_UNUSED(device); + ConnexionData& connexiondata = ConnexionData::getInstance(); + connexiondata.setButton(virtualKeyCode); +} + +//Called when a 3D mouse key is released +void ConnexionClient::On3dmouseKeyUp(HANDLE device, int virtualKeyCode) { + Q_UNUSED(device); + ConnexionData& connexiondata = ConnexionData::getInstance(); + connexiondata.setButton(0); +} + +//Get an initialized array of PRAWINPUTDEVICE for the 3D devices +//pNumDevices returns the number of devices to register. Currently this is always 1. +static PRAWINPUTDEVICE GetDevicesToRegister(unsigned int* pNumDevices) { + // Array of raw input devices to register + static RAWINPUTDEVICE sRawInputDevices[] = { + { 0x01, 0x08, 0x00, 0x00 } // Usage Page = 0x01 Generic Desktop Page, Usage Id= 0x08 Multi-axis Controller + }; + + if (pNumDevices) { + *pNumDevices = sizeof(sRawInputDevices) / sizeof(sRawInputDevices[0]); + } + + return sRawInputDevices; +} + +//Detect the 3D mouse +bool ConnexionClient::Is3dmouseAttached() { + unsigned int numDevicesOfInterest = 0; + PRAWINPUTDEVICE devicesToRegister = GetDevicesToRegister(&numDevicesOfInterest); + + unsigned int nDevices = 0; + + if (::GetRawInputDeviceList(NULL, &nDevices, sizeof(RAWINPUTDEVICELIST)) != 0) { + return false; + } + + if (nDevices == 0) { + return false; + } + + std::vector rawInputDeviceList(nDevices); + if (::GetRawInputDeviceList(&rawInputDeviceList[0], &nDevices, sizeof(RAWINPUTDEVICELIST)) == static_cast(-1)) { + return false; + } + + for (unsigned int i = 0; i < nDevices; ++i) { + RID_DEVICE_INFO rdi = { sizeof(rdi) }; + unsigned int cbSize = sizeof(rdi); + + if (GetRawInputDeviceInfo(rawInputDeviceList[i].hDevice, RIDI_DEVICEINFO, &rdi, &cbSize) > 0) { + //skip non HID and non logitec (3DConnexion) devices + if (rdi.dwType != RIM_TYPEHID || rdi.hid.dwVendorId != LOGITECH_VENDOR_ID) { + continue; + } + + //check if devices matches Multi-axis Controller + for (unsigned int j = 0; j < numDevicesOfInterest; ++j) { + if (devicesToRegister[j].usUsage == rdi.hid.usUsage + && devicesToRegister[j].usUsagePage == rdi.hid.usUsagePage) { + return true; + } + } + } + } + return false; +} + +// Initialize the window to recieve raw-input messages +// This needs to be called initially so that Windows will send the messages from the 3D mouse to the window. +bool ConnexionClient::InitializeRawInput(HWND hwndTarget) { + fWindow = hwndTarget; + + // Simply fail if there is no window + if (!hwndTarget) { + return false; + } + + unsigned int numDevices = 0; + PRAWINPUTDEVICE devicesToRegister = GetDevicesToRegister(&numDevices); + + if (numDevices == 0) { + return false; + } + + // Get OS version. + OSVERSIONINFO osvi = { sizeof(OSVERSIONINFO), 0 }; + ::GetVersionEx(&osvi); + + unsigned int cbSize = sizeof(devicesToRegister[0]); + for (size_t i = 0; i < numDevices; i++) { + // Set the target window to use + //devicesToRegister[i].hwndTarget = hwndTarget; + + // If Vista or newer, enable receiving the WM_INPUT_DEVICE_CHANGE message. + if (osvi.dwMajorVersion >= 6) { + devicesToRegister[i].dwFlags |= RIDEV_DEVNOTIFY; + } + } + return (::RegisterRawInputDevices(devicesToRegister, numDevices, cbSize) != FALSE); +} + +//Get the raw input data from Windows +UINT ConnexionClient::GetRawInputBuffer(PRAWINPUT pData, PUINT pcbSize, UINT cbSizeHeader) { + //Includes workaround for incorrect alignment of the RAWINPUT structure on x64 os + //when running as Wow64 (copied directly from 3DConnexion code) +#ifdef _WIN64 + return ::GetRawInputBuffer(pData, pcbSize, cbSizeHeader); +#else + BOOL bIsWow64 = FALSE; + ::IsWow64Process(GetCurrentProcess(), &bIsWow64); + if (!bIsWow64 || pData == NULL) { + return ::GetRawInputBuffer(pData, pcbSize, cbSizeHeader); + } else { + HWND hwndTarget = fWindow; + + size_t cbDataSize = 0; + UINT nCount = 0; + PRAWINPUT pri = pData; + + MSG msg; + while (PeekMessage(&msg, hwndTarget, WM_INPUT, WM_INPUT, PM_NOREMOVE)) { + HRAWINPUT hRawInput = reinterpret_cast(msg.lParam); + size_t cbSize = *pcbSize - cbDataSize; + if (::GetRawInputData(hRawInput, RID_INPUT, pri, &cbSize, cbSizeHeader) == static_cast(-1)) { + if (nCount == 0) { + return static_cast(-1); + } else { + break; + } + } + ++nCount; + + // Remove the message for the data just read + PeekMessage(&msg, hwndTarget, WM_INPUT, WM_INPUT, PM_REMOVE); + + pri = NEXTRAWINPUTBLOCK(pri); + cbDataSize = reinterpret_cast(pri)-reinterpret_cast(pData); + if (cbDataSize >= *pcbSize) { + cbDataSize = *pcbSize; + break; + } + } + return nCount; + } +#endif +} + +// Process the raw input device data +// On3dmouseInput() does all the preprocessing of the rawinput device data before +// finally calling the Move3d method. +void ConnexionClient::On3dmouseInput() { + // Don't do any data processing in background + bool bIsForeground = (::GetActiveWindow() != NULL); + if (!bIsForeground) { + // set all cached data to zero so that a zero event is seen and the cached data deleted + for (std::map::iterator it = fDevice2Data.begin(); it != fDevice2Data.end(); it++) { + it->second.fAxes.assign(6, .0); + it->second.fIsDirty = true; + } + } + + DWORD dwNow = ::GetTickCount(); // Current time; + DWORD dwElapsedTime; // Elapsed time since we were last here + + if (0 == fLast3dmouseInputTime) { + dwElapsedTime = 10; // System timer resolution + } else { + dwElapsedTime = dwNow - fLast3dmouseInputTime; + if (fLast3dmouseInputTime > dwNow) { + dwElapsedTime = ~dwElapsedTime + 1; + } + if (dwElapsedTime<1) { + dwElapsedTime = 1; + } else if (dwElapsedTime > 500) { + // Check for wild numbers because the device was removed while sending data + dwElapsedTime = 10; + } + } + + //qDebug("On3DmouseInput() period is %dms\n", dwElapsedTime); + + float mouseData2Rotation = k3dmouseAngularVelocity; + // v = w * r, we don't know r yet so lets assume r=1.) + float mouseData2PanZoom = k3dmouseAngularVelocity; + + // Grab the I3dmouseParam interface + I3dMouseParam& i3dmouseParam = f3dMouseParams; + // Take a look at the users preferred speed setting and adjust the sensitivity accordingly + I3dMouseSensor::Speed speedSetting = i3dmouseParam.GetSpeed(); + // See "Programming for the 3D Mouse", Section 5.1.3 + float speed = (speedSetting == I3dMouseSensor::SPEED_LOW ? 0.25f : speedSetting == I3dMouseSensor::SPEED_HIGH ? 4.f : 1.f); + + // Multiplying by the following will convert the 3d mouse data to real world units + mouseData2PanZoom *= speed; + mouseData2Rotation *= speed; + + std::map::iterator iterator = fDevice2Data.begin(); + while (iterator != fDevice2Data.end()) { + + // If we have not received data for a while send a zero event + if ((--(iterator->second.fTimeToLive)) == 0) { + iterator->second.fAxes.assign(6, .0); + } else if ( !iterator->second.fIsDirty) { //!t_bPoll3dmouse && + // If we are not polling then only handle the data that was actually received + ++iterator; + continue; + } + iterator->second.fIsDirty = false; + + // get a copy of the device + HANDLE hdevice = iterator->first; + + // get a copy of the motion vectors and apply the user filters + std::vector motionData = iterator->second.fAxes; + + // apply the user filters + + // Pan Zoom filter + // See "Programming for the 3D Mouse", Section 5.1.2 + if (!i3dmouseParam.IsPanZoom()) { + // Pan zoom is switched off so set the translation vector values to zero + motionData[0] = motionData[1] = motionData[2] = 0.; + } + + // Rotate filter + // See "Programming for the 3D Mouse", Section 5.1.1 + if (!i3dmouseParam.IsRotate()) { + // Rotate is switched off so set the rotation vector values to zero + motionData[3] = motionData[4] = motionData[5] = 0.; + } + + // convert the translation vector into physical data + for (int axis = 0; axis < 3; axis++) { + motionData[axis] *= mouseData2PanZoom; + } + + // convert the directed Rotate vector into physical data + // See "Programming for the 3D Mouse", Section 7.2.2 + for (int axis = 3; axis < 6; axis++) { + motionData[axis] *= mouseData2Rotation; + } + + // Now that the data has had the filters and sensitivty settings applied + // calculate the displacements since the last view update + for (int axis = 0; axis < 6; axis++) { + motionData[axis] *= dwElapsedTime; + } + + // Now a bit of book keeping before passing on the data + if (iterator->second.IsZero()) { + iterator = fDevice2Data.erase(iterator); + } else { + ++iterator; + } + + // Work out which will be the next device + HANDLE hNextDevice = 0; + if (iterator != fDevice2Data.end()) { + hNextDevice = iterator->first; + } + + // Pass the 3dmouse input to the view controller + Move3d(hdevice, motionData); + + // Because we don't know what happened in the previous call, the cache might have + // changed so reload the iterator + iterator = fDevice2Data.find(hNextDevice); + } + + if (!fDevice2Data.empty()) { + fLast3dmouseInputTime = dwNow; + } else { + fLast3dmouseInputTime = 0; + } +} + +//Called when new raw input data is available +void ConnexionClient::OnRawInput(UINT nInputCode, HRAWINPUT hRawInput) { + const size_t cbSizeOfBuffer = 1024; + BYTE pBuffer[cbSizeOfBuffer]; + + PRAWINPUT pRawInput = reinterpret_cast(pBuffer); + UINT cbSize = cbSizeOfBuffer; + + if (::GetRawInputData(hRawInput, RID_INPUT, pRawInput, &cbSize, sizeof(RAWINPUTHEADER)) == static_cast(-1)) { + return; + } + + bool b3dmouseInput = TranslateRawInputData(nInputCode, pRawInput); + ::DefRawInputProc(&pRawInput, 1, sizeof(RAWINPUTHEADER)); + + // Check for any buffered messages + cbSize = cbSizeOfBuffer; + UINT nCount = this->GetRawInputBuffer(pRawInput, &cbSize, sizeof(RAWINPUTHEADER)); + if (nCount == (UINT)-1) { + qDebug("GetRawInputBuffer returned error %d\n", GetLastError()); + } + + while (nCount>0 && nCount != static_cast(-1)) { + PRAWINPUT pri = pRawInput; + UINT nInput; + for (nInput = 0; nInputGetRawInputBuffer(pRawInput, &cbSize, sizeof(RAWINPUTHEADER)); + } + + // If we have mouse input data for the app then tell tha app about it + if (b3dmouseInput) { + On3dmouseInput(); + } +} + +bool ConnexionClient::TranslateRawInputData(UINT nInputCode, PRAWINPUT pRawInput) { + bool bIsForeground = (nInputCode == RIM_INPUT); + + //qDebug("Rawinput.header.dwType=0x%x\n", pRawInput->header.dwType); + + // We are not interested in keyboard or mouse data received via raw input + if (pRawInput->header.dwType != RIM_TYPEHID) { + return false; + } + + if (TRACE_RIDI_DEVICENAME == 1) { + UINT dwSize = 0; + if (::GetRawInputDeviceInfo(pRawInput->header.hDevice, RIDI_DEVICENAME, NULL, &dwSize) == 0) { + std::vector szDeviceName(dwSize + 1); + if (::GetRawInputDeviceInfo(pRawInput->header.hDevice, RIDI_DEVICENAME, &szDeviceName[0], &dwSize) >0) { + qDebug("Device Name = %s\nDevice handle = 0x%x\n", &szDeviceName[0], pRawInput->header.hDevice); + } + } + } + + RID_DEVICE_INFO sRidDeviceInfo; + sRidDeviceInfo.cbSize = sizeof(RID_DEVICE_INFO); + UINT cbSize = sizeof(RID_DEVICE_INFO); + + if (::GetRawInputDeviceInfo(pRawInput->header.hDevice, RIDI_DEVICEINFO, &sRidDeviceInfo, &cbSize) == cbSize) { + if (TRACE_RIDI_DEVICEINFO == 1) { + switch (sRidDeviceInfo.dwType) { + case RIM_TYPEMOUSE: + qDebug("\tsRidDeviceInfo.dwType=RIM_TYPEMOUSE\n"); + break; + case RIM_TYPEKEYBOARD: + qDebug("\tsRidDeviceInfo.dwType=RIM_TYPEKEYBOARD\n"); + break; + case RIM_TYPEHID: + qDebug("\tsRidDeviceInfo.dwType=RIM_TYPEHID\n"); + qDebug("\tVendor=0x%x\n\tProduct=0x%x\n\tUsagePage=0x%x\n\tUsage=0x%x\n", + sRidDeviceInfo.hid.dwVendorId, + sRidDeviceInfo.hid.dwProductId, + sRidDeviceInfo.hid.usUsagePage, + sRidDeviceInfo.hid.usUsage); + break; + } + } + + if (sRidDeviceInfo.hid.dwVendorId == LOGITECH_VENDOR_ID) { + if (pRawInput->data.hid.bRawData[0] == 0x01) { // Translation vector + TInputData& deviceData = fDevice2Data[pRawInput->header.hDevice]; + deviceData.fTimeToLive = kTimeToLive; + if (bIsForeground) { + short* pnRawData = reinterpret_cast(&pRawInput->data.hid.bRawData[1]); + // Cache the pan zoom data + deviceData.fAxes[0] = static_cast(pnRawData[0]); + deviceData.fAxes[1] = static_cast(pnRawData[1]); + deviceData.fAxes[2] = static_cast(pnRawData[2]); + + //qDebug("Pan/Zoom RI Data =\t0x%x,\t0x%x,\t0x%x\n", pnRawData[0], pnRawData[1], pnRawData[2]); + + if (pRawInput->data.hid.dwSizeHid >= 13) { // Highspeed package + // Cache the rotation data + deviceData.fAxes[3] = static_cast(pnRawData[3]); + deviceData.fAxes[4] = static_cast(pnRawData[4]); + deviceData.fAxes[5] = static_cast(pnRawData[5]); + deviceData.fIsDirty = true; + + //qDebug("Rotation RI Data =\t0x%x,\t0x%x,\t0x%x\n", pnRawData[3], pnRawData[4], pnRawData[5]); + return true; + } + } else { // Zero out the data if the app is not in forground + deviceData.fAxes.assign(6, 0.f); + } + } else if (pRawInput->data.hid.bRawData[0] == 0x02) { // Rotation vector + // If we are not in foreground do nothing + // The rotation vector was zeroed out with the translation vector in the previous message + if (bIsForeground) { + TInputData& deviceData = fDevice2Data[pRawInput->header.hDevice]; + deviceData.fTimeToLive = kTimeToLive; + + short* pnRawData = reinterpret_cast(&pRawInput->data.hid.bRawData[1]); + // Cache the rotation data + deviceData.fAxes[3] = static_cast(pnRawData[0]); + deviceData.fAxes[4] = static_cast(pnRawData[1]); + deviceData.fAxes[5] = static_cast(pnRawData[2]); + deviceData.fIsDirty = true; + + //qDebug("Rotation RI Data =\t0x%x,\t0x%x,\t0x%x\n", pnRawData[0], pnRawData[1], pnRawData[2]); + + return true; + } + } else if (pRawInput->data.hid.bRawData[0] == 0x03) { // Keystate change + // this is a package that contains 3d mouse keystate information + // bit0=key1, bit=key2 etc. + + unsigned long dwKeystate = *reinterpret_cast(&pRawInput->data.hid.bRawData[1]); + + //qDebug("ButtonData =0x%x\n", dwKeystate); + + // Log the keystate changes + unsigned long dwOldKeystate = fDevice2Keystate[pRawInput->header.hDevice]; + if (dwKeystate != 0) { + fDevice2Keystate[pRawInput->header.hDevice] = dwKeystate; + } else { + fDevice2Keystate.erase(pRawInput->header.hDevice); + } + + // Only call the keystate change handlers if the app is in foreground + if (bIsForeground) { + unsigned long dwChange = dwKeystate ^ dwOldKeystate; + + for (int nKeycode = 1; nKeycode<33; nKeycode++) { + if (dwChange & 0x01) { + int nVirtualKeyCode = HidToVirtualKey(sRidDeviceInfo.hid.dwProductId, nKeycode); + if (nVirtualKeyCode) { + if (dwKeystate & 0x01) { + On3dmouseKeyDown(pRawInput->header.hDevice, nVirtualKeyCode); + } else { + On3dmouseKeyUp(pRawInput->header.hDevice, nVirtualKeyCode); + } + } + } + dwChange >>= 1; + dwKeystate >>= 1; + } + } + } + } + } + return false; +} + +MouseParameters::MouseParameters() : fNavigation(NAVIGATION_OBJECT_MODE) + , fPivot(PIVOT_AUTO) + , fPivotVisibility(PIVOT_SHOW) + , fIsLockHorizon(true) + , fIsPanZoom(true) + , fIsRotate(true) + , fSpeed(SPEED_LOW) { +} + +MouseParameters::~MouseParameters() { +} + +bool MouseParameters::IsPanZoom() const { + return fIsPanZoom; +} + +bool MouseParameters::IsRotate() const { + return fIsRotate; +} + +MouseParameters::Speed MouseParameters::GetSpeed() const { + return fSpeed; +} + +void MouseParameters::SetPanZoom(bool isPanZoom) { + fIsPanZoom=isPanZoom; +} + +void MouseParameters::SetRotate(bool isRotate) { + fIsRotate=isRotate; +} + +void MouseParameters::SetSpeed(Speed speed) { + fSpeed=speed; +} + +MouseParameters::Navigation MouseParameters::GetNavigationMode() const { + return fNavigation; +} + +MouseParameters::Pivot MouseParameters::GetPivotMode() const { + return fPivot; +} + +MouseParameters::PivotVisibility MouseParameters::GetPivotVisibility() const { + return fPivotVisibility; +} + +bool MouseParameters::IsLockHorizon() const { + return fIsLockHorizon; +} + +void MouseParameters::SetLockHorizon(bool bOn) { + fIsLockHorizon=bOn; +} + +void MouseParameters::SetNavigationMode(Navigation navigation) { + fNavigation=navigation; +} + +void MouseParameters::SetPivotMode(Pivot pivot) { + if (fPivot!=PIVOT_MANUAL || pivot!=PIVOT_AUTO_OVERRIDE) { + fPivot = pivot; + } +} + +void MouseParameters::SetPivotVisibility(PivotVisibility visibility) { + fPivotVisibility = visibility; +} + +#else + +#define WITH_SEPARATE_THREAD false // set to true or false + +// Make the linker happy for the framework check (see link below for more info) +// http://developer.apple.com/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/WeakLinking.html + +extern int16_t SetConnexionHandlers(ConnexionMessageHandlerProc messageHandler, ConnexionAddedHandlerProc addedHandler, ConnexionRemovedHandlerProc removedHandler, bool useSeparateThread) __attribute__((weak_import)); + +int fConnexionClientID; + +static ConnexionDeviceState lastState; + +static void DeviceAddedHandler(unsigned int connection); +static void DeviceRemovedHandler(unsigned int connection); +static void MessageHandler(unsigned int connection, unsigned int messageType, void *messageArgument); + +void ConnexionClient::toggleConnexion(bool shouldEnable) +{ + if (shouldEnable && !ConnexionClient::Is3dmouseAttached()) { + ConnexionClient::init(); + } + if (!shouldEnable && ConnexionClient::Is3dmouseAttached()) { + ConnexionClient::destroy(); + } + +} + +void ConnexionClient::init() { + // Make sure the framework is installed + if (SetConnexionHandlers != NULL && Menu::getInstance()->isOptionChecked(MenuOption::Connexion)) { + // Install message handler and register our client + InstallConnexionHandlers(MessageHandler, DeviceAddedHandler, DeviceRemovedHandler); + // Either use this to take over in our application only... does not work + // fConnexionClientID = RegisterConnexionClient('MCTt', "\pConnexion Client Test", kConnexionClientModeTakeOver, kConnexionMaskAll); + + // ...or use this to take over system-wide + fConnexionClientID = RegisterConnexionClient(kConnexionClientWildcard, NULL, kConnexionClientModeTakeOver, kConnexionMaskAll); + ConnexionData& connexiondata = ConnexionData::getInstance(); + memcpy(&connexiondata.clientId, &fConnexionClientID, (long)sizeof(int)); + + // A separate API call is required to capture buttons beyond the first 8 + SetConnexionClientButtonMask(fConnexionClientID, kConnexionMaskAllButtons); + + // use default switches + ConnexionClientControl(fConnexionClientID, kConnexionCtlSetSwitches, kConnexionSwitchesDisabled, NULL); + + if (ConnexionClient::Is3dmouseAttached() && connexiondata.getDeviceID() == 0) { + connexiondata.registerToUserInputMapper(*Application::getUserInputMapper()); + connexiondata.assignDefaultInputMapping(*Application::getUserInputMapper()); + UserActivityLogger::getInstance().connectedDevice("controller", "3Dconnexion"); + } + //let one axis be dominant + //ConnexionClientControl(fConnexionClientID, kConnexionCtlSetSwitches, kConnexionSwitchDominant | kConnexionSwitchEnableAll, NULL); + } +} + +void ConnexionClient::destroy() { + // Make sure the framework is installed + if (&InstallConnexionHandlers != NULL) { + // Unregister our client and clean up all handlers + if (fConnexionClientID) { + UnregisterConnexionClient(fConnexionClientID); + } + CleanupConnexionHandlers(); + fConnexionClientID = 0; + ConnexionData& connexiondata = ConnexionData::getInstance(); + if (connexiondata.getDeviceID()!=0) { + Application::getUserInputMapper()->removeDevice(connexiondata.getDeviceID()); + connexiondata.setDeviceID(0); + } + } +} + +void DeviceAddedHandler(unsigned int connection) { + ConnexionData& connexiondata = ConnexionData::getInstance(); + if (connexiondata.getDeviceID() == 0) { + qCWarning(interfaceapp) << "3Dconnexion device added "; + connexiondata.registerToUserInputMapper(*Application::getUserInputMapper()); + connexiondata.assignDefaultInputMapping(*Application::getUserInputMapper()); + UserActivityLogger::getInstance().connectedDevice("controller", "3Dconnexion"); + } +} + +void DeviceRemovedHandler(unsigned int connection) { + ConnexionData& connexiondata = ConnexionData::getInstance(); + if (connexiondata.getDeviceID() != 0) { + qCWarning(interfaceapp) << "3Dconnexion device removed"; + Application::getUserInputMapper()->removeDevice(connexiondata.getDeviceID()); + connexiondata.setDeviceID(0); + } +} + +bool ConnexionClient::Is3dmouseAttached() { + int result; + if (fConnexionClientID) { + if (ConnexionControl(kConnexionCtlGetDeviceID, 0, &result)) { + return false; + } + return true; + } + return false; +} + +void MessageHandler(unsigned int connection, unsigned int messageType, void *messageArgument) { + ConnexionDeviceState *state; + + switch (messageType) { + case kConnexionMsgDeviceState: + state = (ConnexionDeviceState*)messageArgument; + if (state->client == fConnexionClientID) { + ConnexionData& connexiondata = ConnexionData::getInstance(); + connexiondata.cc_position = { state->axis[0], state->axis[1], state->axis[2] }; + connexiondata.cc_rotation = { state->axis[3], state->axis[4], state->axis[5] }; + + connexiondata.handleAxisEvent(); + if (state->buttons != lastState.buttons) { + connexiondata.setButton(state->buttons); + } + memmove(&lastState, state, (long)sizeof(ConnexionDeviceState)); + } + break; + case kConnexionMsgPrefsChanged: + // the prefs have changed, do something + break; + default: + // other messageTypes can happen and should be ignored + break; + } + +} + +#endif // __APPLE__ + +#endif // HAVE_CONNEXIONCLIENT diff --git a/interface/src/devices/3Dconnexion.h b/interface/src/devices/3Dconnexion.h new file mode 100755 index 0000000000..28b4924e44 --- /dev/null +++ b/interface/src/devices/3Dconnexion.h @@ -0,0 +1,244 @@ +// 3DConnexion.h +// hifi +// +// Created by Marcel Verhagen on 09-06-15. +// 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_ConnexionClient_h +#define hifi_ConnexionClient_h + +#include +#include +#include "InterfaceLogging.h" +#include "Application.h" + +#include "ui/UserInputMapper.h" + +#ifndef HAVE_CONNEXIONCLIENT +class ConnexionClient : public QObject { + Q_OBJECT +public: + static ConnexionClient& getInstance(); + static void init() {}; + static void destroy() {}; + static bool Is3dmouseAttached() { return false; }; +public slots: + void toggleConnexion(bool shouldEnable) {}; +}; +#endif // NOT_HAVE_CONNEXIONCLIENT + +#ifdef HAVE_CONNEXIONCLIENT +// the windows connexion rawinput +#ifdef _WIN32 + +#include "I3dMouseParams.h" +#include +#include +#include +#include + +// windows rawinput parameters +class MouseParameters : public I3dMouseParam { +public: + MouseParameters(); + ~MouseParameters(); + + // I3dmouseSensor interface + bool IsPanZoom() const; + bool IsRotate() const; + Speed GetSpeed() const; + + void SetPanZoom(bool isPanZoom); + void SetRotate(bool isRotate); + void SetSpeed(Speed speed); + + // I3dmouseNavigation interface + Navigation GetNavigationMode() const; + Pivot GetPivotMode() const; + PivotVisibility GetPivotVisibility() const; + bool IsLockHorizon() const; + + void SetLockHorizon(bool bOn); + void SetNavigationMode(Navigation navigation); + void SetPivotMode(Pivot pivot); + void SetPivotVisibility(PivotVisibility visibility); + + static bool Is3dmouseAttached(); + +private: + MouseParameters(const MouseParameters&); + const MouseParameters& operator = (const MouseParameters&); + + Navigation fNavigation; + Pivot fPivot; + PivotVisibility fPivotVisibility; + bool fIsLockHorizon; + + bool fIsPanZoom; + bool fIsRotate; + Speed fSpeed; +}; + +class ConnexionClient : public QObject, public QAbstractNativeEventFilter { + Q_OBJECT +public: + ConnexionClient(); + ~ConnexionClient(); + + static ConnexionClient& getInstance(); + + ConnexionClient* client; + static void init(); + static void destroy(); + + static bool Is3dmouseAttached(); + + I3dMouseParam& MouseParams(); + const I3dMouseParam& MouseParams() const; + + virtual void Move3d(HANDLE device, std::vector& motionData); + virtual void On3dmouseKeyDown(HANDLE device, int virtualKeyCode); + virtual void On3dmouseKeyUp(HANDLE device, int virtualKeyCode); + + virtual bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) Q_DECL_OVERRIDE + { + MSG* msg = static_cast< MSG * >(message); + return ConnexionClient::RawInputEventFilter(message, result); + } + +public slots: + void toggleConnexion(bool shouldEnable); + +signals: + void Move3d(std::vector& motionData); + void On3dmouseKeyDown(int virtualKeyCode); + void On3dmouseKeyUp(int virtualKeyCode); + +private: + bool InitializeRawInput(HWND hwndTarget); + + static bool RawInputEventFilter(void* msg, long* result); + + void OnRawInput(UINT nInputCode, HRAWINPUT hRawInput); + UINT GetRawInputBuffer(PRAWINPUT pData, PUINT pcbSize, UINT cbSizeHeader); + bool TranslateRawInputData(UINT nInputCode, PRAWINPUT pRawInput); + void On3dmouseInput(); + + class TInputData { + public: + TInputData() : fAxes(6) {} + + bool IsZero() { + return (0.0f == fAxes[0] && 0.0f == fAxes[1] && 0.0f == fAxes[2] && + 0.0f == fAxes[3] && 0.0f == fAxes[4] && 0.0f == fAxes[5]); + } + + int fTimeToLive; // For telling if the device was unplugged while sending data + bool fIsDirty; + std::vector fAxes; + + }; + + HWND fWindow; + + // Data cache to handle multiple rawinput devices + std::map< HANDLE, TInputData> fDevice2Data; + std::map< HANDLE, unsigned long> fDevice2Keystate; + + // 3dmouse parameters + MouseParameters f3dMouseParams; // Rotate, Pan Zoom etc. + + // use to calculate distance traveled since last event + DWORD fLast3dmouseInputTime; +}; + +// the osx connexion api +#else + +#include +#include "3DconnexionClient/ConnexionClientAPI.h" + +class ConnexionClient : public QObject { + Q_OBJECT +public: + static ConnexionClient& getInstance(); + static bool Is3dmouseAttached(); + static void init(); + static void destroy(); +public slots: + void toggleConnexion(bool shouldEnable); +}; + +#endif // __APPLE__ + +#endif // HAVE_CONNEXIONCLIENT + + +// connnects to the userinputmapper +class ConnexionData : public QObject { + Q_OBJECT + +public: + static ConnexionData& getInstance(); + ConnexionData(); + + enum PositionChannel { + POSITION_AXIS_X_POS = 1, + POSITION_AXIS_X_NEG = 2, + POSITION_AXIS_Y_POS = 3, + POSITION_AXIS_Y_NEG = 4, + POSITION_AXIS_Z_POS = 5, + POSITION_AXIS_Z_NEG = 6, + ROTATION_AXIS_X_POS = 7, + ROTATION_AXIS_X_NEG = 8, + ROTATION_AXIS_Y_POS = 9, + ROTATION_AXIS_Y_NEG = 10, + ROTATION_AXIS_Z_POS = 11, + ROTATION_AXIS_Z_NEG = 12 + }; + + enum ButtonChannel { + BUTTON_1 = 1, + BUTTON_2 = 2, + BUTTON_3 = 3 + }; + + typedef std::unordered_set ButtonPressedMap; + typedef std::map AxisStateMap; + + float getButton(int channel) const; + float getAxis(int channel) const; + + UserInputMapper::Input makeInput(ConnexionData::PositionChannel axis); + UserInputMapper::Input makeInput(ConnexionData::ButtonChannel button); + + void registerToUserInputMapper(UserInputMapper& mapper); + void assignDefaultInputMapping(UserInputMapper& mapper); + + void update(); + void focusOutEvent(); + + int getDeviceID() { return _deviceID; } + void setDeviceID(int deviceID) { _deviceID = deviceID; } + + QString _name; + + glm::vec3 cc_position; + glm::vec3 cc_rotation; + int clientId; + + void setButton(int lastButtonState); + void handleAxisEvent(); + +protected: + int _deviceID = 0; + + ButtonPressedMap _buttonPressedMap; + AxisStateMap _axisStateMap; +}; + +#endif // defined(hifi_ConnexionClient_h) \ No newline at end of file diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index f5647bd176..fdc4dbae34 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -107,6 +107,7 @@ public: CachesSize, Chat, Collisions, + Connexion, Console, ControlWithSpeech, CopyAddress, From d3c6d8b3ccd528297de8f7e517499be4171105ef Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 27 Jul 2015 10:06:34 -0700 Subject: [PATCH 095/242] move call to get VoxelPositionSize inside debug that uses it --- interface/src/Application.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 69c10fc0ee..bb564824b0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3605,9 +3605,6 @@ int Application::processOctreeStats(NLPacket& packet, SharedNodePointer sendingN _octreeSceneStatsLock.unlock(); - VoxelPositionSize rootDetails; - voxelDetailsForCode(octreeStats.getJurisdictionRoot(), rootDetails); - // see if this is the first we've heard of this node... NodeToJurisdictionMap* jurisdiction = NULL; QString serverType; @@ -3619,6 +3616,9 @@ int Application::processOctreeStats(NLPacket& packet, SharedNodePointer sendingN jurisdiction->lockForRead(); if (jurisdiction->find(nodeUUID) == jurisdiction->end()) { jurisdiction->unlock(); + + VoxelPositionSize rootDetails; + voxelDetailsForCode(octreeStats.getJurisdictionRoot(), rootDetails); qCDebug(interfaceapp, "stats from new %s server... [%f, %f, %f, %f]", qPrintable(serverType), From d54543e83cb06f25e2c4d0007d1ea9d664803693 Mon Sep 17 00:00:00 2001 From: Bing Shearer Date: Mon, 27 Jul 2015 10:19:42 -0700 Subject: [PATCH 096/242] Tidied up per Brad's request --- examples/leaves.js | 618 ++++++++++++++++++++++++--------------------- 1 file changed, 330 insertions(+), 288 deletions(-) diff --git a/examples/leaves.js b/examples/leaves.js index e611eb7de6..af3c2f0e23 100755 --- a/examples/leaves.js +++ b/examples/leaves.js @@ -1,289 +1,331 @@ -// -// Leaves.js -// examples -// -// Created by Bing Shearer on 14 Jul 2015 -// 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 -// -var leafName = "scriptLeaf"; -var leafSquall = function (properties) { - var // Properties - squallOrigin, - squallRadius, - leavesPerMinute = 60, - leafSize = { x: 0.1, y: 0.1, z: 0.1 }, - leafFallSpeed = 1, // m/s - leafLifetime = 60, // Seconds - leafSpinMax = 0, // Maximum angular velocity per axis; deg/s - debug = false, // Display origin circle; don't use running on Stack Manager - // Other - squallCircle, - SQUALL_CIRCLE_COLOR = { red: 255, green: 0, blue: 0 }, - SQUALL_CIRCLE_ALPHA = 0.5, - SQUALL_CIRCLE_ROTATION = Quat.fromPitchYawRollDegrees(90, 0, 0), - leafProperties, - leaf_MODEL_URL = "https://hifi-public.s3.amazonaws.com/ozan/support/forBing/palmLeaf.fbx", - leafTimer, - leaves = [], // HACK: Work around leaves not always getting velocities - leafVelocities = [], // HACK: Work around leaves not always getting velocities - DEGREES_TO_RADIANS = Math.PI / 180, - leafDeleteOnTearDown = true, - maxLeaves, - leafCount, - nearbyEntities, - complexMovement = false, - movementTime = 0, - maxSpinRadians = properties.leafSpinMax * DEGREES_TO_RADIANS, - windFactor, - leafDeleteOnGround = false, - floorHeight = null; - - - function processProperties() { - if (!properties.hasOwnProperty("origin")) { - print("ERROR: Leaf squall origin must be specified"); - return; - } - squallOrigin = properties.origin; - - if (!properties.hasOwnProperty("radius")) { - print("ERROR: Leaf squall radius must be specified"); - return; - } - squallRadius = properties.radius; - - if (properties.hasOwnProperty("leavesPerMinute")) { - leavesPerMinute = properties.leavesPerMinute; - } - - if (properties.hasOwnProperty("leafSize")) { - leafSize = properties.leafSize; - } - - if (properties.hasOwnProperty("leafFallSpeed")) { - leafFallSpeed = properties.leafFallSpeed; - } - - if (properties.hasOwnProperty("leafLifetime")) { - leafLifetime = properties.leafLifetime; - } - - if (properties.hasOwnProperty("leafSpinMax")) { - leafSpinMax = properties.leafSpinMax; - } - - if (properties.hasOwnProperty("debug")) { - debug = properties.debug; - } - if (properties.hasOwnProperty("floorHeight")) { - floorHeight = properties.floorHeight; - } - if (properties.hasOwnProperty("maxLeaves")) { - maxLeaves = properties.maxLeaves; - } - if (properties.hasOwnProperty("complexMovement")) { - complexMovement = properties.complexMovement; - } - if (properties.hasOwnProperty("leafDeleteOnGround")) { - leafDeleteOnGround = properties.leafDeleteOnGround; - } - if (properties.hasOwnProperty("windFactor")) { - windFactor = properties.windFactor; - } - else { - print("ERROR: Wind Factor must be defined for complex movement") - } - - leafProperties = { - type: "Model", - name: leafName, - modelURL: leaf_MODEL_URL, - lifetime: leafLifetime, - dimensions: leafSize, - velocity: { x: 0, y: -leafFallSpeed, z: 0 }, - damping: 0, - angularDamping: 0, - ignoreForCollisions: true - - }; - } - - function createleaf() { - var angle, - radius, - offset, - leaf, - spin = { x: 0, y: 0, z: 0 }, - i; - - // HACK: Work around leaves not always getting velocities set at creation - for (i = 0; i < leaves.length; i += 1) { - Entities.editEntity(leaves[i], leafVelocities[i]); - } - - angle = Math.random() * 10; - radius = Math.random() * squallRadius; - offset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, angle, 0), { x: 0, y: -0.1, z: radius }); - leafProperties.position = Vec3.sum(squallOrigin, offset); - if (properties.leafSpinMax > 0 && !complexMovement) { - spin = { - x: Math.random() * maxSpinRadians, - y: Math.random() * maxSpinRadians, - z: Math.random() * maxSpinRadians - }; - leafProperties.angularVelocity = spin; - } - else if (complexMovement) { - spin = { x: 0, y: 0, z: 0 }; - leafProperties.angularVelocity = spin - } - leaf = Entities.addEntity(leafProperties); - - // HACK: Work around leaves not always getting velocities set at creation - leaves.push(leaf); - leafVelocities.push({ - velocity: leafProperties.velocity, - angularVelocity: spin - }); - if (leaves.length > 5) { - leaves.shift(); - leafVelocities.shift(); - } - } - - function setUp() { - if (debug) { - squallCircle = Overlays.addOverlay("circle3d", { - size: { x: 2 * squallRadius, y: 2 * squallRadius }, - color: SQUALL_CIRCLE_COLOR, - alpha: SQUALL_CIRCLE_ALPHA, - solid: true, - visible: debug, - position: squallOrigin, - rotation: SQUALL_CIRCLE_ROTATION - }); - } - - leafTimer = Script.setInterval(function () { - if (leafCount <= maxLeaves - 1) { - createleaf() - } - }, 60000 / leavesPerMinute); - } - Script.setInterval(function () { - nearbyEntities = Entities.findEntities(squallOrigin, squallRadius); - newLeafMovement() - }, 100); - - function newLeafMovement() { //new additions to leaf code. Operates at 10 Hz or every 100 ms - movementTime += 0.1; - var currentLeaf, - randomRotationSpeed = { - x: maxSpinRadians * Math.sin(movementTime), - y: maxSpinRadians * Math.random(), - z: maxSpinRadians * Math.sin(movementTime / 7) - }; - for (var i = 0; i < nearbyEntities.length; i++) { - var entityProperties = Entities.getEntityProperties(nearbyEntities[i]); - var entityName = entityProperties.name; - if (leafName === entityName) { - currentLeaf = nearbyEntities[i]; - var leafHeight = entityProperties.position.y; - if (complexMovement && leafHeight > floorHeight || complexMovement && floorHeight == null) { //actual new movement code; - var leafCurrentVel = entityProperties.velocity, - leafCurrentRot = entityProperties.rotation, - yVec = { x: 0, y: 1, z: 0 }, - leafYinWFVec = Vec3.multiplyQbyV(leafCurrentRot, yVec), - leafLocalHorVec = Vec3.cross(leafYinWFVec, yVec), - leafMostDownVec = Vec3.cross(leafYinWFVec, leafLocalHorVec), - leafDesiredVel = Vec3.multiply(leafMostDownVec, windFactor), - leafVelDelt = Vec3.subtract(leafDesiredVel, leafCurrentVel), - leafNewVel = Vec3.sum(leafCurrentVel, Vec3.multiply(leafVelDelt, windFactor)); - Entities.editEntity(currentLeaf, { - angularVelocity: randomRotationSpeed, - velocity: leafNewVel - }) - } - else if (leafHeight <= floorHeight) { - if (!leafDeleteOnGround) { - Entities.editEntity(nearbyEntities[i], { - locked: false, - velocity: { x: 0, y: 0, z: 0 }, - angularVelocity: { x: 0, y: 0, z: 0 } - }) - } - else { - Entity.deleteEntity(currentLeaf); - } - } - } - } - } - - - - getLeafCount = Script.setInterval(function () { - leafCount = 0 - for (var i = 0; i < nearbyEntities.length; i++) { - var entityName = Entities.getEntityProperties(nearbyEntities[i]).name; - //Stop Leaves at floorHeight - if (leafName === entityName) { - leafCount++; - if (i == nearbyEntities.length - 1) { - //print(leafCount); - } - } - } - }, 1000) - - - - function tearDown() { - Script.clearInterval(leafTimer); - Overlays.deleteOverlay(squallCircle); - if (leafDeleteOnTearDown) { - for (var i = 0; i < nearbyEntities.length; i++) { - var entityName = Entities.getEntityProperties(nearbyEntities[i]).name; - if (leafName === entityName) { - //We have a match - delete this entity - Entities.editEntity(nearbyEntities[i], { - locked: false - }); - Entities.deleteEntity(nearbyEntities[i]); - } - } - } - } - - - - processProperties(); - setUp(); - Script.scriptEnding.connect(tearDown); - - return { - }; -}; - -var leafSquall1 = new leafSquall({ - origin: { x: 3071.5, y: 2170, z: 6765.3 }, - radius: 100, - leavesPerMinute: 30, - leafSize: { x: 0.3, y: 0.00, z: 0.3}, - leafFallSpeed: 0.4, - leafLifetime: 100, - leafSpinMax: 30, - debug: false, - maxLeaves: 100, - leafDeleteOnTearDown: true, - complexMovement: true, - floorHeight: 2143.5, - windFactor: 0.5, - leafDeleteOnGround: false -}); - -// todo +// +// Leaves.js +// examples +// +// Created by Bing Shearer on 14 Jul 2015 +// 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 +// +var leafName = "scriptLeaf"; +var leafSquall = function (properties) { + var // Properties + squallOrigin, + squallRadius, + leavesPerMinute = 60, + leafSize = { + x: 0.1, + y: 0.1, + z: 0.1 + }, + leafFallSpeed = 1, // m/s + leafLifetime = 60, // Seconds + leafSpinMax = 0, // Maximum angular velocity per axis; deg/s + debug = false, // Display origin circle; don't use running on Stack Manager + // Other + squallCircle, + SQUALL_CIRCLE_COLOR = { + red: 255, + green: 0, + blue: 0 + }, + SQUALL_CIRCLE_ALPHA = 0.5, + SQUALL_CIRCLE_ROTATION = Quat.fromPitchYawRollDegrees(90, 0, 0), + leafProperties, + leaf_MODEL_URL = "https://hifi-public.s3.amazonaws.com/ozan/support/forBing/palmLeaf.fbx", + leafTimer, + leaves = [], // HACK: Work around leaves not always getting velocities + leafVelocities = [], // HACK: Work around leaves not always getting velocities + DEGREES_TO_RADIANS = Math.PI / 180, + leafDeleteOnTearDown = true, + maxLeaves, + leafCount, + nearbyEntities, + complexMovement = false, + movementTime = 0, + maxSpinRadians = properties.leafSpinMax * DEGREES_TO_RADIANS, + windFactor, + leafDeleteOnGround = false, + floorHeight = null; + + + function processProperties() { + if (!properties.hasOwnProperty("origin")) { + print("ERROR: Leaf squall origin must be specified"); + return; + } + squallOrigin = properties.origin; + + if (!properties.hasOwnProperty("radius")) { + print("ERROR: Leaf squall radius must be specified"); + return; + } + squallRadius = properties.radius; + + if (properties.hasOwnProperty("leavesPerMinute")) { + leavesPerMinute = properties.leavesPerMinute; + } + + if (properties.hasOwnProperty("leafSize")) { + leafSize = properties.leafSize; + } + + if (properties.hasOwnProperty("leafFallSpeed")) { + leafFallSpeed = properties.leafFallSpeed; + } + + if (properties.hasOwnProperty("leafLifetime")) { + leafLifetime = properties.leafLifetime; + } + + if (properties.hasOwnProperty("leafSpinMax")) { + leafSpinMax = properties.leafSpinMax; + } + + if (properties.hasOwnProperty("debug")) { + debug = properties.debug; + } + if (properties.hasOwnProperty("floorHeight")) { + floorHeight = properties.floorHeight; + } + if (properties.hasOwnProperty("maxLeaves")) { + maxLeaves = properties.maxLeaves; + } + if (properties.hasOwnProperty("complexMovement")) { + complexMovement = properties.complexMovement; + } + if (properties.hasOwnProperty("leafDeleteOnGround")) { + leafDeleteOnGround = properties.leafDeleteOnGround; + } + if (properties.hasOwnProperty("windFactor")) { + windFactor = properties.windFactor; + } else { + print("ERROR: Wind Factor must be defined for complex movement") + } + + leafProperties = { + type: "Model", + name: leafName, + modelURL: leaf_MODEL_URL, + lifetime: leafLifetime, + dimensions: leafSize, + velocity: { + x: 0, + y: -leafFallSpeed, + z: 0 + }, + damping: 0, + angularDamping: 0, + ignoreForCollisions: true + + }; + } + + function createleaf() { + var angle, + radius, + offset, + leaf, + spin = { + x: 0, + y: 0, + z: 0 + }, + i; + + // HACK: Work around leaves not always getting velocities set at creation + for (i = 0; i < leaves.length; i++) { + Entities.editEntity(leaves[i], leafVelocities[i]); + } + + angle = Math.random() * leafSpinMax; + radius = Math.random() * squallRadius; + offset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, angle, 0), { + x: 0, + y: -0.1, + z: radius + }); + leafProperties.position = Vec3.sum(squallOrigin, offset); + if (properties.leafSpinMax > 0 && !complexMovement) { + spin = { + x: Math.random() * maxSpinRadians, + y: Math.random() * maxSpinRadians, + z: Math.random() * maxSpinRadians + }; + leafProperties.angularVelocity = spin; + } else if (complexMovement) { + spin = { + x: 0, + y: 0, + z: 0 + }; + leafProperties.angularVelocity = spin + } + leaf = Entities.addEntity(leafProperties); + + // HACK: Work around leaves not always getting velocities set at creation + leaves.push(leaf); + leafVelocities.push({ + velocity: leafProperties.velocity, + angularVelocity: spin + }); + if (leaves.length > 5) { + leaves.shift(); + leafVelocities.shift(); + } + } + + function setUp() { + if (debug) { + squallCircle = Overlays.addOverlay("circle3d", { + size: { + x: 2 * squallRadius, + y: 2 * squallRadius + }, + color: SQUALL_CIRCLE_COLOR, + alpha: SQUALL_CIRCLE_ALPHA, + solid: true, + visible: debug, + position: squallOrigin, + rotation: SQUALL_CIRCLE_ROTATION + }); + } + + leafTimer = Script.setInterval(function () { + if (leafCount <= maxLeaves - 1) { + createleaf() + } + }, 60000 / leavesPerMinute); + } + Script.setInterval(function () { + nearbyEntities = Entities.findEntities(squallOrigin, squallRadius); + newLeafMovement() + }, 100); + + function newLeafMovement() { //new additions to leaf code. Operates at 10 Hz or every 100 ms + movementTime += 0.1; + var currentLeaf, + randomRotationSpeed = { + x: maxSpinRadians * Math.sin(movementTime), + y: maxSpinRadians * Math.random(), + z: maxSpinRadians * Math.sin(movementTime / 7) + }; + for (var i = 0; i < nearbyEntities.length; i++) { + var entityProperties = Entities.getEntityProperties(nearbyEntities[i]); + var entityName = entityProperties.name; + if (leafName === entityName) { + currentLeaf = nearbyEntities[i]; + var leafHeight = entityProperties.position.y; + if (complexMovement && leafHeight > floorHeight || complexMovement && floorHeight == null) { //actual new movement code; + var leafCurrentVel = entityProperties.velocity, + leafCurrentRot = entityProperties.rotation, + yVec = { + x: 0, + y: 1, + z: 0 + }, + leafYinWFVec = Vec3.multiplyQbyV(leafCurrentRot, yVec), + leafLocalHorVec = Vec3.cross(leafYinWFVec, yVec), + leafMostDownVec = Vec3.cross(leafYinWFVec, leafLocalHorVec), + leafDesiredVel = Vec3.multiply(leafMostDownVec, windFactor), + leafVelDelt = Vec3.subtract(leafDesiredVel, leafCurrentVel), + leafNewVel = Vec3.sum(leafCurrentVel, Vec3.multiply(leafVelDelt, windFactor)); + Entities.editEntity(currentLeaf, { + angularVelocity: randomRotationSpeed, + velocity: leafNewVel + }) + } else if (leafHeight <= floorHeight) { + if (!leafDeleteOnGround) { + Entities.editEntity(nearbyEntities[i], { + locked: false, + velocity: { + x: 0, + y: 0, + z: 0 + }, + angularVelocity: { + x: 0, + y: 0, + z: 0 + } + }) + } else { + Entity.deleteEntity(currentLeaf); + } + } + } + } + } + + + + getLeafCount = Script.setInterval(function () { + leafCount = 0 + for (var i = 0; i < nearbyEntities.length; i++) { + var entityName = Entities.getEntityProperties(nearbyEntities[i]).name; + //Stop Leaves at floorHeight + if (leafName === entityName) { + leafCount++; + if (i == nearbyEntities.length - 1) { + //print(leafCount); + } + } + } + }, 1000) + + + + function tearDown() { + Script.clearInterval(leafTimer); + Overlays.deleteOverlay(squallCircle); + if (leafDeleteOnTearDown) { + for (var i = 0; i < nearbyEntities.length; i++) { + var entityName = Entities.getEntityProperties(nearbyEntities[i]).name; + if (leafName === entityName) { + //We have a match - delete this entity + Entities.editEntity(nearbyEntities[i], { + locked: false + }); + Entities.deleteEntity(nearbyEntities[i]); + } + } + } + } + + + + processProperties(); + setUp(); + Script.scriptEnding.connect(tearDown); + + return {}; +}; + +var leafSquall1 = new leafSquall({ + origin: { + x: 3071.5, + y: 2170, + z: 6765.3 + }, + radius: 100, + leavesPerMinute: 30, + leafSize: { + x: 0.3, + y: 0.00, + z: 0.3 + }, + leafFallSpeed: 0.4, + leafLifetime: 100, + leafSpinMax: 30, + debug: false, + maxLeaves: 100, + leafDeleteOnTearDown: true, + complexMovement: true, + floorHeight: 2143.5, + windFactor: 0.5, + leafDeleteOnGround: false +}); + +// todo //deal with depth issue \ No newline at end of file From 73ac941230434fd4a14503f80b5c93fc0328b0da Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 27 Jul 2015 11:15:33 -0700 Subject: [PATCH 097/242] remove unused legacy Shapes from Model and friends --- interface/src/avatar/Avatar.cpp | 37 +---- interface/src/avatar/Avatar.h | 20 --- interface/src/avatar/MyAvatar.cpp | 2 - interface/src/avatar/SkeletonModel.cpp | 8 +- libraries/render-utils/src/Model.cpp | 24 ---- libraries/render-utils/src/Model.h | 6 - libraries/render-utils/src/PhysicsEntity.cpp | 139 +------------------ libraries/render-utils/src/PhysicsEntity.h | 22 --- 8 files changed, 8 insertions(+), 250 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 92fe39687d..ea9e80c132 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -431,15 +431,17 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { } } + /* + // TODO: re-implement these when we have more detailed avatar collision shapes bool renderSkeleton = Menu::getInstance()->isOptionChecked(MenuOption::RenderSkeletonCollisionShapes); - bool renderHead = Menu::getInstance()->isOptionChecked(MenuOption::RenderHeadCollisionShapes); - bool renderBounding = Menu::getInstance()->isOptionChecked(MenuOption::RenderBoundingCollisionShapes); if (renderSkeleton) { - _skeletonModel.renderJointCollisionShapes(0.7f); } + bool renderHead = Menu::getInstance()->isOptionChecked(MenuOption::RenderHeadCollisionShapes); if (renderHead && shouldRenderHead(renderArgs)) { - getHead()->getFaceModel().renderJointCollisionShapes(0.7f); } + */ + + bool renderBounding = Menu::getInstance()->isOptionChecked(MenuOption::RenderBoundingCollisionShapes); if (renderBounding && shouldRenderHead(renderArgs)) { _skeletonModel.renderBoundingCollisionShapes(*renderArgs->_batch, 0.7f); } @@ -794,33 +796,6 @@ void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& frustum, co renderer->draw(batch, text_x, -text_y, nameUTF8.data(), textColor); } -bool Avatar::findRayIntersection(RayIntersectionInfo& intersection) const { - bool hit = _skeletonModel.findRayIntersection(intersection); - hit = getHead()->getFaceModel().findRayIntersection(intersection) || hit; - return hit; -} - -bool Avatar::findSphereCollisions(const glm::vec3& penetratorCenter, float penetratorRadius, CollisionList& collisions) { - return _skeletonModel.findSphereCollisions(penetratorCenter, penetratorRadius, collisions); - // TODO: Andrew to fix: Temporarily disabling collisions against the head - //return getHead()->getFaceModel().findSphereCollisions(penetratorCenter, penetratorRadius, collisions); -} - -bool Avatar::findPlaneCollisions(const glm::vec4& plane, CollisionList& collisions) { - return _skeletonModel.findPlaneCollisions(plane, collisions) || - getHead()->getFaceModel().findPlaneCollisions(plane, collisions); -} - -bool Avatar::findCollisions(const QVector& shapes, CollisionList& collisions) { - // TODO: Andrew to fix: also collide against _skeleton - //bool collided = _skeletonModel.findCollisions(shapes, collisions); - - Model& headModel = getHead()->getFaceModel(); - //collided = headModel.findCollisions(shapes, collisions) || collided; - bool collided = headModel.findCollisions(shapes, collisions); - return collided; -} - void Avatar::setSkeletonOffset(const glm::vec3& offset) { const float MAX_OFFSET_LENGTH = _scale * 0.5f; float offsetLength = glm::length(offset); diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index a587e69642..dcf37c9a1e 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -110,26 +110,6 @@ public: /// Returns the distance to use as a LOD parameter. float getLODDistance() const; - bool findRayIntersection(RayIntersectionInfo& intersection) const; - - /// \param shapes list of shapes to collide against avatar - /// \param collisions list to store collision results - /// \return true if at least one shape collided with avatar - bool findCollisions(const QVector& shapes, CollisionList& collisions); - - /// Checks for penetration between the a sphere and the avatar's models. - /// \param penetratorCenter the center of the penetration test sphere - /// \param penetratorRadius the radius of the penetration test sphere - /// \param collisions[out] a list to which collisions get appended - /// \return whether or not the sphere penetrated - bool findSphereCollisions(const glm::vec3& penetratorCenter, float penetratorRadius, CollisionList& collisions); - - /// Checks for penetration between the described plane and the avatar. - /// \param plane the penetration plane - /// \param collisions[out] a list to which collisions get appended - /// \return whether or not the plane penetrated - bool findPlaneCollisions(const glm::vec4& plane, CollisionList& collisions); - virtual bool isMyAvatar() const { return false; } virtual QVector getJointRotations() const; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 0dceb79402..66c59eb35d 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -110,8 +110,6 @@ MyAvatar::MyAvatar(RigPointer rig) : _driveKeys[i] = 0.0f; } - _skeletonModel.setEnableShapes(true); - // connect to AddressManager signal for location jumps connect(DependencyManager::get().data(), &AddressManager::locationChangeRequired, this, &MyAvatar::goToLocation); diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 08960c913c..19dc2397c5 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -12,9 +12,7 @@ #include #include -#include #include -#include #include "Application.h" #include "Avatar.h" @@ -44,7 +42,6 @@ SkeletonModel::SkeletonModel(Avatar* owningAvatar, QObject* parent, RigPointer r { assert(_rig); assert(_owningAvatar); - _enableShapes = true; } SkeletonModel::~SkeletonModel() { @@ -81,10 +78,7 @@ void SkeletonModel::initJointStates(QVector states) { _rig->updateJointState(i, parentTransform); } - clearShapes(); - if (_enableShapes) { - buildShapes(); - } + buildShapes(); Extents meshExtents = getMeshExtents(); _headClipDistance = -(meshExtents.minimum.z / _scale.z - _defaultEyeModelPosition.z); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index a316da0f99..c28dbf4247 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -23,8 +23,6 @@ #include #include #include "PhysicsEntity.h" -#include -#include #include #include "AbstractViewStateInterface.h" @@ -223,10 +221,6 @@ void Model::setScaleInternal(const glm::vec3& scale) { if (relativeDeltaScale > ONE_PERCENT || scaleLength < EPSILON) { _scale = scale; initJointTransforms(); - if (_shapes.size() > 0) { - clearShapes(); - buildShapes(); - } } } @@ -1169,15 +1163,6 @@ QStringList Model::getJointNames() const { return isActive() ? _geometry->getFBXGeometry().getJointNames() : QStringList(); } -// virtual override from PhysicsEntity -void Model::buildShapes() { - // TODO: figure out how to load/build collision shapes for general models -} - -void Model::updateShapePositions() { - // TODO: implement this when we know how to build shapes for regular Models -} - class Blender : public QRunnable { public: @@ -1348,8 +1333,6 @@ void Model::simulateInternal(float deltaTime) { glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; _rig->simulateInternal(deltaTime, parentTransform, getTranslation(), getRotation()); - _shapesAreDirty = !_shapes.isEmpty(); - glm::mat4 modelToWorld = glm::mat4_cast(_rotation); for (int i = 0; i < _meshStates.size(); i++) { MeshState& state = _meshStates[i]; @@ -1389,7 +1372,6 @@ bool Model::setJointPosition(int jointIndex, const glm::vec3& position, const gl glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; if (_rig->setJointPosition(jointIndex, position, rotation, useRotation, lastFreeIndex, allIntermediatesFree, alignment, priority, freeLineage, parentTransform)) { - _shapesAreDirty = !_shapes.isEmpty(); return true; } return false; @@ -1400,7 +1382,6 @@ void Model::inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm: const QVector& freeLineage = geometry.joints.at(endIndex).freeLineage; glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; _rig->inverseKinematics(endIndex, targetPosition, targetRotation, priority, freeLineage, parentTransform); - _shapesAreDirty = !_shapes.isEmpty(); } bool Model::restoreJointPosition(int jointIndex, float fraction, float priority) { @@ -1415,10 +1396,6 @@ float Model::getLimbLength(int jointIndex) const { return _rig->getLimbLength(jointIndex, freeLineage, _scale, geometry.joints); } -void Model::renderJointCollisionShapes(float alpha) { - // implement this when we have shapes for regular models -} - bool Model::maybeStartBlender() { const FBXGeometry& fbxGeometry = _geometry->getFBXGeometry(); if (fbxGeometry.hasBlendedMeshes()) { @@ -1484,7 +1461,6 @@ void Model::deleteGeometry() { _blendedVertexBuffers.clear(); _rig->clearJointStates(); _meshStates.clear(); - clearShapes(); _rig->deleteAnimations(); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 45d7ce63ab..66f3c63f29 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -39,7 +39,6 @@ class AbstractViewStateInterface; class QScriptEngine; -class Shape; #include "RenderArgs.h" class ViewFrustum; @@ -90,7 +89,6 @@ public: void removeFromScene(std::shared_ptr scene, render::PendingChanges& pendingChanges); void renderSetup(RenderArgs* args); bool isRenderable() const { return !_meshStates.isEmpty() || (isActive() && _geometry->getMeshes().isEmpty()); } - virtual void renderJointCollisionShapes(float alpha); bool isVisible() const { return _isVisible; } @@ -234,10 +232,6 @@ protected: /// \return true if joint exists bool getJointPosition(int jointIndex, glm::vec3& position) const; - // virtual overrides from PhysicsEntity - virtual void buildShapes(); - virtual void updateShapePositions(); - void setShowTrueJointTransforms(bool show) { _showTrueJointTransforms = show; } QSharedPointer _geometry; diff --git a/libraries/render-utils/src/PhysicsEntity.cpp b/libraries/render-utils/src/PhysicsEntity.cpp index 155b2fe4e0..5d58d87e84 100644 --- a/libraries/render-utils/src/PhysicsEntity.cpp +++ b/libraries/render-utils/src/PhysicsEntity.cpp @@ -11,17 +11,10 @@ #include "PhysicsEntity.h" -#include "PlaneShape.h" -#include "Shape.h" -#include "ShapeCollider.h" -#include "SphereShape.h" - PhysicsEntity::PhysicsEntity() : _translation(0.0f), _rotation(), - _boundingRadius(0.0f), - _shapesAreDirty(true), - _enableShapes(false) { + _boundingRadius(0.0f) { } PhysicsEntity::~PhysicsEntity() { @@ -29,143 +22,13 @@ PhysicsEntity::~PhysicsEntity() { void PhysicsEntity::setTranslation(const glm::vec3& translation) { if (_translation != translation) { - _shapesAreDirty = !_shapes.isEmpty(); _translation = translation; } } void PhysicsEntity::setRotation(const glm::quat& rotation) { if (_rotation != rotation) { - _shapesAreDirty = !_shapes.isEmpty(); _rotation = rotation; } } -void PhysicsEntity::setShapeBackPointers() { - for (int i = 0; i < _shapes.size(); i++) { - Shape* shape = _shapes[i]; - if (shape) { - shape->setEntity(this); - } - } -} - -void PhysicsEntity::setEnableShapes(bool enable) { - if (enable != _enableShapes) { - clearShapes(); - _enableShapes = enable; - if (_enableShapes) { - buildShapes(); - } - } -} - -void PhysicsEntity::clearShapes() { - for (int i = 0; i < _shapes.size(); ++i) { - delete _shapes[i]; - } - _shapes.clear(); -} - -bool PhysicsEntity::findRayIntersection(RayIntersectionInfo& intersection) const { - return ShapeCollider::findRayIntersection(_shapes, intersection); -} - -bool PhysicsEntity::findCollisions(const QVector shapes, CollisionList& collisions) { - bool collided = false; - int numTheirShapes = shapes.size(); - for (int i = 0; i < numTheirShapes; ++i) { - const Shape* theirShape = shapes[i]; - if (!theirShape) { - continue; - } - int numOurShapes = _shapes.size(); - for (int j = 0; j < numOurShapes; ++j) { - const Shape* ourShape = _shapes.at(j); - if (ourShape && ShapeCollider::collideShapes(theirShape, ourShape, collisions)) { - collided = true; - } - } - } - return collided; -} - -bool PhysicsEntity::findSphereCollisions(const glm::vec3& sphereCenter, float sphereRadius, CollisionList& collisions) { - bool collided = false; - SphereShape sphere(sphereRadius, sphereCenter); - for (int i = 0; i < _shapes.size(); i++) { - Shape* shape = _shapes[i]; - if (!shape) { - continue; - } - if (ShapeCollider::collideShapes(&sphere, shape, collisions)) { - CollisionInfo* collision = collisions.getLastCollision(); - collision->_data = (void*)(this); - collision->_intData = i; - collided = true; - } - } - return collided; -} - -bool PhysicsEntity::findPlaneCollisions(const glm::vec4& plane, CollisionList& collisions) { - bool collided = false; - PlaneShape planeShape(plane); - for (int i = 0; i < _shapes.size(); i++) { - if (_shapes.at(i) && ShapeCollider::collideShapes(&planeShape, _shapes.at(i), collisions)) { - CollisionInfo* collision = collisions.getLastCollision(); - collision->_data = (void*)(this); - collision->_intData = i; - collided = true; - } - } - return collided; -} - -// ----------------------------------------------------------- -// TODO: enforce this maximum when shapes are actually built. The gotcha here is -// that the Model class (derived from PhysicsEntity) expects numShapes == numJoints, -// so we have to modify that code to be safe. -const int MAX_SHAPES_PER_ENTITY = 256; - -// the first 256 prime numbers -const int primes[256] = { - 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, - 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, - 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, - 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, - 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, - 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, - 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, - 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, - 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, - 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, - 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, - 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, - 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, - 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, - 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, - 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, - 947, 953, 967, 971, 977, 983, 991, 997, 1009, 1013, - 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069, - 1087, 1091, 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151, - 1153, 1163, 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, - 1229, 1231, 1237, 1249, 1259, 1277, 1279, 1283, 1289, 1291, - 1297, 1301, 1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373, - 1381, 1399, 1409, 1423, 1427, 1429, 1433, 1439, 1447, 1451, - 1453, 1459, 1471, 1481, 1483, 1487, 1489, 1493, 1499, 1511, - 1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579, 1583, - 1597, 1601, 1607, 1609, 1613, 1619 }; - -void PhysicsEntity::disableCollisions(int shapeIndexA, int shapeIndexB) { - if (shapeIndexA < MAX_SHAPES_PER_ENTITY && shapeIndexB < MAX_SHAPES_PER_ENTITY) { - _disabledCollisions.insert(primes[shapeIndexA] * primes[shapeIndexB]); - } -} - -bool PhysicsEntity::collisionsAreEnabled(int shapeIndexA, int shapeIndexB) const { - if (shapeIndexA < MAX_SHAPES_PER_ENTITY && shapeIndexB < MAX_SHAPES_PER_ENTITY) { - return !_disabledCollisions.contains(primes[shapeIndexA] * primes[shapeIndexB]); - } - return false; -} diff --git a/libraries/render-utils/src/PhysicsEntity.h b/libraries/render-utils/src/PhysicsEntity.h index f01f1d10a6..3b527c7827 100644 --- a/libraries/render-utils/src/PhysicsEntity.h +++ b/libraries/render-utils/src/PhysicsEntity.h @@ -21,8 +21,6 @@ #include #include -class Shape; - class PhysicsEntity { public: @@ -38,30 +36,10 @@ public: const glm::quat& getRotation() const { return _rotation; } float getBoundingRadius() const { return _boundingRadius; } - void setShapeBackPointers(); - - void setEnableShapes(bool enable); - - virtual void buildShapes() = 0; - virtual void clearShapes(); - const QVector getShapes() const { return _shapes; } - - bool findRayIntersection(RayIntersectionInfo& intersection) const; - bool findCollisions(const QVector shapes, CollisionList& collisions); - bool findSphereCollisions(const glm::vec3& sphereCenter, float sphereRadius, CollisionList& collisions); - bool findPlaneCollisions(const glm::vec4& plane, CollisionList& collisions); - - void disableCollisions(int shapeIndexA, int shapeIndexB); - bool collisionsAreEnabled(int shapeIndexA, int shapeIndexB) const; - protected: glm::vec3 _translation; glm::quat _rotation; float _boundingRadius; - bool _shapesAreDirty; - bool _enableShapes; - QVector _shapes; - QSet _disabledCollisions; }; #endif // hifi_PhysicsEntity_h From ceffbb6383fae7df2d722a24c39622c87169ee23 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 27 Jul 2015 11:41:53 -0700 Subject: [PATCH 098/242] Update edit.js to show an alert window when new objects would be out of bounds --- examples/edit.js | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/examples/edit.js b/examples/edit.js index 1af016958f..ec3106e585 100644 --- a/examples/edit.js +++ b/examples/edit.js @@ -329,7 +329,7 @@ var toolBar = (function () { Script.setTimeout(resize, RESIZE_INTERVAL); } else { - print("Can't add model: Model would be out of bounds."); + Window.alert("Can't add model: Model would be out of bounds."); } } @@ -374,7 +374,7 @@ var toolBar = (function () { }); } else { - print("Can't create box: Box would be out of bounds."); + Window.alert("Can't create box: Box would be out of bounds."); } return true; } @@ -390,7 +390,7 @@ var toolBar = (function () { color: { red: 255, green: 0, blue: 0 } }); } else { - print("Can't create sphere: Sphere would be out of bounds."); + Window.alert("Can't create sphere: Sphere would be out of bounds."); } return true; } @@ -413,7 +413,7 @@ var toolBar = (function () { cutoff: 180, // in degrees }); } else { - print("Can't create Light: Light would be out of bounds."); + Window.alert("Can't create Light: Light would be out of bounds."); } return true; } @@ -433,7 +433,7 @@ var toolBar = (function () { lineHeight: 0.06 }); } else { - print("Can't create box: Text would be out of bounds."); + Window.alert("Can't create box: Text would be out of bounds."); } return true; } @@ -449,7 +449,7 @@ var toolBar = (function () { sourceUrl: "https://highfidelity.com/", }); } else { - print("Can't create Web Entity: would be out of bounds."); + Window.alert("Can't create Web Entity: would be out of bounds."); } return true; } @@ -464,7 +464,7 @@ var toolBar = (function () { dimensions: { x: 10, y: 10, z: 10 }, }); } else { - print("Can't create box: Text would be out of bounds."); + Window.alert("Can't create box: Text would be out of bounds."); } return true; } @@ -482,7 +482,7 @@ var toolBar = (function () { voxelSurfaceStyle: 1 }); } else { - print("Can't create PolyVox: would be out of bounds."); + Window.alert("Can't create PolyVox: would be out of bounds."); } return true; } @@ -1068,13 +1068,16 @@ function importSVO(importURL) { if (Clipboard.getClipboardContentsLargestDimension() < VERY_LARGE) { position = getPositionToCreateEntity(); } - var pastedEntityIDs = Clipboard.pasteEntities(position); - - if (isActive) { - selectionManager.setSelections(pastedEntityIDs); - } + if (position.x > 0 && position.y > 0 && position.z > 0) { + var pastedEntityIDs = Clipboard.pasteEntities(position); + if (isActive) { + selectionManager.setSelections(pastedEntityIDs); + } Window.raiseMainWindow(); + } else { + Window.alert("Can't import objects: objects would be out of bounds."); + } } else { Window.alert("There was an error importing the entity file."); } From 92595583ec280e81a3e9a5783a696bfce2b17063 Mon Sep 17 00:00:00 2001 From: Niraj Venkat Date: Mon, 27 Jul 2015 11:55:17 -0700 Subject: [PATCH 099/242] Coding standard and building --- libraries/gpu/src/gpu/GLBackendTexture.cpp | 32 ------------------- .../render-utils/src/ambient_occlusion.slf | 4 +-- .../render-utils/src/occlusion_blend.slf | 7 ++-- 3 files changed, 5 insertions(+), 38 deletions(-) diff --git a/libraries/gpu/src/gpu/GLBackendTexture.cpp b/libraries/gpu/src/gpu/GLBackendTexture.cpp index 2696d17596..72c7de8504 100755 --- a/libraries/gpu/src/gpu/GLBackendTexture.cpp +++ b/libraries/gpu/src/gpu/GLBackendTexture.cpp @@ -144,38 +144,6 @@ public: case gpu::RGB: case gpu::RGBA: texel.internalFormat = GL_RED; - /* switch (dstFormat.getType()) { - case gpu::UINT32: - case gpu::INT32: - case gpu::NUINT32: - case gpu::NINT32: { - texel.internalFormat = GL_DEPTH_COMPONENT32; - break; - } - case gpu::NFLOAT: - case gpu::FLOAT: { - texel.internalFormat = GL_DEPTH_COMPONENT32F; - break; - } - case gpu::UINT16: - case gpu::INT16: - case gpu::NUINT16: - case gpu::NINT16: - case gpu::HALF: - case gpu::NHALF: { - texel.internalFormat = GL_DEPTH_COMPONENT16; - break; - } - case gpu::UINT8: - case gpu::INT8: - case gpu::NUINT8: - case gpu::NINT8: { - texel.internalFormat = GL_DEPTH_COMPONENT24; - break; - } - case gpu::NUM_TYPES: { // quiet compiler - Q_UNREACHABLE(); - }*/ break; case gpu::DEPTH: texel.format = GL_DEPTH_COMPONENT; // It's depth component to load it diff --git a/libraries/render-utils/src/ambient_occlusion.slf b/libraries/render-utils/src/ambient_occlusion.slf index 023fe2552c..7a80dd559e 100644 --- a/libraries/render-utils/src/ambient_occlusion.slf +++ b/libraries/render-utils/src/ambient_occlusion.slf @@ -45,7 +45,7 @@ void main(void) { TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); - vec3 eyeDir = vec3(0.0); + vec3 eyeDir = vec3(0.0, 0.0, -3.0); vec3 cameraPositionWorldSpace; <$transformEyeToWorldDir(cam, eyeDir, cameraPositionWorldSpace)$> @@ -104,6 +104,6 @@ void main(void) { occlusion = 1.0 - occlusion / float(SAMPLE_COUNT); occlusion = clamp(pow(occlusion, g_intensity), 0.0, 1.0); - gl_FragColor = vec4(occlusion, occlusion, occlusion, 1.0); + gl_FragColor = vec4(vec3(occlusion), 1.0); } diff --git a/libraries/render-utils/src/occlusion_blend.slf b/libraries/render-utils/src/occlusion_blend.slf index 6c0101eb6f..965d806759 100644 --- a/libraries/render-utils/src/occlusion_blend.slf +++ b/libraries/render-utils/src/occlusion_blend.slf @@ -22,9 +22,8 @@ void main(void) { vec4 occlusionColor = texture2D(blurredOcclusionTexture, varTexcoord); if(occlusionColor.r > 0.8 && occlusionColor.r <= 1.0) { - gl_FragColor = vec4(vec3(0.0), 1.0); - } - else { - gl_FragColor = vec4(vec3(0.2), 1.0); + gl_FragColor = vec4(vec3(0.0), 0.0); + } else { + gl_FragColor = vec4(vec3(occlusionColor.r), 1.0); } } From c46e73434c3819d3f2b043be95af123c498d5bd6 Mon Sep 17 00:00:00 2001 From: Bing Shearer Date: Mon, 27 Jul 2015 11:59:09 -0700 Subject: [PATCH 100/242] Added small fix to error message code --- examples/leaves.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/leaves.js b/examples/leaves.js index af3c2f0e23..4610cd2ef0 100755 --- a/examples/leaves.js +++ b/examples/leaves.js @@ -100,7 +100,7 @@ var leafSquall = function (properties) { } if (properties.hasOwnProperty("windFactor")) { windFactor = properties.windFactor; - } else { + } else if (complexMovement == true){ print("ERROR: Wind Factor must be defined for complex movement") } From 1193b89918dba29ab25a0d5d14ee0d2c2b9b525f Mon Sep 17 00:00:00 2001 From: Niraj Venkat Date: Mon, 27 Jul 2015 12:14:29 -0700 Subject: [PATCH 101/242] Coding standard and tab fixes --- libraries/render-utils/src/AmbientOcclusionEffect.cpp | 6 +++--- libraries/render-utils/src/AmbientOcclusionEffect.h | 6 +++--- libraries/render-utils/src/gaussian_blur.slf | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index d7fa88b276..f19fa6e18a 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -1,9 +1,9 @@ // // AmbientOcclusionEffect.cpp -// interface/src/renderer +// libraries/render-utils/src/ // -// Created by Niraj Venkat on 7/14/13. -// Copyright 2013 High Fidelity, Inc. +// Created by Niraj Venkat on 7/15/15. +// 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 diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.h b/libraries/render-utils/src/AmbientOcclusionEffect.h index 76cb0b063d..0b695dd2ad 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.h +++ b/libraries/render-utils/src/AmbientOcclusionEffect.h @@ -1,9 +1,9 @@ // // AmbientOcclusionEffect.h -// interface/src/renderer +// libraries/render-utils/src/ // -// Created by Andrzej Kapolka on 7/14/13. -// Copyright 2013 High Fidelity, Inc. +// Created by Niraj Venkat on 7/15/15. +// 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 diff --git a/libraries/render-utils/src/gaussian_blur.slf b/libraries/render-utils/src/gaussian_blur.slf index 5b66a0b751..772a80249c 100644 --- a/libraries/render-utils/src/gaussian_blur.slf +++ b/libraries/render-utils/src/gaussian_blur.slf @@ -23,7 +23,7 @@ varying vec2 varBlurTexcoords[14]; uniform sampler2D occlusionTexture; void main(void) { - gl_FragColor = vec4(0.0); + gl_FragColor = vec4(0.0); gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 0])*0.0044299121055113265; gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 1])*0.00895781211794; gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 2])*0.0215963866053; From d2ee74f7c7c844783fd7558771d1aaf51efc22af Mon Sep 17 00:00:00 2001 From: Niraj Venkat Date: Mon, 27 Jul 2015 12:20:41 -0700 Subject: [PATCH 102/242] Shader code indentation --- .../render-utils/src/ambient_occlusion.slf | 12 ++++---- libraries/render-utils/src/gaussian_blur.slf | 22 +++++++-------- .../src/gaussian_blur_horizontal.slv | 28 +++++++++---------- .../src/gaussian_blur_vertical.slv | 28 +++++++++---------- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/libraries/render-utils/src/ambient_occlusion.slf b/libraries/render-utils/src/ambient_occlusion.slf index 7a80dd559e..3a49accf58 100644 --- a/libraries/render-utils/src/ambient_occlusion.slf +++ b/libraries/render-utils/src/ambient_occlusion.slf @@ -80,10 +80,10 @@ void main(void) { vec3 normal = normalize(texture2D(normalTexture, varTexcoord).xyz); //normal = normalize(normal * normalMatrix); - vec3 rvec = vec3(getRandom(varTexcoord.xy), getRandom(varTexcoord.yx), getRandom(varTexcoord.xx)) * 2.0 - 1.0; + vec3 rvec = vec3(getRandom(varTexcoord.xy), getRandom(varTexcoord.yx), getRandom(varTexcoord.xx)) * 2.0 - 1.0; vec3 tangent = normalize(rvec - normal * dot(rvec, normal)); - vec3 bitangent = cross(normal, tangent); - mat3 tbn = mat3(tangent, bitangent, normal); + vec3 bitangent = cross(normal, tangent); + mat3 tbn = mat3(tangent, bitangent, normal); float occlusion = 0.0; @@ -91,13 +91,13 @@ void main(void) { vec3 samplePos = origin + (tbn * sampleKernel[i]) * g_sample_rad; vec4 offset = cam._projectionViewUntranslated * vec4(samplePos, 1.0); - offset.xy = (offset.xy / offset.w) * 0.5 + 0.5; + offset.xy = (offset.xy / offset.w) * 0.5 + 0.5; float depth = length(samplePos - cameraPositionWorldSpace); float sampleDepthVal = texture2D(depthTexture, offset.xy).r; - float rangeDelta = abs(depthVal - sampleDepthVal); - float rangeCheck = smoothstep(0.0, 1.0, g_sample_rad / rangeDelta); + float rangeDelta = abs(depthVal - sampleDepthVal); + float rangeCheck = smoothstep(0.0, 1.0, g_sample_rad / rangeDelta); occlusion += rangeCheck * step(sampleDepthVal, depth); } diff --git a/libraries/render-utils/src/gaussian_blur.slf b/libraries/render-utils/src/gaussian_blur.slf index 772a80249c..63ba14a07c 100644 --- a/libraries/render-utils/src/gaussian_blur.slf +++ b/libraries/render-utils/src/gaussian_blur.slf @@ -24,17 +24,17 @@ uniform sampler2D occlusionTexture; void main(void) { gl_FragColor = vec4(0.0); - gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 0])*0.0044299121055113265; - gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 1])*0.00895781211794; - gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 2])*0.0215963866053; - gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 3])*0.0443683338718; - gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 4])*0.0776744219933; - gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 5])*0.115876621105; - gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 6])*0.147308056121; - gl_FragColor += texture2D(occlusionTexture, varTexcoord )*0.159576912161; - gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 7])*0.147308056121; - gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 8])*0.115876621105; - gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[ 9])*0.0776744219933; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[0])*0.0044299121055113265; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[1])*0.00895781211794; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[2])*0.0215963866053; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[3])*0.0443683338718; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[4])*0.0776744219933; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[5])*0.115876621105; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[6])*0.147308056121; + gl_FragColor += texture2D(occlusionTexture, varTexcoord)*0.159576912161; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[7])*0.147308056121; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[8])*0.115876621105; + gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[9])*0.0776744219933; gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[10])*0.0443683338718; gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[11])*0.0215963866053; gl_FragColor += texture2D(occlusionTexture, varBlurTexcoords[12])*0.00895781211794; diff --git a/libraries/render-utils/src/gaussian_blur_horizontal.slv b/libraries/render-utils/src/gaussian_blur_horizontal.slv index 94631b4b08..c3f326daac 100644 --- a/libraries/render-utils/src/gaussian_blur_horizontal.slv +++ b/libraries/render-utils/src/gaussian_blur_horizontal.slv @@ -23,19 +23,19 @@ void main(void) { varTexcoord = gl_MultiTexCoord0.xy; gl_Position = gl_Vertex; - varBlurTexcoords[ 0] = varTexcoord + vec2(-0.028, 0.0); - varBlurTexcoords[ 1] = varTexcoord + vec2(-0.024, 0.0); - varBlurTexcoords[ 2] = varTexcoord + vec2(-0.020, 0.0); - varBlurTexcoords[ 3] = varTexcoord + vec2(-0.016, 0.0); - varBlurTexcoords[ 4] = varTexcoord + vec2(-0.012, 0.0); - varBlurTexcoords[ 5] = varTexcoord + vec2(-0.008, 0.0); - varBlurTexcoords[ 6] = varTexcoord + vec2(-0.004, 0.0); - varBlurTexcoords[ 7] = varTexcoord + vec2( 0.004, 0.0); - varBlurTexcoords[ 8] = varTexcoord + vec2( 0.008, 0.0); - varBlurTexcoords[ 9] = varTexcoord + vec2( 0.012, 0.0); - varBlurTexcoords[10] = varTexcoord + vec2( 0.016, 0.0); - varBlurTexcoords[11] = varTexcoord + vec2( 0.020, 0.0); - varBlurTexcoords[12] = varTexcoord + vec2( 0.024, 0.0); - varBlurTexcoords[13] = varTexcoord + vec2( 0.028, 0.0); + varBlurTexcoords[0] = varTexcoord + vec2(-0.028, 0.0); + varBlurTexcoords[1] = varTexcoord + vec2(-0.024, 0.0); + varBlurTexcoords[2] = varTexcoord + vec2(-0.020, 0.0); + varBlurTexcoords[3] = varTexcoord + vec2(-0.016, 0.0); + varBlurTexcoords[4] = varTexcoord + vec2(-0.012, 0.0); + varBlurTexcoords[5] = varTexcoord + vec2(-0.008, 0.0); + varBlurTexcoords[6] = varTexcoord + vec2(-0.004, 0.0); + varBlurTexcoords[7] = varTexcoord + vec2(0.004, 0.0); + varBlurTexcoords[8] = varTexcoord + vec2(0.008, 0.0); + varBlurTexcoords[9] = varTexcoord + vec2(0.012, 0.0); + varBlurTexcoords[10] = varTexcoord + vec2(0.016, 0.0); + varBlurTexcoords[11] = varTexcoord + vec2(0.020, 0.0); + varBlurTexcoords[12] = varTexcoord + vec2(0.024, 0.0); + varBlurTexcoords[13] = varTexcoord + vec2(0.028, 0.0); } \ No newline at end of file diff --git a/libraries/render-utils/src/gaussian_blur_vertical.slv b/libraries/render-utils/src/gaussian_blur_vertical.slv index 0d6de35b85..fc35a96bf0 100644 --- a/libraries/render-utils/src/gaussian_blur_vertical.slv +++ b/libraries/render-utils/src/gaussian_blur_vertical.slv @@ -23,19 +23,19 @@ void main(void) { varTexcoord = gl_MultiTexCoord0.xy; gl_Position = gl_Vertex; - varBlurTexcoords[ 0] = varTexcoord + vec2(0.0, -0.028); - varBlurTexcoords[ 1] = varTexcoord + vec2(0.0, -0.024); - varBlurTexcoords[ 2] = varTexcoord + vec2(0.0, -0.020); - varBlurTexcoords[ 3] = varTexcoord + vec2(0.0, -0.016); - varBlurTexcoords[ 4] = varTexcoord + vec2(0.0, -0.012); - varBlurTexcoords[ 5] = varTexcoord + vec2(0.0, -0.008); - varBlurTexcoords[ 6] = varTexcoord + vec2(0.0, -0.004); - varBlurTexcoords[ 7] = varTexcoord + vec2(0.0, 0.004); - varBlurTexcoords[ 8] = varTexcoord + vec2(0.0, 0.008); - varBlurTexcoords[ 9] = varTexcoord + vec2(0.0, 0.012); - varBlurTexcoords[10] = varTexcoord + vec2(0.0, 0.016); - varBlurTexcoords[11] = varTexcoord + vec2(0.0, 0.020); - varBlurTexcoords[12] = varTexcoord + vec2(0.0, 0.024); - varBlurTexcoords[13] = varTexcoord + vec2(0.0, 0.028); + varBlurTexcoords[0] = varTexcoord + vec2(0.0, -0.028); + varBlurTexcoords[1] = varTexcoord + vec2(0.0, -0.024); + varBlurTexcoords[2] = varTexcoord + vec2(0.0, -0.020); + varBlurTexcoords[3] = varTexcoord + vec2(0.0, -0.016); + varBlurTexcoords[4] = varTexcoord + vec2(0.0, -0.012); + varBlurTexcoords[5] = varTexcoord + vec2(0.0, -0.008); + varBlurTexcoords[6] = varTexcoord + vec2(0.0, -0.004); + varBlurTexcoords[7] = varTexcoord + vec2(0.0, 0.004); + varBlurTexcoords[8] = varTexcoord + vec2(0.0, 0.008); + varBlurTexcoords[9] = varTexcoord + vec2(0.0, 0.012); + varBlurTexcoords[10] = varTexcoord + vec2(0.0, 0.016); + varBlurTexcoords[11] = varTexcoord + vec2(0.0, 0.020); + varBlurTexcoords[12] = varTexcoord + vec2(0.0, 0.024); + varBlurTexcoords[13] = varTexcoord + vec2(0.0, 0.028); } \ No newline at end of file From 81831975b526f8a2b62a3c18e008d67f94be0c87 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 27 Jul 2015 12:27:17 -0700 Subject: [PATCH 103/242] remove legacy CapsuleShape from SkeletonModel --- interface/src/avatar/Avatar.cpp | 5 ++-- interface/src/avatar/MyAvatar.cpp | 7 ++--- interface/src/avatar/SkeletonModel.cpp | 39 +++++++++++--------------- interface/src/avatar/SkeletonModel.h | 12 ++++---- 4 files changed, 27 insertions(+), 36 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index ea9e80c132..e29e5e4408 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -1115,9 +1115,8 @@ void Avatar::setShowDisplayName(bool showDisplayName) { // virtual void Avatar::computeShapeInfo(ShapeInfo& shapeInfo) { - const CapsuleShape& capsule = _skeletonModel.getBoundingShape(); - shapeInfo.setCapsuleY(capsule.getRadius(), capsule.getHalfHeight()); - shapeInfo.setOffset(_skeletonModel.getBoundingShapeOffset()); + shapeInfo.setCapsuleY(_skeletonModel.getBoundingCapsuleRadius(), 0.5f * _skeletonModel.getBoundingCapsuleHeight()); + shapeInfo.setOffset(_skeletonModel.getBoundingCapsuleOffset()); } // virtual diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 66c59eb35d..cdd7bf2b66 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1088,11 +1088,10 @@ glm::vec3 MyAvatar::getSkeletonPosition() const { void MyAvatar::rebuildSkeletonBody() { // compute localAABox - const CapsuleShape& capsule = _skeletonModel.getBoundingShape(); - float radius = capsule.getRadius(); - float height = 2.0f * (capsule.getHalfHeight() + radius); + float radius = _skeletonModel.getBoundingCapsuleRadius(); + float height = _skeletonModel.getBoundingCapsuleHeight() + 2.0f * radius; glm::vec3 corner(-radius, -0.5f * height, -radius); - corner += _skeletonModel.getBoundingShapeOffset(); + corner += _skeletonModel.getBoundingCapsuleOffset(); glm::vec3 scale(2.0f * radius, height, 2.0f * radius); _characterController.setLocalBoundingBox(corner, scale); } diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 19dc2397c5..5c9cdd95a3 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -32,8 +32,9 @@ SkeletonModel::SkeletonModel(Avatar* owningAvatar, QObject* parent, RigPointer r Model(rig, parent), _triangleFanID(DependencyManager::get()->allocateID()), _owningAvatar(owningAvatar), - _boundingShape(), - _boundingShapeLocalOffset(0.0f), + _boundingCapsuleLocalOffset(0.0f), + _boundingCapsuleRadius(0.0f), + _boundingCapsuleHeight(0.0f), _defaultEyeModelPosition(glm::vec3(0.0f, 0.0f, 0.0f)), _standingFoot(NO_FOOT), _standingOffset(0.0f), @@ -177,9 +178,6 @@ void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { if (_rig->getJointsAreDirty()) { updateClusterMatrices(); } - - _boundingShape.setTranslation(_translation + _rotation * _boundingShapeLocalOffset); - _boundingShape.setRotation(_rotation); } void SkeletonModel::renderIKConstraints(gpu::Batch& batch) { @@ -671,12 +669,11 @@ void SkeletonModel::computeBoundingShape(const FBXGeometry& geometry) { // NOTE: we assume that the longest side of totalExtents is the yAxis... glm::vec3 diagonal = totalExtents.maximum - totalExtents.minimum; // ... and assume the radius is half the RMS of the X and Z sides: - float capsuleRadius = 0.5f * sqrtf(0.5f * (diagonal.x * diagonal.x + diagonal.z * diagonal.z)); - _boundingShape.setRadius(capsuleRadius); - _boundingShape.setHalfHeight(0.5f * diagonal.y - capsuleRadius); + _boundingCapsuleRadius = 0.5f * sqrtf(0.5f * (diagonal.x * diagonal.x + diagonal.z * diagonal.z)); + _boundingCapsuleHeight = diagonal.y - 2.0f * _boundingCapsuleRadius; glm::vec3 rootPosition = _rig->getJointState(geometry.rootJointIndex).getPosition(); - _boundingShapeLocalOffset = 0.5f * (totalExtents.maximum + totalExtents.minimum) - rootPosition; + _boundingCapsuleLocalOffset = 0.5f * (totalExtents.maximum + totalExtents.minimum) - rootPosition; _boundingRadius = 0.5f * glm::length(diagonal); } @@ -687,30 +684,26 @@ void SkeletonModel::renderBoundingCollisionShapes(gpu::Batch& batch, float alpha auto deferredLighting = DependencyManager::get(); Transform transform; // = Transform(); - // draw a blue sphere at the capsule end point - glm::vec3 endPoint; - _boundingShape.getEndPoint(endPoint); - endPoint = endPoint + _translation; - transform.setTranslation(endPoint); + // draw a blue sphere at the capsule top point + glm::vec3 topPoint = _translation + _boundingCapsuleLocalOffset + (0.5f * _boundingCapsuleHeight) * glm::vec3(0.0f, 1.0f, 0.0f); + transform.setTranslation(topPoint); batch.setModelTransform(transform); deferredLighting->bindSimpleProgram(batch); - geometryCache->renderSphere(batch, _boundingShape.getRadius(), BALL_SUBDIVISIONS, BALL_SUBDIVISIONS, + geometryCache->renderSphere(batch, _boundingCapsuleRadius, BALL_SUBDIVISIONS, BALL_SUBDIVISIONS, glm::vec4(0.6f, 0.6f, 0.8f, alpha)); - // draw a yellow sphere at the capsule start point - glm::vec3 startPoint; - _boundingShape.getStartPoint(startPoint); - startPoint = startPoint + _translation; - glm::vec3 axis = endPoint - startPoint; - transform.setTranslation(startPoint); + // draw a yellow sphere at the capsule bottom point + glm::vec3 bottomPoint = topPoint - glm::vec3(0.0f, -_boundingCapsuleHeight, 0.0f); + glm::vec3 axis = topPoint - bottomPoint; + transform.setTranslation(bottomPoint); batch.setModelTransform(transform); deferredLighting->bindSimpleProgram(batch); - geometryCache->renderSphere(batch, _boundingShape.getRadius(), BALL_SUBDIVISIONS, BALL_SUBDIVISIONS, + geometryCache->renderSphere(batch, _boundingCapsuleRadius, BALL_SUBDIVISIONS, BALL_SUBDIVISIONS, glm::vec4(0.8f, 0.8f, 0.6f, alpha)); // draw a green cylinder between the two points glm::vec3 origin(0.0f); - Avatar::renderJointConnectingCone(batch, origin, axis, _boundingShape.getRadius(), _boundingShape.getRadius(), + Avatar::renderJointConnectingCone(batch, origin, axis, _boundingCapsuleRadius, _boundingCapsuleRadius, glm::vec4(0.6f, 0.8f, 0.6f, alpha)); } diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 14dc2da3f0..89c7ab1fcb 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -13,7 +13,6 @@ #define hifi_SkeletonModel_h -#include #include class Avatar; @@ -98,9 +97,9 @@ public: void computeBoundingShape(const FBXGeometry& geometry); void renderBoundingCollisionShapes(gpu::Batch& batch, float alpha); - float getBoundingShapeRadius() const { return _boundingShape.getRadius(); } - const CapsuleShape& getBoundingShape() const { return _boundingShape; } - const glm::vec3 getBoundingShapeOffset() const { return _boundingShapeLocalOffset; } + float getBoundingCapsuleRadius() const { return _boundingCapsuleRadius; } + float getBoundingCapsuleHeight() const { return _boundingCapsuleHeight; } + const glm::vec3 getBoundingCapsuleOffset() const { return _boundingCapsuleLocalOffset; } bool hasSkeleton(); @@ -157,8 +156,9 @@ private: Avatar* _owningAvatar; - CapsuleShape _boundingShape; - glm::vec3 _boundingShapeLocalOffset; + glm::vec3 _boundingCapsuleLocalOffset; + float _boundingCapsuleRadius; + float _boundingCapsuleHeight; glm::vec3 _defaultEyeModelPosition; int _standingFoot; From 990f0d6d0712451396467e17520aa41e6ae9cd85 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 27 Jul 2015 12:57:39 -0700 Subject: [PATCH 104/242] Pass correct position/velocity/orientation to Rig simulation. --- libraries/animation/src/Rig.cpp | 28 ++++++++++++---------------- libraries/animation/src/Rig.h | 4 +++- libraries/render-utils/src/Model.cpp | 8 ++++---- libraries/render-utils/src/Model.h | 4 ++-- 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index c0602dc870..9b2cd10d9d 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -341,17 +341,16 @@ glm::mat4 Rig::getJointVisibleTransform(int jointIndex) const { return maybeCauterizeHead(jointIndex).getVisibleTransform(); } -void Rig::simulateInternal(float deltaTime, glm::mat4 parentTransform, const glm::vec3& worldPosition, const glm::quat& worldRotation) { - glm::vec3 front = worldRotation * IDENTITY_FRONT; - glm::vec3 delta = worldPosition - _lastPosition ; - float forwardSpeed = glm::dot(delta, front) / deltaTime; - float rotationalSpeed = glm::angle(front, _lastFront) / deltaTime; - bool isWalking = std::abs(forwardSpeed) > 0.01; - bool isTurning = std::abs(rotationalSpeed) > 0.5; +void Rig::simulateInternal(float deltaTime, glm::mat4 parentTransform, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation) { - // Crude, until we have blending: - const float EXPECTED_INTERVAL = 1.0f / 60.0f; - if (deltaTime >= EXPECTED_INTERVAL) { + if (_enableRig) { + glm::vec3 front = worldRotation * IDENTITY_FRONT; + float forwardSpeed = glm::dot(worldVelocity, front); + float rotationalSpeed = glm::angle(front, _lastFront) / deltaTime; + bool isWalking = std::abs(forwardSpeed) > 0.01; + bool isTurning = std::abs(rotationalSpeed) > 0.5; + + // Crude, until we have blending: isTurning = isTurning && !isWalking; // Only one of walk/turn, walk wins. isTurning = false; // FIXME bool isIdle = !isWalking && !isTurning; @@ -361,15 +360,12 @@ void Rig::simulateInternal(float deltaTime, glm::mat4 parentTransform, const glm QString toStop = singleRole(_isWalking && !isWalking, _isTurning && !isTurning, _isIdle && !isIdle); if (!toStop.isEmpty()) { //qCDebug(animation) << "isTurning" << isTurning << "fronts" << front << _lastFront << glm::angle(front, _lastFront) << rotationalSpeed; - //stopAnimationByRole(toStop); + stopAnimationByRole(toStop); } QString newRole = singleRole(isWalking && !_isWalking, isTurning && !_isTurning, isIdle && !_isIdle); if (!newRole.isEmpty()) { - //startAnimationByRole(newRole); - qCDebug(animation) << deltaTime << ":" /*<< _lastPosition << worldPosition << "=>" */<< delta << "." << front << "=> " << forwardSpeed << newRole; - /*if (newRole == "idle") { - qCDebug(animation) << deltaTime << ":" << _lastPosition << worldPosition << "=>" << delta; - }*/ + startAnimationByRole(newRole); + qCDebug(animation) << deltaTime << ":" << worldVelocity << "." << front << "=> " << forwardSpeed << newRole; } _lastPosition = worldPosition; diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index b56151b5be..45108936d9 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -108,7 +108,7 @@ public: void setJointTransform(int jointIndex, glm::mat4 newTransform); glm::mat4 getJointVisibleTransform(int jointIndex) const; void setJointVisibleTransform(int jointIndex, glm::mat4 newTransform); - void simulateInternal(float deltaTime, glm::mat4 parentTransform, const glm::vec3& worldPosition, const glm::quat& worldRotation); + void simulateInternal(float deltaTime, glm::mat4 parentTransform, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation); bool setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, bool useRotation, int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, float priority, const QVector& freeLineage, glm::mat4 parentTransform); @@ -132,6 +132,7 @@ public: virtual bool getIsFirstPerson() const { return _isFirstPerson; } bool getJointsAreDirty() { return _jointsAreDirty; } + bool setEnableRig(bool isEnabled) { _enableRig = isEnabled; } protected: QVector _jointStates; @@ -146,6 +147,7 @@ public: bool _jointsAreDirty = false; int _neckJointIndex = -1; + bool _enableRig; bool _isWalking; bool _isTurning; bool _isIdle; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index a316da0f99..cfe25c1b8c 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1316,7 +1316,7 @@ void Model::snapToRegistrationPoint() { _snappedToRegistrationPoint = true; } -void Model::simulate(float deltaTime, bool fullUpdate) { +void Model::simulate(float deltaTime, bool fullUpdate, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation) { PROFILE_RANGE(__FUNCTION__); fullUpdate = updateGeometry() || fullUpdate || (_scaleToFit && !_scaledToFit) || (_snapModelToRegistrationPoint && !_snappedToRegistrationPoint); @@ -1337,16 +1337,16 @@ void Model::simulate(float deltaTime, bool fullUpdate) { if (_snapModelToRegistrationPoint && !_snappedToRegistrationPoint) { snapToRegistrationPoint(); } - simulateInternal(deltaTime); + simulateInternal(deltaTime, worldPosition, worldVelocity, worldRotation); } } -void Model::simulateInternal(float deltaTime) { +void Model::simulateInternal(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation) { // update the world space transforms for all joints const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - _rig->simulateInternal(deltaTime, parentTransform, getTranslation(), getRotation()); + _rig->simulateInternal(deltaTime, parentTransform, worldPosition, worldVelocity, worldRotation); _shapesAreDirty = !_shapes.isEmpty(); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 45d7ce63ab..88446aba47 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -117,7 +117,7 @@ public: void setSnapModelToRegistrationPoint(bool snapModelToRegistrationPoint, const glm::vec3& registrationPoint); bool getSnapModelToRegistrationPoint() { return _snapModelToRegistrationPoint; } - virtual void simulate(float deltaTime, bool fullUpdate = true); + virtual void simulate(float deltaTime, bool fullUpdate = true, const glm::vec3& worldPosition = glm::vec3(), const glm::vec3& worldVelocity = glm::vec3(), const glm::quat& worldRotation = glm::quat()); /// Returns a reference to the shared geometry. const QSharedPointer& getGeometry() const { return _geometry; } @@ -274,7 +274,7 @@ protected: void scaleToFit(); void snapToRegistrationPoint(); - void simulateInternal(float deltaTime); + void simulateInternal(float deltaTime, const glm::vec3& worldPosition = glm::vec3(), const glm::vec3& worldVelocity = glm::vec3(), const glm::quat& worldRotation = glm::quat()); /// Updates the state of the joint at the specified index. virtual void updateJointState(int index); From 8196770ed31217d761ee8f58e104b5f23d28dc6e Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 27 Jul 2015 12:58:43 -0700 Subject: [PATCH 105/242] Wiring, including avatar position/velocity/orientation data, and an enableRig setting so that we don't break stuff unless turned on. --- interface/src/avatar/Head.cpp~ | 416 +++++++++++++++++++++++++ interface/src/avatar/MyAvatar.cpp | 1 + interface/src/avatar/SkeletonModel.cpp | 2 +- tests/rig/src/RigTests.cpp | 3 +- 4 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 interface/src/avatar/Head.cpp~ diff --git a/interface/src/avatar/Head.cpp~ b/interface/src/avatar/Head.cpp~ new file mode 100644 index 0000000000..0dace70b3c --- /dev/null +++ b/interface/src/avatar/Head.cpp~ @@ -0,0 +1,416 @@ +// +// Head.cpp +// interface/src/avatar +// +// Copyright 2013 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 +#include + +#include "Application.h" +#include "Avatar.h" +#include "GeometryUtil.h" +#include "Head.h" +#include "Menu.h" +#include "Util.h" +#include "devices/DdeFaceTracker.h" +#include "devices/Faceshift.h" +#include "AvatarRig.h" + +using namespace std; + +Head::Head(Avatar* owningAvatar) : + HeadData((AvatarData*)owningAvatar), + _returnHeadToCenter(false), + _position(0.0f, 0.0f, 0.0f), + _rotation(0.0f, 0.0f, 0.0f), + _leftEyePosition(0.0f, 0.0f, 0.0f), + _rightEyePosition(0.0f, 0.0f, 0.0f), + _eyePosition(0.0f, 0.0f, 0.0f), + _scale(1.0f), + _lastLoudness(0.0f), + _longTermAverageLoudness(-1.0f), + _audioAttack(0.0f), + _audioJawOpen(0.0f), + _mouth2(0.0f), + _mouth3(0.0f), + _mouth4(0.0f), + _renderLookatVectors(false), + _saccade(0.0f, 0.0f, 0.0f), + _saccadeTarget(0.0f, 0.0f, 0.0f), + _leftEyeBlinkVelocity(0.0f), + _rightEyeBlinkVelocity(0.0f), + _timeWithoutTalking(0.0f), + _deltaPitch(0.0f), + _deltaYaw(0.0f), + _deltaRoll(0.0f), + _deltaLeanSideways(0.0f), + _deltaLeanForward(0.0f), + _isCameraMoving(false), + _isLookingAtMe(false), +<<<<<<< HEAD + _faceModel(this, std::make_shared()), +======= + _lookingAtMeStarted(0), + _wasLastLookingAtMe(0), + _faceModel(this), +>>>>>>> 8a34df380cac67142dbb30bc20e8e022fdd551e0 + _leftEyeLookAtID(DependencyManager::get()->allocateID()), + _rightEyeLookAtID(DependencyManager::get()->allocateID()) +{ +} + +void Head::init() { + _faceModel.init(); +} + +void Head::reset() { + _baseYaw = _basePitch = _baseRoll = 0.0f; + _leanForward = _leanSideways = 0.0f; + _faceModel.reset(); +} + +void Head::simulate(float deltaTime, bool isMine, bool billboard) { + // Update audio trailing average for rendering facial animations + const float AUDIO_AVERAGING_SECS = 0.05f; + const float AUDIO_LONG_TERM_AVERAGING_SECS = 30.0f; + _averageLoudness = glm::mix(_averageLoudness, _audioLoudness, glm::min(deltaTime / AUDIO_AVERAGING_SECS, 1.0f)); + + if (_longTermAverageLoudness == -1.0f) { + _longTermAverageLoudness = _averageLoudness; + } else { + _longTermAverageLoudness = glm::mix(_longTermAverageLoudness, _averageLoudness, glm::min(deltaTime / AUDIO_LONG_TERM_AVERAGING_SECS, 1.0f)); + } + + if (isMine) { + MyAvatar* myAvatar = static_cast(_owningAvatar); + + // Only use face trackers when not playing back a recording. + if (!myAvatar->isPlaying()) { + FaceTracker* faceTracker = Application::getInstance()->getActiveFaceTracker(); + _isFaceTrackerConnected = faceTracker != NULL && !faceTracker->isMuted(); + if (_isFaceTrackerConnected) { + _blendshapeCoefficients = faceTracker->getBlendshapeCoefficients(); + + if (typeid(*faceTracker) == typeid(DdeFaceTracker)) { + + if (Menu::getInstance()->isOptionChecked(MenuOption::UseAudioForMouth)) { + calculateMouthShapes(); + + const int JAW_OPEN_BLENDSHAPE = 21; + const int MMMM_BLENDSHAPE = 34; + const int FUNNEL_BLENDSHAPE = 40; + const int SMILE_LEFT_BLENDSHAPE = 28; + const int SMILE_RIGHT_BLENDSHAPE = 29; + _blendshapeCoefficients[JAW_OPEN_BLENDSHAPE] += _audioJawOpen; + _blendshapeCoefficients[SMILE_LEFT_BLENDSHAPE] += _mouth4; + _blendshapeCoefficients[SMILE_RIGHT_BLENDSHAPE] += _mouth4; + _blendshapeCoefficients[MMMM_BLENDSHAPE] += _mouth2; + _blendshapeCoefficients[FUNNEL_BLENDSHAPE] += _mouth3; + } + + applyEyelidOffset(getFinalOrientationInWorldFrame()); + } + } + } + // Twist the upper body to follow the rotation of the head, but only do this with my avatar, + // since everyone else will see the full joint rotations for other people. + const float BODY_FOLLOW_HEAD_YAW_RATE = 0.1f; + const float BODY_FOLLOW_HEAD_FACTOR = 0.66f; + float currentTwist = getTorsoTwist(); + setTorsoTwist(currentTwist + (getFinalYaw() * BODY_FOLLOW_HEAD_FACTOR - currentTwist) * BODY_FOLLOW_HEAD_YAW_RATE); + } + + if (!(_isFaceTrackerConnected || billboard)) { + // Update eye saccades + const float AVERAGE_MICROSACCADE_INTERVAL = 1.0f; + const float AVERAGE_SACCADE_INTERVAL = 6.0f; + const float MICROSACCADE_MAGNITUDE = 0.002f; + const float SACCADE_MAGNITUDE = 0.04f; + const float NOMINAL_FRAME_RATE = 60.0f; + + if (randFloat() < deltaTime / AVERAGE_MICROSACCADE_INTERVAL) { + _saccadeTarget = MICROSACCADE_MAGNITUDE * randVector(); + } else if (randFloat() < deltaTime / AVERAGE_SACCADE_INTERVAL) { + _saccadeTarget = SACCADE_MAGNITUDE * randVector(); + } + _saccade += (_saccadeTarget - _saccade) * pow(0.5f, NOMINAL_FRAME_RATE * deltaTime); + + // Detect transition from talking to not; force blink after that and a delay + bool forceBlink = false; + const float TALKING_LOUDNESS = 100.0f; + const float BLINK_AFTER_TALKING = 0.25f; + if ((_averageLoudness - _longTermAverageLoudness) > TALKING_LOUDNESS) { + _timeWithoutTalking = 0.0f; + + } else if (_timeWithoutTalking < BLINK_AFTER_TALKING && (_timeWithoutTalking += deltaTime) >= BLINK_AFTER_TALKING) { + forceBlink = true; + } + + // Update audio attack data for facial animation (eyebrows and mouth) + const float AUDIO_ATTACK_AVERAGING_RATE = 0.9f; + _audioAttack = AUDIO_ATTACK_AVERAGING_RATE * _audioAttack + (1.0f - AUDIO_ATTACK_AVERAGING_RATE) * fabs((_audioLoudness - _longTermAverageLoudness) - _lastLoudness); + _lastLoudness = (_audioLoudness - _longTermAverageLoudness); + + const float BROW_LIFT_THRESHOLD = 100.0f; + if (_audioAttack > BROW_LIFT_THRESHOLD) { + _browAudioLift += sqrtf(_audioAttack) * 0.01f; + } + _browAudioLift = glm::clamp(_browAudioLift *= 0.7f, 0.0f, 1.0f); + + const float BLINK_SPEED = 10.0f; + const float BLINK_SPEED_VARIABILITY = 1.0f; + const float BLINK_START_VARIABILITY = 0.25f; + const float FULLY_OPEN = 0.0f; + const float FULLY_CLOSED = 1.0f; + if (_leftEyeBlinkVelocity == 0.0f && _rightEyeBlinkVelocity == 0.0f) { + // no blinking when brows are raised; blink less with increasing loudness + const float BASE_BLINK_RATE = 15.0f / 60.0f; + const float ROOT_LOUDNESS_TO_BLINK_INTERVAL = 0.25f; + if (forceBlink || (_browAudioLift < EPSILON && shouldDo(glm::max(1.0f, sqrt(fabs(_averageLoudness - _longTermAverageLoudness)) * + ROOT_LOUDNESS_TO_BLINK_INTERVAL) / BASE_BLINK_RATE, deltaTime))) { + _leftEyeBlinkVelocity = BLINK_SPEED + randFloat() * BLINK_SPEED_VARIABILITY; + _rightEyeBlinkVelocity = BLINK_SPEED + randFloat() * BLINK_SPEED_VARIABILITY; + if (randFloat() < 0.5f) { + _leftEyeBlink = BLINK_START_VARIABILITY; + } else { + _rightEyeBlink = BLINK_START_VARIABILITY; + } + } + } else { + _leftEyeBlink = glm::clamp(_leftEyeBlink + _leftEyeBlinkVelocity * deltaTime, FULLY_OPEN, FULLY_CLOSED); + _rightEyeBlink = glm::clamp(_rightEyeBlink + _rightEyeBlinkVelocity * deltaTime, FULLY_OPEN, FULLY_CLOSED); + + if (_leftEyeBlink == FULLY_CLOSED) { + _leftEyeBlinkVelocity = -BLINK_SPEED; + + } else if (_leftEyeBlink == FULLY_OPEN) { + _leftEyeBlinkVelocity = 0.0f; + } + if (_rightEyeBlink == FULLY_CLOSED) { + _rightEyeBlinkVelocity = -BLINK_SPEED; + + } else if (_rightEyeBlink == FULLY_OPEN) { + _rightEyeBlinkVelocity = 0.0f; + } + } + + // use data to update fake Faceshift blendshape coefficients + calculateMouthShapes(); + DependencyManager::get()->updateFakeCoefficients(_leftEyeBlink, + _rightEyeBlink, + _browAudioLift, + _audioJawOpen, + _mouth2, + _mouth3, + _mouth4, + _blendshapeCoefficients); + + applyEyelidOffset(getOrientation()); + + } else { + _saccade = glm::vec3(); + } + + if (!isMine) { + _faceModel.setLODDistance(static_cast(_owningAvatar)->getLODDistance()); + } + _leftEyePosition = _rightEyePosition = getPosition(); + if (!billboard) { + _faceModel.simulate(deltaTime); + if (!_faceModel.getEyePositions(_leftEyePosition, _rightEyePosition)) { + static_cast(_owningAvatar)->getSkeletonModel().getEyePositions(_leftEyePosition, _rightEyePosition); + } + } + _eyePosition = calculateAverageEyePosition(); +} + +void Head::calculateMouthShapes() { + const float JAW_OPEN_SCALE = 0.015f; + const float JAW_OPEN_RATE = 0.9f; + const float JAW_CLOSE_RATE = 0.90f; + float audioDelta = sqrtf(glm::max(_averageLoudness - _longTermAverageLoudness, 0.0f)) * JAW_OPEN_SCALE; + if (audioDelta > _audioJawOpen) { + _audioJawOpen += (audioDelta - _audioJawOpen) * JAW_OPEN_RATE; + } else { + _audioJawOpen *= JAW_CLOSE_RATE; + } + _audioJawOpen = glm::clamp(_audioJawOpen, 0.0f, 1.0f); + + // _mouth2 = "mmmm" shape + // _mouth3 = "funnel" shape + // _mouth4 = "smile" shape + const float FUNNEL_PERIOD = 0.985f; + const float FUNNEL_RANDOM_PERIOD = 0.01f; + const float MMMM_POWER = 0.25f; + const float MMMM_PERIOD = 0.91f; + const float MMMM_RANDOM_PERIOD = 0.15f; + const float SMILE_PERIOD = 0.925f; + const float SMILE_RANDOM_PERIOD = 0.05f; + + _mouth3 = glm::mix(_audioJawOpen, _mouth3, FUNNEL_PERIOD + randFloat() * FUNNEL_RANDOM_PERIOD); + _mouth2 = glm::mix(_audioJawOpen * MMMM_POWER, _mouth2, MMMM_PERIOD + randFloat() * MMMM_RANDOM_PERIOD); + _mouth4 = glm::mix(_audioJawOpen, _mouth4, SMILE_PERIOD + randFloat() * SMILE_RANDOM_PERIOD); +} + +void Head::applyEyelidOffset(glm::quat headOrientation) { + // Adjusts the eyelid blendshape coefficients so that the eyelid follows the iris as the head pitches. + + glm::quat eyeRotation = rotationBetween(headOrientation * IDENTITY_FRONT, getCorrectedLookAtPosition() - _eyePosition); + eyeRotation = eyeRotation * glm::angleAxis(safeEulerAngles(headOrientation).y, IDENTITY_UP); // Rotation w.r.t. head + float eyePitch = safeEulerAngles(eyeRotation).x; + + const float EYE_PITCH_TO_COEFFICIENT = 1.6f; // Empirically determined + const float MAX_EYELID_OFFSET = 0.8f; // So that don't fully close eyes when looking way down + float eyelidOffset = glm::clamp(-eyePitch * EYE_PITCH_TO_COEFFICIENT, -1.0f, MAX_EYELID_OFFSET); + + for (int i = 0; i < 2; i++) { + const int LEFT_EYE = 8; + float eyeCoefficient = _blendshapeCoefficients[i] - _blendshapeCoefficients[LEFT_EYE + i]; // Raw value + eyeCoefficient = glm::clamp(eyelidOffset + eyeCoefficient * (1.0f - eyelidOffset), -1.0f, 1.0f); + if (eyeCoefficient > 0.0f) { + _blendshapeCoefficients[i] = eyeCoefficient; + _blendshapeCoefficients[LEFT_EYE + i] = 0.0f; + + } else { + _blendshapeCoefficients[i] = 0.0f; + _blendshapeCoefficients[LEFT_EYE + i] = -eyeCoefficient; + } + } +} + +void Head::relaxLean(float deltaTime) { + // restore rotation, lean to neutral positions + const float LEAN_RELAXATION_PERIOD = 0.25f; // seconds + float relaxationFactor = 1.0f - glm::min(deltaTime / LEAN_RELAXATION_PERIOD, 1.0f); + _deltaYaw *= relaxationFactor; + _deltaPitch *= relaxationFactor; + _deltaRoll *= relaxationFactor; + _leanSideways *= relaxationFactor; + _leanForward *= relaxationFactor; + _deltaLeanSideways *= relaxationFactor; + _deltaLeanForward *= relaxationFactor; +} + +void Head::render(RenderArgs* renderArgs, float alpha, ViewFrustum* renderFrustum) { + if (_renderLookatVectors) { + renderLookatVectors(renderArgs, _leftEyePosition, _rightEyePosition, getCorrectedLookAtPosition()); + } +} + +void Head::setScale (float scale) { + if (_scale == scale) { + return; + } + _scale = scale; +} + +glm::quat Head::getFinalOrientationInWorldFrame() const { + return _owningAvatar->getOrientation() * getFinalOrientationInLocalFrame(); +} + +glm::quat Head::getFinalOrientationInLocalFrame() const { + return glm::quat(glm::radians(glm::vec3(getFinalPitch(), getFinalYaw(), getFinalRoll() ))); +} + +glm::vec3 Head::getCorrectedLookAtPosition() { + if (isLookingAtMe()) { + return _correctedLookAtPosition; + } else { + return getLookAtPosition(); + } +} + +void Head::setCorrectedLookAtPosition(glm::vec3 correctedLookAtPosition) { + if (!isLookingAtMe()) { + _lookingAtMeStarted = usecTimestampNow(); + } + _isLookingAtMe = true; + _wasLastLookingAtMe = usecTimestampNow(); + _correctedLookAtPosition = correctedLookAtPosition; +} + +bool Head::isLookingAtMe() { + // Allow for outages such as may be encountered during avatar movement + quint64 now = usecTimestampNow(); + const quint64 LOOKING_AT_ME_GAP_ALLOWED = 1000000; // microseconds + return _isLookingAtMe || (now - _wasLastLookingAtMe) < LOOKING_AT_ME_GAP_ALLOWED; +} + +glm::quat Head::getCameraOrientation() const { + // NOTE: Head::getCameraOrientation() is not used for orienting the camera "view" while in Oculus mode, so + // you may wonder why this code is here. This method will be called while in Oculus mode to determine how + // to change the driving direction while in Oculus mode. It is used to support driving toward where you're + // head is looking. Note that in oculus mode, your actual camera view and where your head is looking is not + // always the same. + if (qApp->isHMDMode()) { + return getOrientation(); + } + Avatar* owningAvatar = static_cast(_owningAvatar); + return owningAvatar->getWorldAlignedOrientation() * glm::quat(glm::radians(glm::vec3(_basePitch, 0.0f, 0.0f))); +} + +glm::quat Head::getEyeRotation(const glm::vec3& eyePosition) const { + glm::quat orientation = getOrientation(); + glm::vec3 lookAtDelta = _lookAtPosition - eyePosition; + return rotationBetween(orientation * IDENTITY_FRONT, lookAtDelta + glm::length(lookAtDelta) * _saccade) * orientation; +} + +glm::vec3 Head::getScalePivot() const { + return _faceModel.isActive() ? _faceModel.getTranslation() : _position; +} + +void Head::setFinalPitch(float finalPitch) { + _deltaPitch = glm::clamp(finalPitch, MIN_HEAD_PITCH, MAX_HEAD_PITCH) - _basePitch; +} + +void Head::setFinalYaw(float finalYaw) { + _deltaYaw = glm::clamp(finalYaw, MIN_HEAD_YAW, MAX_HEAD_YAW) - _baseYaw; +} + +void Head::setFinalRoll(float finalRoll) { + _deltaRoll = glm::clamp(finalRoll, MIN_HEAD_ROLL, MAX_HEAD_ROLL) - _baseRoll; +} + +float Head::getFinalYaw() const { + return glm::clamp(_baseYaw + _deltaYaw, MIN_HEAD_YAW, MAX_HEAD_YAW); +} + +float Head::getFinalPitch() const { + return glm::clamp(_basePitch + _deltaPitch, MIN_HEAD_PITCH, MAX_HEAD_PITCH); +} + +float Head::getFinalRoll() const { + return glm::clamp(_baseRoll + _deltaRoll, MIN_HEAD_ROLL, MAX_HEAD_ROLL); +} + +void Head::addLeanDeltas(float sideways, float forward) { + _deltaLeanSideways += sideways; + _deltaLeanForward += forward; +} + +void Head::renderLookatVectors(RenderArgs* renderArgs, glm::vec3 leftEyePosition, glm::vec3 rightEyePosition, glm::vec3 lookatPosition) { + auto& batch = *renderArgs->_batch; + auto transform = Transform{}; + batch.setModelTransform(transform); + batch._glLineWidth(2.0f); + + auto deferredLighting = DependencyManager::get(); + deferredLighting->bindSimpleProgram(batch); + + auto geometryCache = DependencyManager::get(); + glm::vec4 startColor(0.2f, 0.2f, 0.2f, 1.0f); + glm::vec4 endColor(1.0f, 1.0f, 1.0f, 0.0f); + geometryCache->renderLine(batch, leftEyePosition, lookatPosition, startColor, endColor, _leftEyeLookAtID); + geometryCache->renderLine(batch, rightEyePosition, lookatPosition, startColor, endColor, _rightEyeLookAtID); +} + + diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 0dceb79402..f1afd235bf 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -742,6 +742,7 @@ void MyAvatar::loadData() { setCollisionSoundURL(settings.value("collisionSoundURL", DEFAULT_AVATAR_COLLISION_SOUND_URL).toString()); settings.endGroup(); + _rig->setEnableRig(settings.value("enableRig").toBool()); } void MyAvatar::saveAttachmentData(const AttachmentData& attachment) const { diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 08960c913c..dfceab24aa 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -127,7 +127,7 @@ void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { setScale(glm::vec3(1.0f, 1.0f, 1.0f) * _owningAvatar->getScale()); setBlendshapeCoefficients(_owningAvatar->getHead()->getBlendshapeCoefficients()); - Model::simulate(deltaTime, fullUpdate); + Model::simulate(deltaTime, fullUpdate, _owningAvatar->getPosition(), _owningAvatar->getVelocity(), _owningAvatar->getOrientation()); if (!isActive() || !_owningAvatar->isMyAvatar()) { return; // only simulate for own avatar diff --git a/tests/rig/src/RigTests.cpp b/tests/rig/src/RigTests.cpp index 74047c0162..05cef428c6 100644 --- a/tests/rig/src/RigTests.cpp +++ b/tests/rig/src/RigTests.cpp @@ -69,7 +69,7 @@ void RigTests::initTestCase() { loop.exec();*/ // Blocking all further tests until signalled. // Joint geometry from fbx. -#define FROM_FILE "/Users/howardstearns/howardHiFi/Zack.fbx" +//#define FROM_FILE "/Users/howardstearns/howardHiFi/Zack.fbx" #ifdef FROM_FILE QFile file(FROM_FILE); QCOMPARE(file.open(QIODevice::ReadOnly), true); @@ -124,4 +124,5 @@ void reportSome(RigPointer rig) { void RigTests::initialPoseArmsDown() { //reportAll(_rig); reportSome(_rig); + QCOMPARE(false, true); } From 146ddd224028dce6bd4d99f00acc9c7d3deb1bae Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 27 Jul 2015 13:42:18 -0700 Subject: [PATCH 106/242] Cleanup false starts. --- libraries/animation/src/Rig.cpp | 1 - libraries/animation/src/Rig.h | 4 ---- libraries/avatars/src/AvatarData.cpp | 1 - libraries/avatars/src/AvatarData.h | 3 --- tests/rig/src/RigTests.cpp | 29 +++++----------------------- 5 files changed, 5 insertions(+), 33 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 9b2cd10d9d..d20abc58d6 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -369,7 +369,6 @@ void Rig::simulateInternal(float deltaTime, glm::mat4 parentTransform, const glm } _lastPosition = worldPosition; - _positions[(++_positionIndex) % _positions.count()] = worldPosition; // exp. alt. to above line _lastFront = front; _isWalking = isWalking; _isTurning = isTurning; diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 45108936d9..379ed64877 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -153,10 +153,6 @@ public: bool _isIdle; glm::vec3 _lastFront; glm::vec3 _lastPosition; - // or, experimentally... - QVector _positions = QVector(4); - QVector _timeIntervals = QVector(4); - int _positionIndex; }; #endif /* defined(__hifi__Rig__) */ diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 5a0ba6c674..5b970a95a3 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1087,7 +1087,6 @@ void AvatarData::setJointMappingsFromNetworkReply() { } networkReply->deleteLater(); - emit jointMappingLoaded(); } void AvatarData::sendAvatarDataPacket() { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 60c643eff9..a020be0f7a 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -312,9 +312,6 @@ public: bool shouldDie() const { return _owningAvatarMixer.isNull() || getUsecsSinceLastUpdate() > AVATAR_SILENCE_THRESHOLD_USECS; } -signals: - void jointMappingLoaded(); // So that test cases or anyone waiting on asynchronous loading can be informed. - public slots: void sendAvatarDataPacket(); void sendIdentityPacket(); diff --git a/tests/rig/src/RigTests.cpp b/tests/rig/src/RigTests.cpp index 05cef428c6..f1eb7cc440 100644 --- a/tests/rig/src/RigTests.cpp +++ b/tests/rig/src/RigTests.cpp @@ -52,23 +52,6 @@ QTEST_MAIN(RigTests) void RigTests::initTestCase() { - - // There are two good ways we could organize this: - // 1. Create a MyAvatar the same way that Interface does, and poke at it. - // We can't do that because MyAvatar (and even Avatar) are in interface, not a library, and our build system won't allow that dependency. - // 2. Create just the minimum skeleton in the most direct way possible, using only very basic library APIs (such as fbx). - // I don't think we can do that because not everything we need is exposed directly from, e.g., the fst and fbx readers. - // So here we do neither. Using as much as we can from AvatarData (which is in the avatar and further requires network and audio), and - // duplicating whatever other code we need from (My)Avatar. Ugh. We may refactor that later, but right now, cleaning this up is not on our critical path. - - // Joint mapping from fst. FIXME: Do we need this??? - /*auto avatar = std::make_shared(); - QEventLoop loop; // Create an event loop that will quit when we get the finished signal - QObject::connect(avatar.get(), &AvatarData::jointMappingLoaded, &loop, &QEventLoop::quit); - avatar->setSkeletonModelURL(QUrl("https://hifi-public.s3.amazonaws.com/marketplace/contents/4a690585-3fa3-499e-9f8b-fd1226e561b1/e47e6898027aa40f1beb6adecc6a7db5.fst")); // Zach fst - loop.exec();*/ // Blocking all further tests until signalled. - - // Joint geometry from fbx. //#define FROM_FILE "/Users/howardstearns/howardHiFi/Zack.fbx" #ifdef FROM_FILE QFile file(FROM_FILE); @@ -81,8 +64,7 @@ void RigTests::initTestCase() { QCOMPARE(fbxHttpCode, 200); FBXGeometry geometry = readFBX(reply->readAll(), QVariantHash()); #endif - //QCOMPARE(geometry.joints.count(), avatar->getJointNames().count()); - + QVector jointStates; for (int i = 0; i < geometry.joints.size(); ++i) { // Note that if the geometry is stack allocated and goes away, so will the joints. Hence the heap copy here. @@ -97,24 +79,24 @@ void RigTests::initTestCase() { std::cout << "Rig is ready " << geometry.joints.count() << " joints " << std::endl; } -void reportJoint(int index, JointState joint) { // Handy for debugging +static void reportJoint(int index, JointState joint) { // Handy for debugging std::cout << "\n"; std::cout << index << " " << joint.getFBXJoint().name.toUtf8().data() << "\n"; std::cout << " pos:" << joint.getPosition() << "/" << joint.getPositionInParentFrame() << " from " << joint.getParentIndex() << "\n"; std::cout << " rot:" << safeEulerAngles(joint.getRotation()) << "/" << safeEulerAngles(joint.getRotationInParentFrame()) << "/" << safeEulerAngles(joint.getRotationInBindFrame()) << "\n"; std::cout << "\n"; } -void reportByName(RigPointer rig, const QString& name) { +static void reportByName(RigPointer rig, const QString& name) { int jointIndex = rig->indexOfJoint(name); reportJoint(jointIndex, rig->getJointState(jointIndex)); } -void reportAll(RigPointer rig) { +static void reportAll(RigPointer rig) { for (int i = 0; i < rig->getJointStateCount(); i++) { JointState joint = rig->getJointState(i); reportJoint(i, joint); } } -void reportSome(RigPointer rig) { +static void reportSome(RigPointer rig) { QString names[] = {"Head", "Neck", "RightShoulder", "RightArm", "RightForeArm", "RightHand", "Spine2", "Spine1", "Spine", "Hips", "RightUpLeg", "RightLeg", "RightFoot", "RightToeBase", "RightToe_End"}; for (auto name : names) { reportByName(rig, name); @@ -124,5 +106,4 @@ void reportSome(RigPointer rig) { void RigTests::initialPoseArmsDown() { //reportAll(_rig); reportSome(_rig); - QCOMPARE(false, true); } From 51a12adc08be27a4fccb47087f002a13cadd3b41 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 27 Jul 2015 14:02:47 -0700 Subject: [PATCH 107/242] Less dependencies in tests. --- tests/rig/CMakeLists.txt | 2 +- tests/rig/src/RigTests.cpp | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/rig/CMakeLists.txt b/tests/rig/CMakeLists.txt index 5e965c3ee8..2e9dbc9424 100644 --- a/tests/rig/CMakeLists.txt +++ b/tests/rig/CMakeLists.txt @@ -1,7 +1,7 @@ # Declare dependencies macro (setup_testcase_dependencies) # link in the shared libraries - link_hifi_libraries(shared animation gpu fbx model avatars networking audio) + link_hifi_libraries(shared animation gpu fbx model) copy_dlls_beside_windows_executable() endmacro () diff --git a/tests/rig/src/RigTests.cpp b/tests/rig/src/RigTests.cpp index f1eb7cc440..0530ad5638 100644 --- a/tests/rig/src/RigTests.cpp +++ b/tests/rig/src/RigTests.cpp @@ -40,11 +40,9 @@ */ #include -#include -#include "AvatarData.h" -#include "OBJReader.h" #include "FBXReader.h" +#include "OBJReader.h" #include "AvatarRig.h" // We might later test Rig vs AvatarRig separately, but for now, we're concentrating on the main use case. #include "RigTests.h" From a5ad40bee97e14a860335f0fb3a1f74d5fc981fd Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 27 Jul 2015 14:07:28 -0700 Subject: [PATCH 108/242] INtroduce the resetStage command to clear up all cache and state in the gpu::Conference and make sure no more resource are linked --- interface/src/Application.cpp | 6 ++ libraries/gpu/src/gpu/Batch.cpp | 4 ++ libraries/gpu/src/gpu/Batch.h | 5 ++ libraries/gpu/src/gpu/Context.cpp | 1 + libraries/gpu/src/gpu/GLBackend.cpp | 20 +++++++ libraries/gpu/src/gpu/GLBackend.h | 45 ++++++++++---- libraries/gpu/src/gpu/GLBackendInput.cpp | 59 ++++++++++++++++--- libraries/gpu/src/gpu/GLBackendOutput.cpp | 5 ++ libraries/gpu/src/gpu/GLBackendPipeline.cpp | 22 +++++++ libraries/gpu/src/gpu/GLBackendQuery.cpp | 4 ++ libraries/gpu/src/gpu/GLBackendState.cpp | 2 +- libraries/gpu/src/gpu/GLBackendTransform.cpp | 4 +- .../render-utils/src/RenderDeferredTask.cpp | 1 - libraries/render/src/render/DrawTask.cpp | 40 ------------- libraries/render/src/render/DrawTask.h | 8 --- 15 files changed, 156 insertions(+), 70 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index c2be31cfb3..7bfad60eaf 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -996,6 +996,12 @@ void Application::paintGL() { _compositor.displayOverlayTexture(&renderArgs); } + // Reset the gpu::Context Stages + gpu::Batch batch; + batch.resetStages(); + renderArgs._context->render(batch); + + if (!OculusManager::isConnected() || OculusManager::allowSwap()) { PROFILE_RANGE(__FUNCTION__ "/bufferSwap"); _glWidget->swapBuffers(); diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index 4ac33d8f14..8815c1bae1 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -288,6 +288,10 @@ void Batch::getQuery(const QueryPointer& query) { _params.push_back(_queries.cache(query)); } +void Batch::resetStages() { + ADD_COMMAND(resetStages); +} + void push_back(Batch::Params& params, const vec3& v) { params.push_back(v.x); params.push_back(v.y); diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index 58fe03c9cf..5f2c2dd089 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -113,6 +113,9 @@ public: void endQuery(const QueryPointer& query); void getQuery(const QueryPointer& query); + // Reset the stage caches and states + void resetStages(); + // TODO: As long as we have gl calls explicitely issued from interface // code, we need to be able to record and batch these calls. THe long // term strategy is to get rid of any GL calls in favor of the HIFI GPU API @@ -186,6 +189,8 @@ public: COMMAND_endQuery, COMMAND_getQuery, + COMMAND_resetStages, + // TODO: As long as we have gl calls explicitely issued from interface // code, we need to be able to record and batch these calls. THe long // term strategy is to get rid of any GL calls in favor of the HIFI GPU API diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index 604f39f46f..6730be33bb 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -45,3 +45,4 @@ void Context::syncCache() { void Context::downloadFramebuffer(const FramebufferPointer& srcFramebuffer, const Vec4i& region, QImage& destImage) { _backend->downloadFramebuffer(srcFramebuffer, region, destImage); } + diff --git a/libraries/gpu/src/gpu/GLBackend.cpp b/libraries/gpu/src/gpu/GLBackend.cpp index 6b1d552be9..51637462f1 100644 --- a/libraries/gpu/src/gpu/GLBackend.cpp +++ b/libraries/gpu/src/gpu/GLBackend.cpp @@ -46,6 +46,8 @@ GLBackend::CommandCall GLBackend::_commandCalls[Batch::NUM_COMMANDS] = (&::gpu::GLBackend::do_endQuery), (&::gpu::GLBackend::do_getQuery), + (&::gpu::GLBackend::do_resetStages), + (&::gpu::GLBackend::do_glEnable), (&::gpu::GLBackend::do_glDisable), @@ -128,6 +130,8 @@ GLBackend::GLBackend() : } GLBackend::~GLBackend() { + resetStages(); + killInput(); killTransform(); } @@ -246,6 +250,22 @@ void GLBackend::do_drawIndexedInstanced(Batch& batch, uint32 paramOffset) { (void) CHECK_GL_ERROR(); } +void GLBackend::do_resetStages(Batch& batch, uint32 paramOffset) { + resetStages(); +} + +void GLBackend::resetStages() { + resetInputStage(); + resetPipelineStage(); + resetTransformStage(); + resetUniformStage(); + resetResourceStage(); + resetOutputStage(); + resetQueryStage(); + + (void) CHECK_GL_ERROR(); +} + // TODO: As long as we have gl calls explicitely issued from interface // code, we need to be able to record and batch these calls. THe long // term strategy is to get rid of any GL calls in favor of the HIFI GPU API diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index 9d8c9ef805..59a6c1ee7e 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -195,7 +195,7 @@ public: static const int MAX_NUM_ATTRIBUTES = Stream::NUM_INPUT_SLOTS; static const int MAX_NUM_INPUT_BUFFERS = 16; - uint32 getNumInputBuffers() const { return _input._buffersState.size(); } + uint32 getNumInputBuffers() const { return _input._invalidBuffers.size(); } // The State setters called by the GLState::Commands when a new state is assigned @@ -250,12 +250,16 @@ protected: void killInput(); void syncInputStateCache(); void updateInput(); + void resetInputStage(); struct InputStageState { bool _invalidFormat = true; Stream::FormatPointer _format; + typedef std::bitset ActivationCache; + ActivationCache _attributeActivation; + typedef std::bitset BuffersState; - BuffersState _buffersState; + BuffersState _invalidBuffers; Buffers _buffers; Offsets _bufferOffsets; @@ -266,23 +270,20 @@ protected: Offset _indexBufferOffset; Type _indexBufferType; - typedef std::bitset ActivationCache; - ActivationCache _attributeActivation; - GLuint _defaultVAO; InputStageState() : _invalidFormat(true), _format(0), - _buffersState(0), - _buffers(_buffersState.size(), BufferPointer(0)), - _bufferOffsets(_buffersState.size(), 0), - _bufferStrides(_buffersState.size(), 0), - _bufferVBOs(_buffersState.size(), 0), + _attributeActivation(0), + _invalidBuffers(0), + _buffers(_invalidBuffers.size(), BufferPointer(0)), + _bufferOffsets(_invalidBuffers.size(), 0), + _bufferStrides(_invalidBuffers.size(), 0), + _bufferVBOs(_invalidBuffers.size(), 0), _indexBuffer(0), _indexBufferOffset(0), _indexBufferType(UINT32), - _attributeActivation(0), _defaultVAO(0) {} } _input; @@ -298,6 +299,7 @@ protected: // Synchronize the state cache of this Backend with the actual real state of the GL Context void syncTransformStateCache(); void updateTransform(); + void resetTransformStage(); struct TransformStageState { TransformObject _transformObject; TransformCamera _transformCamera; @@ -330,12 +332,20 @@ protected: // Uniform Stage void do_setUniformBuffer(Batch& batch, uint32 paramOffset); - void do_setResourceTexture(Batch& batch, uint32 paramOffset); + void resetUniformStage(); struct UniformStageState { }; + // Resource Stage + void do_setResourceTexture(Batch& batch, uint32 paramOffset); + + void resetResourceStage(); + struct ResourceStageState { + + }; + // Pipeline Stage void do_setPipeline(Batch& batch, uint32 paramOffset); void do_setStateBlendFactor(Batch& batch, uint32 paramOffset); @@ -349,6 +359,7 @@ protected: void syncPipelineStateCache(); // Grab the actual gl state into it's gpu::State equivalent. THis is used by the above call syncPipleineStateCache() void getCurrentGLState(State::Data& state); + void resetPipelineStage(); struct PipelineStageState { @@ -388,6 +399,7 @@ protected: // Synchronize the state cache of this Backend with the actual real state of the GL Context void syncOutputStateCache(); + void resetOutputStage(); struct OutputStageState { @@ -402,6 +414,15 @@ protected: void do_endQuery(Batch& batch, uint32 paramOffset); void do_getQuery(Batch& batch, uint32 paramOffset); + void resetQueryStage(); + struct QueryStageState { + + }; + + // Reset stages + void do_resetStages(Batch& batch, uint32 paramOffset); + void resetStages(); + // TODO: As long as we have gl calls explicitely issued from interface // code, we need to be able to record and batch these calls. THe long // term strategy is to get rid of any GL calls in favor of the HIFI GPU API diff --git a/libraries/gpu/src/gpu/GLBackendInput.cpp b/libraries/gpu/src/gpu/GLBackendInput.cpp index 229b29cd43..9014024914 100755 --- a/libraries/gpu/src/gpu/GLBackendInput.cpp +++ b/libraries/gpu/src/gpu/GLBackendInput.cpp @@ -52,7 +52,7 @@ void GLBackend::do_setInputBuffer(Batch& batch, uint32 paramOffset) { } if (isModified) { - _input._buffersState.set(channel); + _input._invalidBuffers.set(channel); } } } @@ -154,7 +154,7 @@ void GLBackend::updateInput() { _stats._ISNumFormatChanges++; } - if (_input._buffersState.any()) { + if (_input._invalidBuffers.any()) { int numBuffers = _input._buffers.size(); auto buffer = _input._buffers.data(); auto vbo = _input._bufferVBOs.data(); @@ -162,7 +162,7 @@ void GLBackend::updateInput() { auto stride = _input._bufferStrides.data(); for (int bufferNum = 0; bufferNum < numBuffers; bufferNum++) { - if (_input._buffersState.test(bufferNum)) { + if (_input._invalidBuffers.test(bufferNum)) { glBindVertexBuffer(bufferNum, (*vbo), (*offset), (*stride)); } buffer++; @@ -170,11 +170,11 @@ void GLBackend::updateInput() { offset++; stride++; } - _input._buffersState.reset(); + _input._invalidBuffers.reset(); (void) CHECK_GL_ERROR(); } #else - if (_input._invalidFormat || _input._buffersState.any()) { + if (_input._invalidFormat || _input._invalidBuffers.any()) { if (_input._invalidFormat) { InputStageState::ActivationCache newActivation; @@ -232,7 +232,7 @@ void GLBackend::updateInput() { if ((channelIt).first < buffers.size()) { int bufferNum = (channelIt).first; - if (_input._buffersState.test(bufferNum) || _input._invalidFormat) { + if (_input._invalidBuffers.test(bufferNum) || _input._invalidFormat) { // GLuint vbo = gpu::GLBackend::getBufferID((*buffers[bufferNum])); GLuint vbo = _input._bufferVBOs[bufferNum]; if (boundVBO != vbo) { @@ -240,7 +240,7 @@ void GLBackend::updateInput() { (void) CHECK_GL_ERROR(); boundVBO = vbo; } - _input._buffersState[bufferNum] = false; + _input._invalidBuffers[bufferNum] = false; for (unsigned int i = 0; i < channel._slots.size(); i++) { const Stream::Attribute& attrib = attributes.at(channel._slots[i]); @@ -285,6 +285,51 @@ void GLBackend::updateInput() { #endif } +void GLBackend::resetInputStage() { + // Reset index buffer + _input._indexBufferType = UINT32; + _input._indexBufferOffset = 0; + _input._indexBuffer.reset(); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + (void) CHECK_GL_ERROR(); + + +#if defined(SUPPORT_VAO) + // TODO +#else + glBindBuffer(GL_ARRAY_BUFFER, 0); + + size_t i = 0; +#if defined(SUPPORT_LEGACY_OPENGL) + for (; i < NUM_CLASSIC_ATTRIBS; i++) { + glDisableClientState(attributeSlotToClassicAttribName[i]); + } + glVertexPointer(4, GL_FLOAT, 0, 0); + glNormalPointer(GL_FLOAT, 0, 0); + glColorPointer(4, GL_FLOAT, 0, 0); + glTexCoordPointer(4, GL_FLOAT, 0, 0); +#endif + + for (; i < _input._attributeActivation.size(); i++) { + glDisableVertexAttribArray(i); + glVertexAttribPointer(i, 4, GL_FLOAT, GL_FALSE, 0, 0); + } +#endif + + // Reset vertex buffer and format + _input._format.reset(); + _input._invalidFormat = false; + _input._attributeActivation.reset(); + + for (int i = 0; i < _input._buffers.size(); i++) { + _input._buffers[i].reset(); + _input._bufferOffsets[i] = 0; + _input._bufferStrides[i] = 0; + _input._bufferVBOs[i] = 0; + } + _input._invalidBuffers.reset(); + +} void GLBackend::do_setIndexBuffer(Batch& batch, uint32 paramOffset) { _input._indexBufferType = (Type) batch._params[paramOffset + 2]._uint; diff --git a/libraries/gpu/src/gpu/GLBackendOutput.cpp b/libraries/gpu/src/gpu/GLBackendOutput.cpp index b37def48e7..6b0bfae635 100755 --- a/libraries/gpu/src/gpu/GLBackendOutput.cpp +++ b/libraries/gpu/src/gpu/GLBackendOutput.cpp @@ -177,6 +177,11 @@ void GLBackend::syncOutputStateCache() { _output._framebuffer.reset(); } +void GLBackend::resetOutputStage() { + _output._framebuffer.reset(); + _output._drawFBO = 0; + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); +} void GLBackend::do_setFramebuffer(Batch& batch, uint32 paramOffset) { auto framebuffer = batch._framebuffers.get(batch._params[paramOffset]._uint); diff --git a/libraries/gpu/src/gpu/GLBackendPipeline.cpp b/libraries/gpu/src/gpu/GLBackendPipeline.cpp index 51a3a24e9b..4a54753af8 100755 --- a/libraries/gpu/src/gpu/GLBackendPipeline.cpp +++ b/libraries/gpu/src/gpu/GLBackendPipeline.cpp @@ -164,6 +164,24 @@ void GLBackend::updatePipeline() { #endif } +void GLBackend::resetPipelineStage() { + // First reset State to default + State::Signature resetSignature(0); + resetPipelineState(resetSignature); + _pipeline._state = nullptr; + _pipeline._invalidState = false; + + // Second the shader side + _pipeline._invalidProgram = false; + _pipeline._program = 0; + _pipeline._pipeline.reset(); + glUseProgram(0); +} + +void GLBackend::resetUniformStage() { + +} + void GLBackend::do_setUniformBuffer(Batch& batch, uint32 paramOffset) { GLuint slot = batch._params[paramOffset + 3]._uint; BufferPointer uniformBuffer = batch._buffers.get(batch._params[paramOffset + 2]._uint); @@ -188,6 +206,10 @@ void GLBackend::do_setUniformBuffer(Batch& batch, uint32 paramOffset) { (void) CHECK_GL_ERROR(); } +void GLBackend::resetResourceStage() { + +} + void GLBackend::do_setResourceTexture(Batch& batch, uint32 paramOffset) { GLuint slot = batch._params[paramOffset + 1]._uint; TexturePointer uniformTexture = batch._textures.get(batch._params[paramOffset + 0]._uint); diff --git a/libraries/gpu/src/gpu/GLBackendQuery.cpp b/libraries/gpu/src/gpu/GLBackendQuery.cpp index 39db19dafd..2bdf7c86ad 100644 --- a/libraries/gpu/src/gpu/GLBackendQuery.cpp +++ b/libraries/gpu/src/gpu/GLBackendQuery.cpp @@ -104,3 +104,7 @@ void GLBackend::do_getQuery(Batch& batch, uint32 paramOffset) { (void)CHECK_GL_ERROR(); } } + +void GLBackend::resetQueryStage() { +} + diff --git a/libraries/gpu/src/gpu/GLBackendState.cpp b/libraries/gpu/src/gpu/GLBackendState.cpp index 18fc9ddd3c..618a13e2c4 100644 --- a/libraries/gpu/src/gpu/GLBackendState.cpp +++ b/libraries/gpu/src/gpu/GLBackendState.cpp @@ -484,7 +484,7 @@ void GLBackend::syncPipelineStateCache() { glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); getCurrentGLState(state); State::Signature signature = State::evalSignature(state); - + _pipeline._stateCache = state; _pipeline._stateSignatureCache = signature; } diff --git a/libraries/gpu/src/gpu/GLBackendTransform.cpp b/libraries/gpu/src/gpu/GLBackendTransform.cpp index 65d9c6ed75..e05e0fc1c0 100755 --- a/libraries/gpu/src/gpu/GLBackendTransform.cpp +++ b/libraries/gpu/src/gpu/GLBackendTransform.cpp @@ -184,4 +184,6 @@ void GLBackend::updateTransform() { _transform._invalidView = _transform._invalidProj = _transform._invalidModel = _transform._invalidViewport = false; } - +void GLBackend::resetTransformStage() { + +} diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 0c8d19250b..30c8dffc0e 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -97,7 +97,6 @@ RenderDeferredTask::RenderDeferredTask() : Task() { _drawStatusJobIndex = _jobs.size() - 1; _jobs.push_back(Job(new DrawOverlay3D::JobModel("DrawOverlay3D"))); - _jobs.push_back(Job(new ResetGLState::JobModel())); // Give ourselves 3 frmaes of timer queries _timerQueries.push_back(std::make_shared()); diff --git a/libraries/render/src/render/DrawTask.cpp b/libraries/render/src/render/DrawTask.cpp index 0e3eba0b53..0aef913d50 100755 --- a/libraries/render/src/render/DrawTask.cpp +++ b/libraries/render/src/render/DrawTask.cpp @@ -216,46 +216,6 @@ void render::renderItems(const SceneContextPointer& sceneContext, const RenderCo } } -void addClearStateCommands(gpu::Batch& batch) { - batch._glDepthMask(true); - batch._glDepthFunc(GL_LESS); - batch._glDisable(GL_CULL_FACE); - - batch._glActiveTexture(GL_TEXTURE0 + 1); - batch._glBindTexture(GL_TEXTURE_2D, 0); - batch._glActiveTexture(GL_TEXTURE0 + 2); - batch._glBindTexture(GL_TEXTURE_2D, 0); - batch._glActiveTexture(GL_TEXTURE0 + 3); - batch._glBindTexture(GL_TEXTURE_2D, 0); - batch._glActiveTexture(GL_TEXTURE0); - batch._glBindTexture(GL_TEXTURE_2D, 0); - - - // deactivate vertex arrays after drawing - batch._glDisableClientState(GL_NORMAL_ARRAY); - batch._glDisableClientState(GL_VERTEX_ARRAY); - batch._glDisableClientState(GL_TEXTURE_COORD_ARRAY); - batch._glDisableClientState(GL_COLOR_ARRAY); - batch._glDisableVertexAttribArray(gpu::Stream::TANGENT); - batch._glDisableVertexAttribArray(gpu::Stream::SKIN_CLUSTER_INDEX); - batch._glDisableVertexAttribArray(gpu::Stream::SKIN_CLUSTER_WEIGHT); - - // bind with 0 to switch back to normal operation - batch._glBindBuffer(GL_ARRAY_BUFFER, 0); - batch._glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - batch._glBindTexture(GL_TEXTURE_2D, 0); - - // Back to no program - batch._glUseProgram(0); -} -void ResetGLState::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { - - gpu::Batch theBatch; - addClearStateCommands(theBatch); - assert(renderContext->args); - renderContext->args->_context->render(theBatch); -} - void DrawLight::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { assert(renderContext->args); assert(renderContext->args->_viewFrustum); diff --git a/libraries/render/src/render/DrawTask.h b/libraries/render/src/render/DrawTask.h index 62810d47f0..ec6656c0dc 100755 --- a/libraries/render/src/render/DrawTask.h +++ b/libraries/render/src/render/DrawTask.h @@ -260,14 +260,6 @@ public: typedef Job::Model JobModel; }; -class ResetGLState { -public: - void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext); - - typedef Job::Model JobModel; -}; - - class DrawSceneTask : public Task { public: From 98fe6dbf3deb1271e61248301a337060ef4b0afa Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 27 Jul 2015 14:13:54 -0700 Subject: [PATCH 109/242] Rename tests/rig to tests/animation, per Tony. --- tests/{rig => animation}/CMakeLists.txt | 0 tests/{rig => animation}/src/RigTests.cpp | 0 tests/{rig => animation}/src/RigTests.h | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename tests/{rig => animation}/CMakeLists.txt (100%) rename tests/{rig => animation}/src/RigTests.cpp (100%) rename tests/{rig => animation}/src/RigTests.h (100%) diff --git a/tests/rig/CMakeLists.txt b/tests/animation/CMakeLists.txt similarity index 100% rename from tests/rig/CMakeLists.txt rename to tests/animation/CMakeLists.txt diff --git a/tests/rig/src/RigTests.cpp b/tests/animation/src/RigTests.cpp similarity index 100% rename from tests/rig/src/RigTests.cpp rename to tests/animation/src/RigTests.cpp diff --git a/tests/rig/src/RigTests.h b/tests/animation/src/RigTests.h similarity index 100% rename from tests/rig/src/RigTests.h rename to tests/animation/src/RigTests.h From 064f2b471f3f3cc59fefc9e7820eab5a959f8119 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 27 Jul 2015 14:29:44 -0700 Subject: [PATCH 110/242] Remove incorrectly-added file. --- interface/src/avatar/Head.cpp~ | 416 --------------------------------- 1 file changed, 416 deletions(-) delete mode 100644 interface/src/avatar/Head.cpp~ diff --git a/interface/src/avatar/Head.cpp~ b/interface/src/avatar/Head.cpp~ deleted file mode 100644 index 0dace70b3c..0000000000 --- a/interface/src/avatar/Head.cpp~ +++ /dev/null @@ -1,416 +0,0 @@ -// -// Head.cpp -// interface/src/avatar -// -// Copyright 2013 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 -#include - -#include "Application.h" -#include "Avatar.h" -#include "GeometryUtil.h" -#include "Head.h" -#include "Menu.h" -#include "Util.h" -#include "devices/DdeFaceTracker.h" -#include "devices/Faceshift.h" -#include "AvatarRig.h" - -using namespace std; - -Head::Head(Avatar* owningAvatar) : - HeadData((AvatarData*)owningAvatar), - _returnHeadToCenter(false), - _position(0.0f, 0.0f, 0.0f), - _rotation(0.0f, 0.0f, 0.0f), - _leftEyePosition(0.0f, 0.0f, 0.0f), - _rightEyePosition(0.0f, 0.0f, 0.0f), - _eyePosition(0.0f, 0.0f, 0.0f), - _scale(1.0f), - _lastLoudness(0.0f), - _longTermAverageLoudness(-1.0f), - _audioAttack(0.0f), - _audioJawOpen(0.0f), - _mouth2(0.0f), - _mouth3(0.0f), - _mouth4(0.0f), - _renderLookatVectors(false), - _saccade(0.0f, 0.0f, 0.0f), - _saccadeTarget(0.0f, 0.0f, 0.0f), - _leftEyeBlinkVelocity(0.0f), - _rightEyeBlinkVelocity(0.0f), - _timeWithoutTalking(0.0f), - _deltaPitch(0.0f), - _deltaYaw(0.0f), - _deltaRoll(0.0f), - _deltaLeanSideways(0.0f), - _deltaLeanForward(0.0f), - _isCameraMoving(false), - _isLookingAtMe(false), -<<<<<<< HEAD - _faceModel(this, std::make_shared()), -======= - _lookingAtMeStarted(0), - _wasLastLookingAtMe(0), - _faceModel(this), ->>>>>>> 8a34df380cac67142dbb30bc20e8e022fdd551e0 - _leftEyeLookAtID(DependencyManager::get()->allocateID()), - _rightEyeLookAtID(DependencyManager::get()->allocateID()) -{ -} - -void Head::init() { - _faceModel.init(); -} - -void Head::reset() { - _baseYaw = _basePitch = _baseRoll = 0.0f; - _leanForward = _leanSideways = 0.0f; - _faceModel.reset(); -} - -void Head::simulate(float deltaTime, bool isMine, bool billboard) { - // Update audio trailing average for rendering facial animations - const float AUDIO_AVERAGING_SECS = 0.05f; - const float AUDIO_LONG_TERM_AVERAGING_SECS = 30.0f; - _averageLoudness = glm::mix(_averageLoudness, _audioLoudness, glm::min(deltaTime / AUDIO_AVERAGING_SECS, 1.0f)); - - if (_longTermAverageLoudness == -1.0f) { - _longTermAverageLoudness = _averageLoudness; - } else { - _longTermAverageLoudness = glm::mix(_longTermAverageLoudness, _averageLoudness, glm::min(deltaTime / AUDIO_LONG_TERM_AVERAGING_SECS, 1.0f)); - } - - if (isMine) { - MyAvatar* myAvatar = static_cast(_owningAvatar); - - // Only use face trackers when not playing back a recording. - if (!myAvatar->isPlaying()) { - FaceTracker* faceTracker = Application::getInstance()->getActiveFaceTracker(); - _isFaceTrackerConnected = faceTracker != NULL && !faceTracker->isMuted(); - if (_isFaceTrackerConnected) { - _blendshapeCoefficients = faceTracker->getBlendshapeCoefficients(); - - if (typeid(*faceTracker) == typeid(DdeFaceTracker)) { - - if (Menu::getInstance()->isOptionChecked(MenuOption::UseAudioForMouth)) { - calculateMouthShapes(); - - const int JAW_OPEN_BLENDSHAPE = 21; - const int MMMM_BLENDSHAPE = 34; - const int FUNNEL_BLENDSHAPE = 40; - const int SMILE_LEFT_BLENDSHAPE = 28; - const int SMILE_RIGHT_BLENDSHAPE = 29; - _blendshapeCoefficients[JAW_OPEN_BLENDSHAPE] += _audioJawOpen; - _blendshapeCoefficients[SMILE_LEFT_BLENDSHAPE] += _mouth4; - _blendshapeCoefficients[SMILE_RIGHT_BLENDSHAPE] += _mouth4; - _blendshapeCoefficients[MMMM_BLENDSHAPE] += _mouth2; - _blendshapeCoefficients[FUNNEL_BLENDSHAPE] += _mouth3; - } - - applyEyelidOffset(getFinalOrientationInWorldFrame()); - } - } - } - // Twist the upper body to follow the rotation of the head, but only do this with my avatar, - // since everyone else will see the full joint rotations for other people. - const float BODY_FOLLOW_HEAD_YAW_RATE = 0.1f; - const float BODY_FOLLOW_HEAD_FACTOR = 0.66f; - float currentTwist = getTorsoTwist(); - setTorsoTwist(currentTwist + (getFinalYaw() * BODY_FOLLOW_HEAD_FACTOR - currentTwist) * BODY_FOLLOW_HEAD_YAW_RATE); - } - - if (!(_isFaceTrackerConnected || billboard)) { - // Update eye saccades - const float AVERAGE_MICROSACCADE_INTERVAL = 1.0f; - const float AVERAGE_SACCADE_INTERVAL = 6.0f; - const float MICROSACCADE_MAGNITUDE = 0.002f; - const float SACCADE_MAGNITUDE = 0.04f; - const float NOMINAL_FRAME_RATE = 60.0f; - - if (randFloat() < deltaTime / AVERAGE_MICROSACCADE_INTERVAL) { - _saccadeTarget = MICROSACCADE_MAGNITUDE * randVector(); - } else if (randFloat() < deltaTime / AVERAGE_SACCADE_INTERVAL) { - _saccadeTarget = SACCADE_MAGNITUDE * randVector(); - } - _saccade += (_saccadeTarget - _saccade) * pow(0.5f, NOMINAL_FRAME_RATE * deltaTime); - - // Detect transition from talking to not; force blink after that and a delay - bool forceBlink = false; - const float TALKING_LOUDNESS = 100.0f; - const float BLINK_AFTER_TALKING = 0.25f; - if ((_averageLoudness - _longTermAverageLoudness) > TALKING_LOUDNESS) { - _timeWithoutTalking = 0.0f; - - } else if (_timeWithoutTalking < BLINK_AFTER_TALKING && (_timeWithoutTalking += deltaTime) >= BLINK_AFTER_TALKING) { - forceBlink = true; - } - - // Update audio attack data for facial animation (eyebrows and mouth) - const float AUDIO_ATTACK_AVERAGING_RATE = 0.9f; - _audioAttack = AUDIO_ATTACK_AVERAGING_RATE * _audioAttack + (1.0f - AUDIO_ATTACK_AVERAGING_RATE) * fabs((_audioLoudness - _longTermAverageLoudness) - _lastLoudness); - _lastLoudness = (_audioLoudness - _longTermAverageLoudness); - - const float BROW_LIFT_THRESHOLD = 100.0f; - if (_audioAttack > BROW_LIFT_THRESHOLD) { - _browAudioLift += sqrtf(_audioAttack) * 0.01f; - } - _browAudioLift = glm::clamp(_browAudioLift *= 0.7f, 0.0f, 1.0f); - - const float BLINK_SPEED = 10.0f; - const float BLINK_SPEED_VARIABILITY = 1.0f; - const float BLINK_START_VARIABILITY = 0.25f; - const float FULLY_OPEN = 0.0f; - const float FULLY_CLOSED = 1.0f; - if (_leftEyeBlinkVelocity == 0.0f && _rightEyeBlinkVelocity == 0.0f) { - // no blinking when brows are raised; blink less with increasing loudness - const float BASE_BLINK_RATE = 15.0f / 60.0f; - const float ROOT_LOUDNESS_TO_BLINK_INTERVAL = 0.25f; - if (forceBlink || (_browAudioLift < EPSILON && shouldDo(glm::max(1.0f, sqrt(fabs(_averageLoudness - _longTermAverageLoudness)) * - ROOT_LOUDNESS_TO_BLINK_INTERVAL) / BASE_BLINK_RATE, deltaTime))) { - _leftEyeBlinkVelocity = BLINK_SPEED + randFloat() * BLINK_SPEED_VARIABILITY; - _rightEyeBlinkVelocity = BLINK_SPEED + randFloat() * BLINK_SPEED_VARIABILITY; - if (randFloat() < 0.5f) { - _leftEyeBlink = BLINK_START_VARIABILITY; - } else { - _rightEyeBlink = BLINK_START_VARIABILITY; - } - } - } else { - _leftEyeBlink = glm::clamp(_leftEyeBlink + _leftEyeBlinkVelocity * deltaTime, FULLY_OPEN, FULLY_CLOSED); - _rightEyeBlink = glm::clamp(_rightEyeBlink + _rightEyeBlinkVelocity * deltaTime, FULLY_OPEN, FULLY_CLOSED); - - if (_leftEyeBlink == FULLY_CLOSED) { - _leftEyeBlinkVelocity = -BLINK_SPEED; - - } else if (_leftEyeBlink == FULLY_OPEN) { - _leftEyeBlinkVelocity = 0.0f; - } - if (_rightEyeBlink == FULLY_CLOSED) { - _rightEyeBlinkVelocity = -BLINK_SPEED; - - } else if (_rightEyeBlink == FULLY_OPEN) { - _rightEyeBlinkVelocity = 0.0f; - } - } - - // use data to update fake Faceshift blendshape coefficients - calculateMouthShapes(); - DependencyManager::get()->updateFakeCoefficients(_leftEyeBlink, - _rightEyeBlink, - _browAudioLift, - _audioJawOpen, - _mouth2, - _mouth3, - _mouth4, - _blendshapeCoefficients); - - applyEyelidOffset(getOrientation()); - - } else { - _saccade = glm::vec3(); - } - - if (!isMine) { - _faceModel.setLODDistance(static_cast(_owningAvatar)->getLODDistance()); - } - _leftEyePosition = _rightEyePosition = getPosition(); - if (!billboard) { - _faceModel.simulate(deltaTime); - if (!_faceModel.getEyePositions(_leftEyePosition, _rightEyePosition)) { - static_cast(_owningAvatar)->getSkeletonModel().getEyePositions(_leftEyePosition, _rightEyePosition); - } - } - _eyePosition = calculateAverageEyePosition(); -} - -void Head::calculateMouthShapes() { - const float JAW_OPEN_SCALE = 0.015f; - const float JAW_OPEN_RATE = 0.9f; - const float JAW_CLOSE_RATE = 0.90f; - float audioDelta = sqrtf(glm::max(_averageLoudness - _longTermAverageLoudness, 0.0f)) * JAW_OPEN_SCALE; - if (audioDelta > _audioJawOpen) { - _audioJawOpen += (audioDelta - _audioJawOpen) * JAW_OPEN_RATE; - } else { - _audioJawOpen *= JAW_CLOSE_RATE; - } - _audioJawOpen = glm::clamp(_audioJawOpen, 0.0f, 1.0f); - - // _mouth2 = "mmmm" shape - // _mouth3 = "funnel" shape - // _mouth4 = "smile" shape - const float FUNNEL_PERIOD = 0.985f; - const float FUNNEL_RANDOM_PERIOD = 0.01f; - const float MMMM_POWER = 0.25f; - const float MMMM_PERIOD = 0.91f; - const float MMMM_RANDOM_PERIOD = 0.15f; - const float SMILE_PERIOD = 0.925f; - const float SMILE_RANDOM_PERIOD = 0.05f; - - _mouth3 = glm::mix(_audioJawOpen, _mouth3, FUNNEL_PERIOD + randFloat() * FUNNEL_RANDOM_PERIOD); - _mouth2 = glm::mix(_audioJawOpen * MMMM_POWER, _mouth2, MMMM_PERIOD + randFloat() * MMMM_RANDOM_PERIOD); - _mouth4 = glm::mix(_audioJawOpen, _mouth4, SMILE_PERIOD + randFloat() * SMILE_RANDOM_PERIOD); -} - -void Head::applyEyelidOffset(glm::quat headOrientation) { - // Adjusts the eyelid blendshape coefficients so that the eyelid follows the iris as the head pitches. - - glm::quat eyeRotation = rotationBetween(headOrientation * IDENTITY_FRONT, getCorrectedLookAtPosition() - _eyePosition); - eyeRotation = eyeRotation * glm::angleAxis(safeEulerAngles(headOrientation).y, IDENTITY_UP); // Rotation w.r.t. head - float eyePitch = safeEulerAngles(eyeRotation).x; - - const float EYE_PITCH_TO_COEFFICIENT = 1.6f; // Empirically determined - const float MAX_EYELID_OFFSET = 0.8f; // So that don't fully close eyes when looking way down - float eyelidOffset = glm::clamp(-eyePitch * EYE_PITCH_TO_COEFFICIENT, -1.0f, MAX_EYELID_OFFSET); - - for (int i = 0; i < 2; i++) { - const int LEFT_EYE = 8; - float eyeCoefficient = _blendshapeCoefficients[i] - _blendshapeCoefficients[LEFT_EYE + i]; // Raw value - eyeCoefficient = glm::clamp(eyelidOffset + eyeCoefficient * (1.0f - eyelidOffset), -1.0f, 1.0f); - if (eyeCoefficient > 0.0f) { - _blendshapeCoefficients[i] = eyeCoefficient; - _blendshapeCoefficients[LEFT_EYE + i] = 0.0f; - - } else { - _blendshapeCoefficients[i] = 0.0f; - _blendshapeCoefficients[LEFT_EYE + i] = -eyeCoefficient; - } - } -} - -void Head::relaxLean(float deltaTime) { - // restore rotation, lean to neutral positions - const float LEAN_RELAXATION_PERIOD = 0.25f; // seconds - float relaxationFactor = 1.0f - glm::min(deltaTime / LEAN_RELAXATION_PERIOD, 1.0f); - _deltaYaw *= relaxationFactor; - _deltaPitch *= relaxationFactor; - _deltaRoll *= relaxationFactor; - _leanSideways *= relaxationFactor; - _leanForward *= relaxationFactor; - _deltaLeanSideways *= relaxationFactor; - _deltaLeanForward *= relaxationFactor; -} - -void Head::render(RenderArgs* renderArgs, float alpha, ViewFrustum* renderFrustum) { - if (_renderLookatVectors) { - renderLookatVectors(renderArgs, _leftEyePosition, _rightEyePosition, getCorrectedLookAtPosition()); - } -} - -void Head::setScale (float scale) { - if (_scale == scale) { - return; - } - _scale = scale; -} - -glm::quat Head::getFinalOrientationInWorldFrame() const { - return _owningAvatar->getOrientation() * getFinalOrientationInLocalFrame(); -} - -glm::quat Head::getFinalOrientationInLocalFrame() const { - return glm::quat(glm::radians(glm::vec3(getFinalPitch(), getFinalYaw(), getFinalRoll() ))); -} - -glm::vec3 Head::getCorrectedLookAtPosition() { - if (isLookingAtMe()) { - return _correctedLookAtPosition; - } else { - return getLookAtPosition(); - } -} - -void Head::setCorrectedLookAtPosition(glm::vec3 correctedLookAtPosition) { - if (!isLookingAtMe()) { - _lookingAtMeStarted = usecTimestampNow(); - } - _isLookingAtMe = true; - _wasLastLookingAtMe = usecTimestampNow(); - _correctedLookAtPosition = correctedLookAtPosition; -} - -bool Head::isLookingAtMe() { - // Allow for outages such as may be encountered during avatar movement - quint64 now = usecTimestampNow(); - const quint64 LOOKING_AT_ME_GAP_ALLOWED = 1000000; // microseconds - return _isLookingAtMe || (now - _wasLastLookingAtMe) < LOOKING_AT_ME_GAP_ALLOWED; -} - -glm::quat Head::getCameraOrientation() const { - // NOTE: Head::getCameraOrientation() is not used for orienting the camera "view" while in Oculus mode, so - // you may wonder why this code is here. This method will be called while in Oculus mode to determine how - // to change the driving direction while in Oculus mode. It is used to support driving toward where you're - // head is looking. Note that in oculus mode, your actual camera view and where your head is looking is not - // always the same. - if (qApp->isHMDMode()) { - return getOrientation(); - } - Avatar* owningAvatar = static_cast(_owningAvatar); - return owningAvatar->getWorldAlignedOrientation() * glm::quat(glm::radians(glm::vec3(_basePitch, 0.0f, 0.0f))); -} - -glm::quat Head::getEyeRotation(const glm::vec3& eyePosition) const { - glm::quat orientation = getOrientation(); - glm::vec3 lookAtDelta = _lookAtPosition - eyePosition; - return rotationBetween(orientation * IDENTITY_FRONT, lookAtDelta + glm::length(lookAtDelta) * _saccade) * orientation; -} - -glm::vec3 Head::getScalePivot() const { - return _faceModel.isActive() ? _faceModel.getTranslation() : _position; -} - -void Head::setFinalPitch(float finalPitch) { - _deltaPitch = glm::clamp(finalPitch, MIN_HEAD_PITCH, MAX_HEAD_PITCH) - _basePitch; -} - -void Head::setFinalYaw(float finalYaw) { - _deltaYaw = glm::clamp(finalYaw, MIN_HEAD_YAW, MAX_HEAD_YAW) - _baseYaw; -} - -void Head::setFinalRoll(float finalRoll) { - _deltaRoll = glm::clamp(finalRoll, MIN_HEAD_ROLL, MAX_HEAD_ROLL) - _baseRoll; -} - -float Head::getFinalYaw() const { - return glm::clamp(_baseYaw + _deltaYaw, MIN_HEAD_YAW, MAX_HEAD_YAW); -} - -float Head::getFinalPitch() const { - return glm::clamp(_basePitch + _deltaPitch, MIN_HEAD_PITCH, MAX_HEAD_PITCH); -} - -float Head::getFinalRoll() const { - return glm::clamp(_baseRoll + _deltaRoll, MIN_HEAD_ROLL, MAX_HEAD_ROLL); -} - -void Head::addLeanDeltas(float sideways, float forward) { - _deltaLeanSideways += sideways; - _deltaLeanForward += forward; -} - -void Head::renderLookatVectors(RenderArgs* renderArgs, glm::vec3 leftEyePosition, glm::vec3 rightEyePosition, glm::vec3 lookatPosition) { - auto& batch = *renderArgs->_batch; - auto transform = Transform{}; - batch.setModelTransform(transform); - batch._glLineWidth(2.0f); - - auto deferredLighting = DependencyManager::get(); - deferredLighting->bindSimpleProgram(batch); - - auto geometryCache = DependencyManager::get(); - glm::vec4 startColor(0.2f, 0.2f, 0.2f, 1.0f); - glm::vec4 endColor(1.0f, 1.0f, 1.0f, 0.0f); - geometryCache->renderLine(batch, leftEyePosition, lookatPosition, startColor, endColor, _leftEyeLookAtID); - geometryCache->renderLine(batch, rightEyePosition, lookatPosition, startColor, endColor, _rightEyeLookAtID); -} - - From 1bb734aec0bbf6e1eb4e0b64627832307d6d8e16 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 27 Jul 2015 15:26:03 -0700 Subject: [PATCH 111/242] Kill warnings. --- interface/src/avatar/Head.cpp | 2 +- interface/src/avatar/SkeletonModel.cpp | 6 +++++- interface/src/avatar/SkeletonModel.h | 3 ++- libraries/animation/src/Rig.h | 2 +- libraries/render-utils/src/Model.cpp | 8 ++++---- libraries/render-utils/src/Model.h | 5 +++-- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index b9c453421b..bfa2e437a1 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -56,9 +56,9 @@ Head::Head(Avatar* owningAvatar) : _deltaLeanForward(0.0f), _isCameraMoving(false), _isLookingAtMe(false), - _faceModel(this, std::make_shared()), _lookingAtMeStarted(0), _wasLastLookingAtMe(0), + _faceModel(this, std::make_shared()), _leftEyeLookAtID(DependencyManager::get()->allocateID()), _rightEyeLookAtID(DependencyManager::get()->allocateID()) { diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 798b3c628d..7b6fb85ae5 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -115,6 +115,10 @@ void SkeletonModel::updateClusterMatrices() { } } +void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { + _rig->simulateInternal(deltaTime, parentTransform, _owningAvatar->getPosition(), _owningAvatar->getVelocity(), _owningAvatar->getOrientation()); +} + void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { setTranslation(_owningAvatar->getSkeletonPosition()); static const glm::quat refOrientation = glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); @@ -122,7 +126,7 @@ void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { setScale(glm::vec3(1.0f, 1.0f, 1.0f) * _owningAvatar->getScale()); setBlendshapeCoefficients(_owningAvatar->getHead()->getBlendshapeCoefficients()); - Model::simulate(deltaTime, fullUpdate, _owningAvatar->getPosition(), _owningAvatar->getVelocity(), _owningAvatar->getOrientation()); + Model::simulate(deltaTime, fullUpdate); if (!isActive() || !_owningAvatar->isMyAvatar()) { return; // only simulate for own avatar diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 89c7ab1fcb..10b811415e 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -29,7 +29,8 @@ public: virtual void initJointStates(QVector states); - void simulate(float deltaTime, bool fullUpdate = true); + virtual void simulate(float deltaTime, bool fullUpdate = true); + virtual void updateRig(float deltaTime, glm::mat4 parentTransform); void renderIKConstraints(gpu::Batch& batch); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 379ed64877..8b2fc10fa5 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -132,7 +132,7 @@ public: virtual bool getIsFirstPerson() const { return _isFirstPerson; } bool getJointsAreDirty() { return _jointsAreDirty; } - bool setEnableRig(bool isEnabled) { _enableRig = isEnabled; } + void setEnableRig(bool isEnabled) { _enableRig = isEnabled; } protected: QVector _jointStates; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index dc1051dad3..dd2d3369c8 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1301,7 +1301,7 @@ void Model::snapToRegistrationPoint() { _snappedToRegistrationPoint = true; } -void Model::simulate(float deltaTime, bool fullUpdate, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation) { +void Model::simulate(float deltaTime, bool fullUpdate) { PROFILE_RANGE(__FUNCTION__); fullUpdate = updateGeometry() || fullUpdate || (_scaleToFit && !_scaledToFit) || (_snapModelToRegistrationPoint && !_snappedToRegistrationPoint); @@ -1322,16 +1322,16 @@ void Model::simulate(float deltaTime, bool fullUpdate, const glm::vec3& worldPos if (_snapModelToRegistrationPoint && !_snappedToRegistrationPoint) { snapToRegistrationPoint(); } - simulateInternal(deltaTime, worldPosition, worldVelocity, worldRotation); + simulateInternal(deltaTime); } } -void Model::simulateInternal(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation) { +void Model::simulateInternal(float deltaTime) { // update the world space transforms for all joints const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - _rig->simulateInternal(deltaTime, parentTransform, worldPosition, worldVelocity, worldRotation); + updateRig(deltaTime, parentTransform); glm::mat4 modelToWorld = glm::mat4_cast(_rotation); for (int i = 0; i < _meshStates.size(); i++) { diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index a7b81abea9..27ef808ef0 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -115,7 +115,7 @@ public: void setSnapModelToRegistrationPoint(bool snapModelToRegistrationPoint, const glm::vec3& registrationPoint); bool getSnapModelToRegistrationPoint() { return _snapModelToRegistrationPoint; } - virtual void simulate(float deltaTime, bool fullUpdate = true, const glm::vec3& worldPosition = glm::vec3(), const glm::vec3& worldVelocity = glm::vec3(), const glm::quat& worldRotation = glm::quat()); + virtual void simulate(float deltaTime, bool fullUpdate = true); /// Returns a reference to the shared geometry. const QSharedPointer& getGeometry() const { return _geometry; } @@ -268,7 +268,8 @@ protected: void scaleToFit(); void snapToRegistrationPoint(); - void simulateInternal(float deltaTime, const glm::vec3& worldPosition = glm::vec3(), const glm::vec3& worldVelocity = glm::vec3(), const glm::quat& worldRotation = glm::quat()); + void simulateInternal(float deltaTime); + virtual void updateRig(float deltaTime, glm::mat4 parentTransform) {}; // Subclasses may be more interesting /// Updates the state of the joint at the specified index. virtual void updateJointState(int index); From e527f49d6563e09673e923d8b64cac6b60089b8a Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 27 Jul 2015 15:30:10 -0700 Subject: [PATCH 112/242] removing interface dependency on ShapeCollider --- interface/src/avatar/MyAvatar.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index cdd7bf2b66..42c1d15db7 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -29,7 +29,6 @@ #include #include #include -#include #include #include #include @@ -105,7 +104,6 @@ MyAvatar::MyAvatar(RigPointer rig) : _rig(rig), _prevShouldDrawHead(true) { - ShapeCollider::initDispatchTable(); for (int i = 0; i < MAX_DRIVE_KEYS; i++) { _driveKeys[i] = 0.0f; } From c7f367bfd7ac3923a05f66f0ae1942572c1c3d15 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Mon, 27 Jul 2015 15:30:37 -0700 Subject: [PATCH 113/242] removing unnecessary #include --- libraries/fbx/src/FBXReader.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 507e7938de..4ba47d4b46 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -27,7 +27,6 @@ #include #include #include -#include #include #include From 5ae15d46b40877c6cac412382ef6bbda56e26637 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 27 Jul 2015 16:42:54 -0700 Subject: [PATCH 114/242] add some javascript callable stuff back to MyAvatar --- interface/src/avatar/MyAvatar.cpp | 29 ++++++++++++++++++- interface/src/avatar/MyAvatar.h | 5 ++++ interface/src/avatar/SkeletonModel.h | 42 ++++++++++++++-------------- libraries/render-utils/src/Model.cpp | 4 --- libraries/render-utils/src/Model.h | 5 ---- 5 files changed, 54 insertions(+), 31 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index fe4d4bc3cb..f29c21f114 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -900,10 +900,37 @@ glm::vec3 MyAvatar::getDefaultEyePosition() const { const float SCRIPT_PRIORITY = DEFAULT_PRIORITY + 1.0f; const float RECORDER_PRIORITY = SCRIPT_PRIORITY + 1.0f; +void MyAvatar::setJointRotations(QVector jointRotations) { + int numStates = glm::min(_skeletonModel.getJointStateCount(), jointRotations.size()); + for (int i = 0; i < numStates; ++i) { + // HACK: ATM only Recorder calls setJointRotations() so we hardcode its priority here + _skeletonModel.setJointState(i, true, jointRotations[i], RECORDER_PRIORITY); + } +} + +void MyAvatar::setJointData(int index, const glm::quat& rotation) { + if (QThread::currentThread() == thread()) { + // HACK: ATM only JS scripts call setJointData() on MyAvatar so we hardcode the priority + _rig->setJointState(index, true, rotation, SCRIPT_PRIORITY); + } +} + +void MyAvatar::clearJointData(int index) { + if (QThread::currentThread() == thread()) { + // HACK: ATM only JS scripts call clearJointData() on MyAvatar so we hardcode the priority + _rig->setJointState(index, false, glm::quat(), 0.0f); + _rig->clearJointAnimationPriority(index); + } +} + +void MyAvatar::clearJointsData() { + clearJointAnimationPriorities(); +} + void MyAvatar::clearJointAnimationPriorities() { int numStates = _skeletonModel.getJointStateCount(); for (int i = 0; i < numStates; ++i) { - _skeletonModel.clearJointAnimationPriority(i); + _rig->clearJointAnimationPriority(i); } } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index e83deddf16..4dfe0611af 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -104,6 +104,11 @@ public: void updateLookAtTargetAvatar(); void clearLookAtTargetAvatar(); + virtual void setJointRotations(QVector jointRotations); + virtual void setJointData(int index, const glm::quat& rotation); + virtual void clearJointData(int index); + virtual void clearJointsData(); + Q_INVOKABLE void useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName = QString()); Q_INVOKABLE void useHeadURL(const QUrl& headURL, const QString& modelName = QString()); Q_INVOKABLE void useBodyURL(const QUrl& bodyURL, const QString& modelName = QString()); diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 14dc2da3f0..5d76ac1149 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -22,68 +22,68 @@ class MuscleConstraint; /// A skeleton loaded from a model. class SkeletonModel : public Model { Q_OBJECT - + public: SkeletonModel(Avatar* owningAvatar, QObject* parent = nullptr, RigPointer rig = nullptr); ~SkeletonModel(); - + virtual void initJointStates(QVector states); void simulate(float deltaTime, bool fullUpdate = true); void renderIKConstraints(gpu::Batch& batch); - + /// Returns the index of the left hand joint, or -1 if not found. int getLeftHandJointIndex() const { return isActive() ? _geometry->getFBXGeometry().leftHandJointIndex : -1; } - + /// Returns the index of the right hand joint, or -1 if not found. int getRightHandJointIndex() const { return isActive() ? _geometry->getFBXGeometry().rightHandJointIndex : -1; } /// Retrieve the position of the left hand /// \return true whether or not the position was found bool getLeftHandPosition(glm::vec3& position) const; - + /// Retrieve the position of the right hand /// \return true whether or not the position was found bool getRightHandPosition(glm::vec3& position) const; - + /// Restores some fraction of the default position of the left hand. /// \param fraction the fraction of the default position to restore /// \return whether or not the left hand joint was found bool restoreLeftHandPosition(float fraction = 1.0f, float priority = 1.0f); - + /// Gets the position of the left shoulder. /// \return whether or not the left shoulder joint was found bool getLeftShoulderPosition(glm::vec3& position) const; - + /// Returns the extended length from the left hand to its last free ancestor. float getLeftArmLength() const; - + /// Restores some fraction of the default position of the right hand. /// \param fraction the fraction of the default position to restore /// \return whether or not the right hand joint was found bool restoreRightHandPosition(float fraction = 1.0f, float priority = 1.0f); - + /// Gets the position of the right shoulder. /// \return whether or not the right shoulder joint was found bool getRightShoulderPosition(glm::vec3& position) const; - + /// Returns the extended length from the right hand to its first free ancestor. float getRightArmLength() const; /// Returns the position of the head joint. /// \return whether or not the head was found bool getHeadPosition(glm::vec3& headPosition) const; - + /// Returns the position of the neck joint. /// \return whether or not the neck was found bool getNeckPosition(glm::vec3& neckPosition) const; - + /// Returns the rotation of the neck joint's parent from default orientation /// \return whether or not the neck was found bool getNeckParentRotationFromDefaultOrientation(glm::quat& neckParentRotation) const; - + /// Retrieve the positions of up to two eye meshes. /// \return whether or not both eye meshes were found bool getEyePositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const; @@ -119,12 +119,12 @@ protected: /// \param jointIndex index of joint in model /// \param position position of joint in model-frame void applyHandPosition(int jointIndex, const glm::vec3& position); - + void applyPalmData(int jointIndex, PalmData& palm); - + /// Updates the state of the joint at the specified index. - virtual void updateJointState(int index); - + virtual void updateJointState(int index); + void maybeUpdateLeanRotation(const JointState& parentState, int index); void maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, int index); void maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, int index); @@ -137,9 +137,9 @@ protected: private: void renderJointConstraints(gpu::Batch& batch, int jointIndex); - void renderOrientationDirections(gpu::Batch& batch, int jointIndex, + void renderOrientationDirections(gpu::Batch& batch, int jointIndex, glm::vec3 position, const glm::quat& orientation, float size); - + struct OrientationLineIDs { int _up; int _front; @@ -154,7 +154,7 @@ private: void setHandPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation); bool getEyeModelPositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const; - + Avatar* _owningAvatar; CapsuleShape _boundingShape; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index a316da0f99..1f7b67ef05 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1052,10 +1052,6 @@ void Model::clearJointState(int index) { _rig->clearJointState(index); } -void Model::clearJointAnimationPriority(int index) { - _rig->clearJointAnimationPriority(index); -} - void Model::setJointState(int index, bool valid, const glm::quat& rotation, float priority) { _rig->setJointState(index, valid, rotation, priority); } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 45d7ce63ab..fc8347581b 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -21,7 +21,6 @@ #include #include -#include #include #include #include @@ -150,10 +149,6 @@ public: /// Sets the distance parameter used for LOD computations. void setLODDistance(float distance) { _lodDistance = distance; } - const QList& getRunningAnimations() const { return _rig->getRunningAnimations(); } - /// Clear the joint animation priority - void clearJointAnimationPriority(int index); - void setScaleToFit(bool scaleToFit, float largestDimension = 0.0f, bool forceRescale = false); bool getScaleToFit() const { return _scaleToFit; } /// is scale to fit enabled From 34e400ac67a04983976ec276711ec39ff3816fdf Mon Sep 17 00:00:00 2001 From: Niraj Venkat Date: Mon, 27 Jul 2015 17:14:09 -0700 Subject: [PATCH 115/242] Changing var name to 'DebugAmbientOcclusion' --- interface/src/Application.cpp | 2 +- interface/src/Menu.cpp | 2 +- interface/src/Menu.h | 2 +- tests/ui/src/main.cpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9d28e40a66..093f87b494 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3265,7 +3265,7 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se renderContext._drawItemStatus = sceneInterface->doEngineDisplayItemStatus(); - renderContext._occlusionStatus = Menu::getInstance()->isOptionChecked(MenuOption::AmbientOcclusion); + renderContext._occlusionStatus = Menu::getInstance()->isOptionChecked(MenuOption::DebugAmbientOcclusion); renderArgs->_shouldRender = LODManager::shouldRender; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 91ae6a4d02..ea284d4f23 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -330,7 +330,7 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Atmosphere, 0, // QML Qt::SHIFT | Qt::Key_A, true); - addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::AmbientOcclusion); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::DebugAmbientOcclusion); MenuWrapper* ambientLightMenu = renderOptionsMenu->addMenu(MenuOption::RenderAmbientLight); QActionGroup* ambientLightGroup = new QActionGroup(ambientLightMenu); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 8ad71bbdaa..4c3e9332a1 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -134,7 +134,6 @@ namespace MenuOption { const QString AddressBar = "Show Address Bar"; const QString AlignForearmsWithWrists = "Align Forearms with Wrists"; const QString AlternateIK = "Alternate IK"; - const QString AmbientOcclusion = "Debug Ambient Occlusion"; const QString Animations = "Animations..."; const QString Atmosphere = "Atmosphere"; const QString Attachments = "Attachments..."; @@ -165,6 +164,7 @@ namespace MenuOption { const QString ControlWithSpeech = "Control With Speech"; const QString CopyAddress = "Copy Address to Clipboard"; const QString CopyPath = "Copy Path to Clipboard"; + const QString DebugAmbientOcclusion = "Debug Ambient Occlusion"; const QString DecreaseAvatarSize = "Decrease Avatar Size"; const QString DeleteBookmark = "Delete Bookmark..."; const QString DisableActivityLogger = "Disable Activity Logger"; diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index f5647bd176..919c27f32f 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -86,7 +86,6 @@ public: AddressBar, AlignForearmsWithWrists, AlternateIK, - AmbientOcclusion, Animations, Atmosphere, Attachments, @@ -111,6 +110,7 @@ public: ControlWithSpeech, CopyAddress, CopyPath, + DebugAmbientOcclusion, DecreaseAvatarSize, DeleteBookmark, DisableActivityLogger, From f525a8a245dd9f3f1fa647d3865f0e24ba88d683 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 27 Jul 2015 17:17:56 -0700 Subject: [PATCH 116/242] Removing all the unecessary calls of Batch from the gl legacy time --- interface/src/Application.cpp | 12 +- interface/src/Stars.cpp | 5 +- interface/src/ui/ApplicationOverlay.cpp | 2 +- .../src/RenderableWebEntityItem.cpp | 3 +- libraries/gpu/src/gpu/Batch.h | 52 +--- libraries/gpu/src/gpu/GLBackend.cpp | 250 +----------------- libraries/gpu/src/gpu/GLBackend.h | 25 +- libraries/gpu/src/gpu/GLBackendOutput.cpp | 8 +- libraries/gpu/src/gpu/GLBackendState.cpp | 5 + 9 files changed, 34 insertions(+), 328 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7bfad60eaf..42868166d0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -995,11 +995,6 @@ void Application::paintGL() { _compositor.displayOverlayTexture(&renderArgs); } - - // Reset the gpu::Context Stages - gpu::Batch batch; - batch.resetStages(); - renderArgs._context->render(batch); if (!OculusManager::isConnected() || OculusManager::allowSwap()) { @@ -1013,6 +1008,13 @@ void Application::paintGL() { _frameCount++; _numFramesSinceLastResize++; Stats::getInstance()->setRenderDetails(renderArgs._details); + + + // Reset the gpu::Context Stages + // Back to the default framebuffer; + gpu::Batch batch; + batch.resetStages(); + renderArgs._context->render(batch); } void Application::runTests() { diff --git a/interface/src/Stars.cpp b/interface/src/Stars.cpp index ebfddee38c..7d09b9b1c3 100644 --- a/interface/src/Stars.cpp +++ b/interface/src/Stars.cpp @@ -154,6 +154,7 @@ void Stars::render(RenderArgs* renderArgs, float alpha) { auto state = gpu::StatePointer(new gpu::State()); // enable decal blend state->setDepthTest(gpu::State::DepthTest(false)); + state->setAntialiasedLineEnable(true); // line smoothing also smooth points state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); _starsPipeline.reset(gpu::Pipeline::create(program, state)); @@ -217,10 +218,10 @@ void Stars::render(RenderArgs* renderArgs, float alpha) { // Render the stars batch.setPipeline(_starsPipeline); - batch._glEnable(GL_PROGRAM_POINT_SIZE_EXT); + /* batch._glEnable(GL_PROGRAM_POINT_SIZE_EXT); batch._glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); batch._glEnable(GL_POINT_SMOOTH); - + */ batch.setInputFormat(streamFormat); batch.setInputBuffer(VERTICES_SLOT, posView); batch.setInputBuffer(COLOR_SLOT, colView); diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index bc10b555e4..acc24ba2be 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -117,7 +117,7 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) { batch.setProjectionTransform(mat4()); batch.setModelTransform(Transform()); batch.setViewTransform(Transform()); - batch._glBindTexture(GL_TEXTURE_2D, _uiTexture); + batch._glActiveBindTexture(GL_TEXTURE0, GL_TEXTURE_2D, _uiTexture); geometryCache->renderUnitQuad(batch, glm::vec4(1)); } diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 7fa615073b..ee1fdfae45 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -178,8 +178,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) { batch.setModelTransform(getTransformToCenter()); bool textured = false, culled = false, emissive = false; if (_texture) { - batch._glActiveTexture(GL_TEXTURE0); - batch._glBindTexture(GL_TEXTURE_2D, _texture); + batch._glActiveBindTexture(GL_TEXTURE0, GL_TEXTURE_2D, _texture); textured = emissive = true; } diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index 5f2c2dd089..5135d1ac4f 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -94,7 +94,7 @@ public: void setResourceTexture(uint32 slot, const TexturePointer& view); void setResourceTexture(uint32 slot, const TextureView& view); // not a command, just a shortcut from a TextureView - // Framebuffer Stage + // Ouput Stage void setFramebuffer(const FramebufferPointer& framebuffer); // Clear framebuffer layers @@ -122,28 +122,8 @@ public: // For now, instead of calling the raw gl Call, use the equivalent call on the batch so the call is beeing recorded // THe implementation of these functions is in GLBackend.cpp - void _glEnable(unsigned int cap); - void _glDisable(unsigned int cap); + void _glActiveBindTexture(unsigned int unit, unsigned int target, unsigned int texture); - void _glEnableClientState(unsigned int array); - void _glDisableClientState(unsigned int array); - - void _glCullFace(unsigned int mode); - void _glAlphaFunc(unsigned int func, float ref); - - void _glDepthFunc(unsigned int func); - void _glDepthMask(unsigned char flag); - void _glDepthRange(float zNear, float zFar); - - void _glBindBuffer(unsigned int target, unsigned int buffer); - - void _glBindTexture(unsigned int target, unsigned int texture); - void _glActiveTexture(unsigned int texture); - void _glTexParameteri(unsigned int target, unsigned int pname, int param); - - void _glDrawBuffers(int n, const unsigned int* bufs); - - void _glUseProgram(unsigned int program); void _glUniform1i(int location, int v0); void _glUniform1f(int location, float v0); void _glUniform2f(int location, float v0, float v1); @@ -153,9 +133,6 @@ public: void _glUniform4iv(int location, int count, const int* value); void _glUniformMatrix4fv(int location, int count, unsigned char transpose, const float* value); - void _glEnableVertexAttribArray(int location); - void _glDisableVertexAttribArray(int location); - void _glColor4f(float red, float green, float blue, float alpha); void _glLineWidth(float width); @@ -194,28 +171,8 @@ public: // TODO: As long as we have gl calls explicitely issued from interface // code, we need to be able to record and batch these calls. THe long // term strategy is to get rid of any GL calls in favor of the HIFI GPU API - COMMAND_glEnable, - COMMAND_glDisable, + COMMAND_glActiveBindTexture, - COMMAND_glEnableClientState, - COMMAND_glDisableClientState, - - COMMAND_glCullFace, - COMMAND_glAlphaFunc, - - COMMAND_glDepthFunc, - COMMAND_glDepthMask, - COMMAND_glDepthRange, - - COMMAND_glBindBuffer, - - COMMAND_glBindTexture, - COMMAND_glActiveTexture, - COMMAND_glTexParameteri, - - COMMAND_glDrawBuffers, - - COMMAND_glUseProgram, COMMAND_glUniform1i, COMMAND_glUniform1f, COMMAND_glUniform2f, @@ -225,9 +182,6 @@ public: COMMAND_glUniform4iv, COMMAND_glUniformMatrix4fv, - COMMAND_glEnableVertexAttribArray, - COMMAND_glDisableVertexAttribArray, - COMMAND_glColor4f, COMMAND_glLineWidth, diff --git a/libraries/gpu/src/gpu/GLBackend.cpp b/libraries/gpu/src/gpu/GLBackend.cpp index 51637462f1..8db192b827 100644 --- a/libraries/gpu/src/gpu/GLBackend.cpp +++ b/libraries/gpu/src/gpu/GLBackend.cpp @@ -48,28 +48,8 @@ GLBackend::CommandCall GLBackend::_commandCalls[Batch::NUM_COMMANDS] = (&::gpu::GLBackend::do_resetStages), - (&::gpu::GLBackend::do_glEnable), - (&::gpu::GLBackend::do_glDisable), + (&::gpu::GLBackend::do_glActiveBindTexture), - (&::gpu::GLBackend::do_glEnableClientState), - (&::gpu::GLBackend::do_glDisableClientState), - - (&::gpu::GLBackend::do_glCullFace), - (&::gpu::GLBackend::do_glAlphaFunc), - - (&::gpu::GLBackend::do_glDepthFunc), - (&::gpu::GLBackend::do_glDepthMask), - (&::gpu::GLBackend::do_glDepthRange), - - (&::gpu::GLBackend::do_glBindBuffer), - - (&::gpu::GLBackend::do_glBindTexture), - (&::gpu::GLBackend::do_glActiveTexture), - (&::gpu::GLBackend::do_glTexParameteri), - - (&::gpu::GLBackend::do_glDrawBuffers), - - (&::gpu::GLBackend::do_glUseProgram), (&::gpu::GLBackend::do_glUniform1i), (&::gpu::GLBackend::do_glUniform1f), (&::gpu::GLBackend::do_glUniform2f), @@ -79,9 +59,6 @@ GLBackend::CommandCall GLBackend::_commandCalls[Batch::NUM_COMMANDS] = (&::gpu::GLBackend::do_glUniform4iv), (&::gpu::GLBackend::do_glUniformMatrix4fv), - (&::gpu::GLBackend::do_glEnableVertexAttribArray), - (&::gpu::GLBackend::do_glDisableVertexAttribArray), - (&::gpu::GLBackend::do_glColor4f), (&::gpu::GLBackend::do_glLineWidth), }; @@ -275,211 +252,24 @@ void GLBackend::resetStages() { //#define DO_IT_NOW(call, offset) runLastCommand(); #define DO_IT_NOW(call, offset) - -void Batch::_glEnable(GLenum cap) { - ADD_COMMAND_GL(glEnable); - - _params.push_back(cap); - - DO_IT_NOW(_glEnable, 1); -} -void GLBackend::do_glEnable(Batch& batch, uint32 paramOffset) { - glEnable(batch._params[paramOffset]._uint); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glDisable(GLenum cap) { - ADD_COMMAND_GL(glDisable); - - _params.push_back(cap); - - DO_IT_NOW(_glDisable, 1); -} -void GLBackend::do_glDisable(Batch& batch, uint32 paramOffset) { - glDisable(batch._params[paramOffset]._uint); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glEnableClientState(GLenum array) { - ADD_COMMAND_GL(glEnableClientState); - - _params.push_back(array); - - DO_IT_NOW(_glEnableClientState, 1); -} -void GLBackend::do_glEnableClientState(Batch& batch, uint32 paramOffset) { - glEnableClientState(batch._params[paramOffset]._uint); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glDisableClientState(GLenum array) { - ADD_COMMAND_GL(glDisableClientState); - - _params.push_back(array); - - DO_IT_NOW(_glDisableClientState, 1); -} -void GLBackend::do_glDisableClientState(Batch& batch, uint32 paramOffset) { - glDisableClientState(batch._params[paramOffset]._uint); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glCullFace(GLenum mode) { - ADD_COMMAND_GL(glCullFace); - - _params.push_back(mode); - - DO_IT_NOW(_glCullFace, 1); -} -void GLBackend::do_glCullFace(Batch& batch, uint32 paramOffset) { - glCullFace(batch._params[paramOffset]._uint); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glAlphaFunc(GLenum func, GLclampf ref) { - ADD_COMMAND_GL(glAlphaFunc); - - _params.push_back(ref); - _params.push_back(func); - - DO_IT_NOW(_glAlphaFunc, 2); -} -void GLBackend::do_glAlphaFunc(Batch& batch, uint32 paramOffset) { - glAlphaFunc( - batch._params[paramOffset + 1]._uint, - batch._params[paramOffset + 0]._float); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glDepthFunc(GLenum func) { - ADD_COMMAND_GL(glDepthFunc); - - _params.push_back(func); - - DO_IT_NOW(_glDepthFunc, 1); -} -void GLBackend::do_glDepthFunc(Batch& batch, uint32 paramOffset) { - glDepthFunc(batch._params[paramOffset]._uint); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glDepthMask(GLboolean flag) { - ADD_COMMAND_GL(glDepthMask); - - _params.push_back(flag); - - DO_IT_NOW(_glDepthMask, 1); -} -void GLBackend::do_glDepthMask(Batch& batch, uint32 paramOffset) { - glDepthMask(batch._params[paramOffset]._uint); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glDepthRange(GLfloat zNear, GLfloat zFar) { - ADD_COMMAND_GL(glDepthRange); - - _params.push_back(zFar); - _params.push_back(zNear); - - DO_IT_NOW(_glDepthRange, 2); -} -void GLBackend::do_glDepthRange(Batch& batch, uint32 paramOffset) { - glDepthRange( - batch._params[paramOffset + 1]._float, - batch._params[paramOffset + 0]._float); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glBindBuffer(GLenum target, GLuint buffer) { - ADD_COMMAND_GL(glBindBuffer); - - _params.push_back(buffer); - _params.push_back(target); - - DO_IT_NOW(_glBindBuffer, 2); -} -void GLBackend::do_glBindBuffer(Batch& batch, uint32 paramOffset) { - glBindBuffer( - batch._params[paramOffset + 1]._uint, - batch._params[paramOffset + 0]._uint); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glBindTexture(GLenum target, GLuint texture) { - ADD_COMMAND_GL(glBindTexture); +void Batch::_glActiveBindTexture(GLenum unit, GLenum target, GLuint texture) { + ADD_COMMAND_GL(glActiveBindTexture); _params.push_back(texture); _params.push_back(target); + _params.push_back(unit); - DO_IT_NOW(_glBindTexture, 2); + + DO_IT_NOW(_glActiveBindTexture, 3); } -void GLBackend::do_glBindTexture(Batch& batch, uint32 paramOffset) { +void GLBackend::do_glActiveBindTexture(Batch& batch, uint32 paramOffset) { + glActiveTexture(batch._params[paramOffset + 2]._uint); glBindTexture( batch._params[paramOffset + 1]._uint, batch._params[paramOffset + 0]._uint); (void) CHECK_GL_ERROR(); } -void Batch::_glActiveTexture(GLenum texture) { - ADD_COMMAND_GL(glActiveTexture); - - _params.push_back(texture); - - DO_IT_NOW(_glActiveTexture, 1); -} -void GLBackend::do_glActiveTexture(Batch& batch, uint32 paramOffset) { - glActiveTexture(batch._params[paramOffset]._uint); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glTexParameteri(GLenum target, GLenum pname, GLint param) { - ADD_COMMAND_GL(glTexParameteri); - - _params.push_back(param); - _params.push_back(pname); - _params.push_back(target); - - DO_IT_NOW(glTexParameteri, 3); -} -void GLBackend::do_glTexParameteri(Batch& batch, uint32 paramOffset) { - glTexParameteri(batch._params[paramOffset + 2]._uint, - batch._params[paramOffset + 1]._uint, - batch._params[paramOffset + 0]._int); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glDrawBuffers(GLsizei n, const GLenum* bufs) { - ADD_COMMAND_GL(glDrawBuffers); - - _params.push_back(cacheData(n * sizeof(GLenum), bufs)); - _params.push_back(n); - - DO_IT_NOW(_glDrawBuffers, 2); -} -void GLBackend::do_glDrawBuffers(Batch& batch, uint32 paramOffset) { - glDrawBuffers( - batch._params[paramOffset + 1]._uint, - (const GLenum*)batch.editData(batch._params[paramOffset + 0]._uint)); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glUseProgram(GLuint program) { - ADD_COMMAND_GL(glUseProgram); - - _params.push_back(program); - - DO_IT_NOW(_glUseProgram, 1); -} -void GLBackend::do_glUseProgram(Batch& batch, uint32 paramOffset) { - - _pipeline._program = batch._params[paramOffset]._uint; - // for this call we still want to execute the glUseProgram in the order of the glCOmmand to avoid any issue - _pipeline._invalidProgram = false; - glUseProgram(_pipeline._program); - - (void) CHECK_GL_ERROR(); -} - void Batch::_glUniform1i(GLint location, GLint v0) { if (location < 0) { return; @@ -680,30 +470,6 @@ void GLBackend::do_glUniformMatrix4fv(Batch& batch, uint32 paramOffset) { (void) CHECK_GL_ERROR(); } -void Batch::_glEnableVertexAttribArray(GLint location) { - ADD_COMMAND_GL(glEnableVertexAttribArray); - - _params.push_back(location); - - DO_IT_NOW(_glEnableVertexAttribArray, 1); -} -void GLBackend::do_glEnableVertexAttribArray(Batch& batch, uint32 paramOffset) { - glEnableVertexAttribArray(batch._params[paramOffset]._uint); - (void) CHECK_GL_ERROR(); -} - -void Batch::_glDisableVertexAttribArray(GLint location) { - ADD_COMMAND_GL(glDisableVertexAttribArray); - - _params.push_back(location); - - DO_IT_NOW(_glDisableVertexAttribArray, 1); -} -void GLBackend::do_glDisableVertexAttribArray(Batch& batch, uint32 paramOffset) { - glDisableVertexAttribArray(batch._params[paramOffset]._uint); - (void) CHECK_GL_ERROR(); -} - void Batch::_glColor4f(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) { ADD_COMMAND_GL(glColor4f); diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index 59a6c1ee7e..843e5d2006 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -426,28 +426,8 @@ protected: // TODO: As long as we have gl calls explicitely issued from interface // code, we need to be able to record and batch these calls. THe long // term strategy is to get rid of any GL calls in favor of the HIFI GPU API - void do_glEnable(Batch& batch, uint32 paramOffset); - void do_glDisable(Batch& batch, uint32 paramOffset); + void do_glActiveBindTexture(Batch& batch, uint32 paramOffset); - void do_glEnableClientState(Batch& batch, uint32 paramOffset); - void do_glDisableClientState(Batch& batch, uint32 paramOffset); - - void do_glCullFace(Batch& batch, uint32 paramOffset); - void do_glAlphaFunc(Batch& batch, uint32 paramOffset); - - void do_glDepthFunc(Batch& batch, uint32 paramOffset); - void do_glDepthMask(Batch& batch, uint32 paramOffset); - void do_glDepthRange(Batch& batch, uint32 paramOffset); - - void do_glBindBuffer(Batch& batch, uint32 paramOffset); - - void do_glBindTexture(Batch& batch, uint32 paramOffset); - void do_glActiveTexture(Batch& batch, uint32 paramOffset); - void do_glTexParameteri(Batch& batch, uint32 paramOffset); - - void do_glDrawBuffers(Batch& batch, uint32 paramOffset); - - void do_glUseProgram(Batch& batch, uint32 paramOffset); void do_glUniform1i(Batch& batch, uint32 paramOffset); void do_glUniform1f(Batch& batch, uint32 paramOffset); void do_glUniform2f(Batch& batch, uint32 paramOffset); @@ -457,9 +437,6 @@ protected: void do_glUniform4iv(Batch& batch, uint32 paramOffset); void do_glUniformMatrix4fv(Batch& batch, uint32 paramOffset); - void do_glEnableVertexAttribArray(Batch& batch, uint32 paramOffset); - void do_glDisableVertexAttribArray(Batch& batch, uint32 paramOffset); - void do_glColor4f(Batch& batch, uint32 paramOffset); void do_glLineWidth(Batch& batch, uint32 paramOffset); diff --git a/libraries/gpu/src/gpu/GLBackendOutput.cpp b/libraries/gpu/src/gpu/GLBackendOutput.cpp index 6b0bfae635..d57ef57d78 100755 --- a/libraries/gpu/src/gpu/GLBackendOutput.cpp +++ b/libraries/gpu/src/gpu/GLBackendOutput.cpp @@ -178,9 +178,11 @@ void GLBackend::syncOutputStateCache() { } void GLBackend::resetOutputStage() { - _output._framebuffer.reset(); - _output._drawFBO = 0; - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + if (_output._framebuffer) { + _output._framebuffer.reset(); + _output._drawFBO = 0; + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + } } void GLBackend::do_setFramebuffer(Batch& batch, uint32 paramOffset) { diff --git a/libraries/gpu/src/gpu/GLBackendState.cpp b/libraries/gpu/src/gpu/GLBackendState.cpp index 618a13e2c4..bd683e0136 100644 --- a/libraries/gpu/src/gpu/GLBackendState.cpp +++ b/libraries/gpu/src/gpu/GLBackendState.cpp @@ -482,6 +482,11 @@ void GLBackend::syncPipelineStateCache() { State::Data state; glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); + + // Point size is always on + glEnable(GL_PROGRAM_POINT_SIZE_EXT); + glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); + getCurrentGLState(state); State::Signature signature = State::evalSignature(state); From 66c5aec744273781b735c10b5d625d761d3b95b5 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 27 Jul 2015 17:34:46 -0700 Subject: [PATCH 117/242] Remove commented dead code --- interface/src/Stars.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/interface/src/Stars.cpp b/interface/src/Stars.cpp index 7d09b9b1c3..c1e65086bf 100644 --- a/interface/src/Stars.cpp +++ b/interface/src/Stars.cpp @@ -218,10 +218,6 @@ void Stars::render(RenderArgs* renderArgs, float alpha) { // Render the stars batch.setPipeline(_starsPipeline); - /* batch._glEnable(GL_PROGRAM_POINT_SIZE_EXT); - batch._glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); - batch._glEnable(GL_POINT_SMOOTH); - */ batch.setInputFormat(streamFormat); batch.setInputBuffer(VERTICES_SLOT, posView); batch.setInputBuffer(COLOR_SLOT, colView); From f4a23065b4adbd86ec5a674cf5c65b886bad8a6a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 27 Jul 2015 19:06:14 -0700 Subject: [PATCH 118/242] if obj data isn't from a url, don't dereference null url pointer --- libraries/fbx/src/OBJReader.cpp | 72 +++++++++++++++++++-------------- libraries/fbx/src/OBJReader.h | 7 ++-- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 2ec80e3d85..f16c6ba215 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -195,7 +195,10 @@ void OBJFace::addFrom(const OBJFace* face, int index) { // add using data from f } bool OBJReader::isValidTexture(const QByteArray &filename) { - QUrl candidateUrl = url->resolved(QUrl(filename)); + if (!_url) { + return false; + } + QUrl candidateUrl = _url->resolved(QUrl(filename)); QNetworkReply *netReply = request(candidateUrl, true); bool isValid = netReply->isFinished() && (netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200); netReply->deleteLater(); @@ -242,7 +245,7 @@ void OBJReader::parseMaterialLibrary(QIODevice* device) { } else if ((token == "map_Kd") || (token == "map_Ks")) { QByteArray filename = QUrl(tokenizer.getLineAsDatum()).fileName().toUtf8(); if (filename.endsWith(".tga")) { - qCDebug(modelformat) << "OBJ Reader WARNING: currently ignoring tga texture " << filename << " in " << url; + qCDebug(modelformat) << "OBJ Reader WARNING: currently ignoring tga texture " << filename << " in " << _url; break; } if (isValidTexture(filename)) { @@ -252,7 +255,7 @@ void OBJReader::parseMaterialLibrary(QIODevice* device) { currentMaterial.specularTextureFilename = filename; } } else { - qCDebug(modelformat) << "OBJ Reader WARNING: " << url << " ignoring missing texture " << filename; + qCDebug(modelformat) << "OBJ Reader WARNING: " << _url << " ignoring missing texture " << filename; } } } @@ -316,7 +319,7 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi QByteArray groupName = tokenizer.getDatum(); currentGroup = groupName; //qCDebug(modelformat) << "new group:" << groupName; - } else if (token == "mtllib") { + } else if (token == "mtllib" && _url) { if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { break; } @@ -325,13 +328,15 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi break; // Some files use mtllib over and over again for the same libraryName } librariesSeen[libraryName] = true; - QUrl libraryUrl = url->resolved(QUrl(libraryName).fileName()); // Throw away any path part of libraryName, and merge against original url. + // Throw away any path part of libraryName, and merge against original url. + QUrl libraryUrl = _url->resolved(QUrl(libraryName).fileName()); qCDebug(modelformat) << "OBJ Reader new library:" << libraryName << " at:" << libraryUrl; QNetworkReply* netReply = request(libraryUrl, false); if (netReply->isFinished() && (netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200)) { parseMaterialLibrary(netReply); } else { - qCDebug(modelformat) << "OBJ Reader " << libraryName << " did not answer. Got " << netReply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + qCDebug(modelformat) << "OBJ Reader " << libraryName << " did not answer. Got " + << netReply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); } netReply->deleteLater(); } else if (token == "usemtl") { @@ -406,10 +411,10 @@ FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping, Q OBJTokenizer tokenizer(device); float scaleGuess = 1.0f; - this->url = url; + _url = url; geometry.meshExtents.reset(); geometry.meshes.append(FBXMesh()); - + try { // call parseOBJGroup as long as it's returning true. Each successful call will // add a new meshPart to the geometry's single mesh. @@ -417,7 +422,7 @@ FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping, Q FBXMesh& mesh = geometry.meshes[0]; mesh.meshIndex = 0; - + geometry.joints.resize(1); geometry.joints[0].isFree = false; geometry.joints[0].parentIndex = -1; @@ -440,37 +445,44 @@ FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping, Q 0, 0, 1, 0, 0, 0, 0, 1); mesh.clusters.append(cluster); - - // Some .obj files use the convention that a group with uv coordinates that doesn't define a material, should use a texture with the same basename as the .obj file. - QString filename = url->fileName(); - int extIndex = filename.lastIndexOf('.'); // by construction, this does not fail - QString basename = filename.remove(extIndex + 1, sizeof("obj")); - OBJMaterial& preDefinedMaterial = materials[SMART_DEFAULT_MATERIAL_NAME]; - preDefinedMaterial.diffuseColor = glm::vec3(1.0f); - QVector extensions = {"jpg", "jpeg", "png", "tga"}; - QByteArray base = basename.toUtf8(), textName = ""; - for (int i = 0; i < extensions.count(); i++) { - QByteArray candidateString = base + extensions[i]; - if (isValidTexture(candidateString)) { - textName = candidateString; - break; + + // Some .obj files use the convention that a group with uv coordinates that doesn't define a material, should use + // a texture with the same basename as the .obj file. + if (url) { + QString filename = url->fileName(); + int extIndex = filename.lastIndexOf('.'); // by construction, this does not fail + QString basename = filename.remove(extIndex + 1, sizeof("obj")); + OBJMaterial& preDefinedMaterial = materials[SMART_DEFAULT_MATERIAL_NAME]; + preDefinedMaterial.diffuseColor = glm::vec3(1.0f); + QVector extensions = {"jpg", "jpeg", "png", "tga"}; + QByteArray base = basename.toUtf8(), textName = ""; + for (int i = 0; i < extensions.count(); i++) { + QByteArray candidateString = base + extensions[i]; + if (isValidTexture(candidateString)) { + textName = candidateString; + break; + } } + + if (!textName.isEmpty()) { + preDefinedMaterial.diffuseTextureFilename = textName; + } + materials[SMART_DEFAULT_MATERIAL_NAME] = preDefinedMaterial; } - if (!textName.isEmpty()) { - preDefinedMaterial.diffuseTextureFilename = textName; - } - materials[SMART_DEFAULT_MATERIAL_NAME] = preDefinedMaterial; - + for (int i = 0, meshPartCount = 0; i < mesh.parts.count(); i++, meshPartCount++) { FBXMeshPart& meshPart = mesh.parts[i]; FaceGroup faceGroup = faceGroups[meshPartCount]; OBJFace leadFace = faceGroup[0]; // All the faces in the same group will have the same name and material. QString groupMaterialName = leadFace.materialName; if (groupMaterialName.isEmpty() && (leadFace.textureUVIndices.count() > 0)) { - qCDebug(modelformat) << "OBJ Reader WARNING: " << url << " needs a texture that isn't specified. Using default mechanism."; + qCDebug(modelformat) << "OBJ Reader WARNING: " << url + << " needs a texture that isn't specified. Using default mechanism."; groupMaterialName = SMART_DEFAULT_MATERIAL_NAME; } else if (!groupMaterialName.isEmpty() && !materials.contains(groupMaterialName)) { - qCDebug(modelformat) << "OBJ Reader WARNING: " << url << " specifies a material " << groupMaterialName << " that is not defined. Using default mechanism."; + qCDebug(modelformat) << "OBJ Reader WARNING: " << url + << " specifies a material " << groupMaterialName + << " that is not defined. Using default mechanism."; groupMaterialName = SMART_DEFAULT_MATERIAL_NAME; } if (!groupMaterialName.isEmpty()) { diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index 771c0b1b63..2e7b050b0a 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -69,12 +69,13 @@ public: QVector faceGroups; QString currentMaterialName; QHash materials; - QUrl* url; - + QNetworkReply* request(QUrl& url, bool isTest); FBXGeometry readOBJ(const QByteArray& model, const QVariantHash& mapping); FBXGeometry readOBJ(QIODevice* device, const QVariantHash& mapping, QUrl* url); private: + QUrl* _url = nullptr; + QHash librariesSeen; bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, FBXGeometry& geometry, float& scaleGuess); void parseMaterialLibrary(QIODevice* device); @@ -83,4 +84,4 @@ private: // What are these utilities doing here? One is used by fbx loading code in VHACD Utils, and the other a general debugging utility. void setMeshPartDefaults(FBXMeshPart& meshPart, QString materialID); -void fbxDebugDump(const FBXGeometry& fbxgeo); \ No newline at end of file +void fbxDebugDump(const FBXGeometry& fbxgeo); From 8a7cdb1c64f0fd13f5473252ca13a5df1f59da6d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 27 Jul 2015 19:53:38 -0700 Subject: [PATCH 119/242] Make Throttle FPS If Not Focus be enabled by default --- interface/src/Application.cpp | 2 +- interface/src/Menu.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index bb564824b0..161e36d649 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -330,7 +330,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _lastNackTime(usecTimestampNow()), _lastSendDownstreamAudioStats(usecTimestampNow()), _isVSyncOn(true), - _isThrottleFPSEnabled(false), + _isThrottleFPSEnabled(true), _aboutToQuit(false), _notifiedPacketVersionMismatchThisDomain(false), _glWidget(new GLCanvas()), diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 91ae6a4d02..424c9b2227 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -368,7 +368,7 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::RenderTargetFramerateVSyncOn, 0, true, qApp, SLOT(setVSyncEnabled())); #endif - addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::ThrottleFPSIfNotFocus, 0, false, + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::ThrottleFPSIfNotFocus, 0, true, qApp, SLOT(setThrottleFPSEnabled())); } From 81e80996f791ef40a4d701ea38842d91a2be4377 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 28 Jul 2015 11:24:33 -0700 Subject: [PATCH 120/242] Fix large marketplace imports not working --- examples/edit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/edit.js b/examples/edit.js index ec3106e585..a07779c19d 100644 --- a/examples/edit.js +++ b/examples/edit.js @@ -1064,7 +1064,7 @@ function importSVO(importURL) { if (success) { var VERY_LARGE = 10000; - var position = { x: 0, y: 0, z: 0}; + var position = { x: 0.01, y: 0.01, z: 0.01}; if (Clipboard.getClipboardContentsLargestDimension() < VERY_LARGE) { position = getPositionToCreateEntity(); } @@ -1074,7 +1074,7 @@ function importSVO(importURL) { if (isActive) { selectionManager.setSelections(pastedEntityIDs); } - Window.raiseMainWindow(); + Window.raiseMainWindow(); } else { Window.alert("Can't import objects: objects would be out of bounds."); } From 2aa453b6102f0fb092283a5644498fcf7c355aeb Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 28 Jul 2015 12:05:45 -0700 Subject: [PATCH 121/242] fix build errors for shared-tests --- tests/shared/src/AngularConstraintTests.cpp | 1 + tests/shared/src/AngularConstraintTests.h | 7 ++----- tests/shared/src/MovingPercentileTests.cpp | 1 + tests/shared/src/MovingPercentileTests.h | 1 - tests/shared/src/TransformTests.cpp | 4 +++- tests/shared/src/TransformTests.h | 2 -- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/shared/src/AngularConstraintTests.cpp b/tests/shared/src/AngularConstraintTests.cpp index 95c5db18c2..4dcf3d7296 100644 --- a/tests/shared/src/AngularConstraintTests.cpp +++ b/tests/shared/src/AngularConstraintTests.cpp @@ -16,6 +16,7 @@ #include #include "AngularConstraintTests.h" +#include "../QTestExtensions.h" // Computes the error value between two quaternions (using glm::dot) float getErrorDifference(const glm::quat& a, const glm::quat& b) { diff --git a/tests/shared/src/AngularConstraintTests.h b/tests/shared/src/AngularConstraintTests.h index df2fe8e9c3..705639b571 100644 --- a/tests/shared/src/AngularConstraintTests.h +++ b/tests/shared/src/AngularConstraintTests.h @@ -12,6 +12,7 @@ #ifndef hifi_AngularConstraintTests_h #define hifi_AngularConstraintTests_h +#include #include class AngularConstraintTests : public QObject { @@ -21,10 +22,6 @@ private slots: void testConeRollerConstraint(); }; -// Use QCOMPARE_WITH_ABS_ERROR and define it for glm::quat -#include -float getErrorDifference (const glm::quat& a, const glm::quat& b); -QTextStream & operator << (QTextStream& stream, const glm::quat& q); -#include "../QTestExtensions.h" +float getErrorDifference(const glm::quat& a, const glm::quat& b); #endif // hifi_AngularConstraintTests_h diff --git a/tests/shared/src/MovingPercentileTests.cpp b/tests/shared/src/MovingPercentileTests.cpp index b9593fca83..fbbc3c7b9e 100644 --- a/tests/shared/src/MovingPercentileTests.cpp +++ b/tests/shared/src/MovingPercentileTests.cpp @@ -16,6 +16,7 @@ #include #include +#include <../QTestExtensions.h> QTEST_MAIN(MovingPercentileTests) diff --git a/tests/shared/src/MovingPercentileTests.h b/tests/shared/src/MovingPercentileTests.h index 4a1d4b33d2..ffc8ddb0f6 100644 --- a/tests/shared/src/MovingPercentileTests.h +++ b/tests/shared/src/MovingPercentileTests.h @@ -13,7 +13,6 @@ #define hifi_MovingPercentileTests_h #include -#include <../QTestExtensions.h> class MovingPercentileTests : public QObject { Q_OBJECT diff --git a/tests/shared/src/TransformTests.cpp b/tests/shared/src/TransformTests.cpp index 93b0583aa6..be22914b9d 100644 --- a/tests/shared/src/TransformTests.cpp +++ b/tests/shared/src/TransformTests.cpp @@ -8,10 +8,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "TransformTests.h" + #include -#include "TransformTests.h" #include "SharedLogging.h" +#include <../QTestExtensions.h> using namespace glm; diff --git a/tests/shared/src/TransformTests.h b/tests/shared/src/TransformTests.h index ab5c8cf144..a4d9b2a6c0 100644 --- a/tests/shared/src/TransformTests.h +++ b/tests/shared/src/TransformTests.h @@ -40,8 +40,6 @@ inline QTextStream& operator<< (QTextStream& stream, const glm::mat4& matrix) { return stream; } -#include <../QTestExtensions.h> - class TransformTests : public QObject { Q_OBJECT private slots: From d03a7d1b701d0459468ac7d4afe908d973957707 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 28 Jul 2015 12:24:17 -0700 Subject: [PATCH 122/242] fix physics-tests build errors --- tests/physics/src/BulletUtilTests.cpp | 7 +- tests/physics/src/BulletUtilTests.h | 3 - tests/physics/src/CollisionInfoTests.cpp | 70 ------------------- tests/physics/src/CollisionInfoTests.h | 29 -------- tests/physics/src/MeshMassPropertiesTests.cpp | 8 ++- tests/physics/src/MeshMassPropertiesTests.h | 6 -- tests/physics/src/ShapeColliderTests.cpp | 14 ++-- tests/physics/src/ShapeColliderTests.h | 7 -- 8 files changed, 18 insertions(+), 126 deletions(-) delete mode 100644 tests/physics/src/CollisionInfoTests.cpp delete mode 100644 tests/physics/src/CollisionInfoTests.h diff --git a/tests/physics/src/BulletUtilTests.cpp b/tests/physics/src/BulletUtilTests.cpp index bbd88f88b7..181d22327e 100644 --- a/tests/physics/src/BulletUtilTests.cpp +++ b/tests/physics/src/BulletUtilTests.cpp @@ -9,13 +9,16 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "BulletUtilTests.h" + #include -//#include "PhysicsTestUtil.h" #include #include -#include "BulletUtilTests.h" +// Add additional qtest functionality (the include order is important!) +#include "GlmTestUtils.h" +#include "../QTestExtensions.h" // Constants const glm::vec3 origin(0.0f); diff --git a/tests/physics/src/BulletUtilTests.h b/tests/physics/src/BulletUtilTests.h index fd4fe13d09..e8fee1e473 100644 --- a/tests/physics/src/BulletUtilTests.h +++ b/tests/physics/src/BulletUtilTests.h @@ -13,9 +13,6 @@ #define hifi_BulletUtilTests_h #include -// Add additional qtest functionality (the include order is important!) -#include "GlmTestUtils.h" -#include "../QTestExtensions.h" class BulletUtilTests : public QObject { Q_OBJECT diff --git a/tests/physics/src/CollisionInfoTests.cpp b/tests/physics/src/CollisionInfoTests.cpp deleted file mode 100644 index 70e2e14bb0..0000000000 --- a/tests/physics/src/CollisionInfoTests.cpp +++ /dev/null @@ -1,70 +0,0 @@ -// -// CollisionInfoTests.cpp -// tests/physics/src -// -// Created by Andrew Meadows on 2/21/2014. -// 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 -#include -#include - -#include "CollisionInfoTests.h" - - - -QTEST_MAIN(CollisionInfoTests) -/* -static glm::vec3 xAxis(1.0f, 0.0f, 0.0f); -static glm::vec3 xZxis(0.0f, 1.0f, 0.0f); -static glm::vec3 xYxis(0.0f, 0.0f, 1.0f); - -void CollisionInfoTests::rotateThenTranslate() { - CollisionInfo collision; - collision._penetration = xAxis; - collision._contactPoint = xYxis; - collision._addedVelocity = xAxis + xYxis + xZxis; - - glm::quat rotation = glm::angleAxis(PI_OVER_TWO, zAxis); - float distance = 3.0f; - glm::vec3 translation = distance * xYxis; - - collision.rotateThenTranslate(rotation, translation); - QCOMPARE(collision._penetration, xYxis); - - glm::vec3 expectedContactPoint = -xAxis + translation; - QCOMPARE(collision._contactPoint, expectedContactPoint); - - glm::vec3 expectedAddedVelocity = xYxis - xAxis + xZxis; - QCOMPARE(collision._addedVelocity, expectedAddedVelocity); -} - -void CollisionInfoTests::translateThenRotate() { - CollisionInfo collision; - collision._penetration = xAxis; - collision._contactPoint = xYxis; - collision._addedVelocity = xAxis + xYxis + xZxis; - - glm::quat rotation = glm::angleAxis( -PI_OVER_TWO, zAxis); - float distance = 3.0f; - glm::vec3 translation = distance * xYxis; - - collision.translateThenRotate(translation, rotation); - QCOMPARE(collision._penetration, -xYxis); - - glm::vec3 expectedContactPoint = (1.0f + distance) * xAxis; - QCOMPARE(collision._contactPoint, expectedContactPoint); - - glm::vec3 expectedAddedVelocity = - xYxis + xAxis + xYxis; - QCOMPARE(collision._addedVelocity, expectedAddedVelocity); -}*/ - diff --git a/tests/physics/src/CollisionInfoTests.h b/tests/physics/src/CollisionInfoTests.h deleted file mode 100644 index d26d39be4b..0000000000 --- a/tests/physics/src/CollisionInfoTests.h +++ /dev/null @@ -1,29 +0,0 @@ -// -// CollisionInfoTests.h -// tests/physics/src -// -// Created by Andrew Meadows on 2/21/2014. -// 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_CollisionInfoTests_h -#define hifi_CollisionInfoTests_h - -#include - -// Add additional qtest functionality (the include order is important!) -#include "GlmTestUtils.h" -#include "../QTestExtensions.h" - -class CollisionInfoTests : public QObject { - Q_OBJECT - -private slots: -// void rotateThenTranslate(); -// void translateThenRotate(); -}; - -#endif // hifi_CollisionInfoTests_h diff --git a/tests/physics/src/MeshMassPropertiesTests.cpp b/tests/physics/src/MeshMassPropertiesTests.cpp index 794eee0fcf..e88bcae1b7 100644 --- a/tests/physics/src/MeshMassPropertiesTests.cpp +++ b/tests/physics/src/MeshMassPropertiesTests.cpp @@ -9,11 +9,15 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "MeshMassPropertiesTests.h" + #include -#include #include -#include "MeshMassPropertiesTests.h" +// Add additional qtest functionality (the include order is important!) +#include "BulletTestUtils.h" +#include "GlmTestUtils.h" +#include "../QTestExtensions.h" const btScalar acceptableRelativeError(1.0e-5f); const btScalar acceptableAbsoluteError(1.0e-4f); diff --git a/tests/physics/src/MeshMassPropertiesTests.h b/tests/physics/src/MeshMassPropertiesTests.h index 35471bdbad..b8af9d9db6 100644 --- a/tests/physics/src/MeshMassPropertiesTests.h +++ b/tests/physics/src/MeshMassPropertiesTests.h @@ -13,12 +13,6 @@ #define hifi_MeshMassPropertiesTests_h #include -#include - -// Add additional qtest functionality (the include order is important!) -#include "BulletTestUtils.h" -#include "GlmTestUtils.h" -#include "../QTestExtensions.h" // Relative error macro (see errorTest in BulletTestUtils.h) #define QCOMPARE_WITH_RELATIVE_ERROR(actual, expected, relativeError) \ diff --git a/tests/physics/src/ShapeColliderTests.cpp b/tests/physics/src/ShapeColliderTests.cpp index cb42f534cb..cb51e18fbd 100644 --- a/tests/physics/src/ShapeColliderTests.cpp +++ b/tests/physics/src/ShapeColliderTests.cpp @@ -9,12 +9,15 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "ShapeColliderTests.h" + //#include #include #include #include #include +#include #include #include @@ -25,7 +28,10 @@ #include #include -#include "ShapeColliderTests.h" +// Add additional qtest functionality (the include order is important!) +#include "BulletTestUtils.h" +#include "GlmTestUtils.h" +#include "../QTestExtensions.h" const glm::vec3 origin(0.0f); @@ -1553,8 +1559,6 @@ void ShapeColliderTests::rayHitsCapsule() { intersection._rayDirection = - xAxis; QCOMPARE(capsule.findRayIntersection(intersection), true); float expectedDistance = startDistance - radius * sqrtf(2.0f * delta); // using small angle approximation of cosine -// float relativeError = fabsf(intersection._hitDistance - expectedDistance) / startDistance; - // for edge cases we allow a LOT of error QCOMPARE_WITH_ABS_ERROR(intersection._hitDistance, expectedDistance, startDistance * EDGE_CASE_SLOP_FACTOR * EPSILON); } @@ -1564,8 +1568,6 @@ void ShapeColliderTests::rayHitsCapsule() { intersection._rayDirection = - xAxis; QCOMPARE(capsule.findRayIntersection(intersection), true); float expectedDistance = startDistance - radius * sqrtf(2.0f * delta); // using small angle approximation of cosine -// float relativeError = fabsf(intersection._hitDistance - expectedDistance) / startDistance; - // for edge cases we allow a LOT of error QCOMPARE_WITH_ABS_ERROR(intersection._hitDistance, expectedDistance, startDistance * EPSILON * EDGE_CASE_SLOP_FACTOR); } @@ -1575,8 +1577,6 @@ void ShapeColliderTests::rayHitsCapsule() { intersection._rayDirection = - xAxis; QCOMPARE(capsule.findRayIntersection(intersection), true); float expectedDistance = startDistance - radius * sqrtf(2.0f * delta); // using small angle approximation of cosine - float relativeError = fabsf(intersection._hitDistance - expectedDistance) / startDistance; - // for edge cases we allow a LOT of error QCOMPARE_WITH_ABS_ERROR(intersection._hitDistance, expectedDistance, startDistance * EPSILON * EDGE_CASE_SLOP_FACTOR); } // TODO: test at steep angles near cylinder/cap junction diff --git a/tests/physics/src/ShapeColliderTests.h b/tests/physics/src/ShapeColliderTests.h index 48d9cbd742..73e2b972a9 100644 --- a/tests/physics/src/ShapeColliderTests.h +++ b/tests/physics/src/ShapeColliderTests.h @@ -13,13 +13,6 @@ #define hifi_ShapeColliderTests_h #include -#include - -// Add additional qtest functionality (the include order is important!) -#include "BulletTestUtils.h" -#include "GlmTestUtils.h" -#include "../QTestExtensions.h" - class ShapeColliderTests : public QObject { Q_OBJECT From 4754615159f5c4f554181d59e67cc367db2177c2 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 28 Jul 2015 12:34:10 -0700 Subject: [PATCH 123/242] Fix NPC animations. --- interface/src/avatar/SkeletonModel.cpp | 3 ++- libraries/animation/src/Rig.cpp | 5 +++-- libraries/animation/src/Rig.h | 5 ++++- libraries/render-utils/src/Model.cpp | 4 ++++ libraries/render-utils/src/Model.h | 2 +- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 7b6fb85ae5..636e58c5a8 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -116,7 +116,8 @@ void SkeletonModel::updateClusterMatrices() { } void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { - _rig->simulateInternal(deltaTime, parentTransform, _owningAvatar->getPosition(), _owningAvatar->getVelocity(), _owningAvatar->getOrientation()); + _rig->computeMotionAnimationState(deltaTime, _owningAvatar->getPosition(), _owningAvatar->getVelocity(), _owningAvatar->getOrientation()); + Model::updateRig(deltaTime, parentTransform); } void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index d20abc58d6..0bd3f14096 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -341,7 +341,7 @@ glm::mat4 Rig::getJointVisibleTransform(int jointIndex) const { return maybeCauterizeHead(jointIndex).getVisibleTransform(); } -void Rig::simulateInternal(float deltaTime, glm::mat4 parentTransform, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation) { +void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation) { if (_enableRig) { glm::vec3 front = worldRotation * IDENTITY_FRONT; @@ -374,8 +374,9 @@ void Rig::simulateInternal(float deltaTime, glm::mat4 parentTransform, const glm _isTurning = isTurning; _isIdle = isIdle; } +} - // update animations +void Rig::updateAnimations(float deltaTime, glm::mat4 parentTransform) { foreach (const AnimationHandlePointer& handle, _runningAnimations) { handle->simulate(deltaTime); } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 8b2fc10fa5..52d5866369 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -108,7 +108,10 @@ public: void setJointTransform(int jointIndex, glm::mat4 newTransform); glm::mat4 getJointVisibleTransform(int jointIndex) const; void setJointVisibleTransform(int jointIndex, glm::mat4 newTransform); - void simulateInternal(float deltaTime, glm::mat4 parentTransform, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation); + // Start or stop animations as needed. + void computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation); + // Regardless of who started the animations or how many, update the joints. + void updateAnimations(float deltaTime, glm::mat4 parentTransform); bool setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, bool useRotation, int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, float priority, const QVector& freeLineage, glm::mat4 parentTransform); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index dd2d3369c8..92e49fdc55 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1326,6 +1326,10 @@ void Model::simulate(float deltaTime, bool fullUpdate) { } } +//virtual +void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { + _rig->updateAnimations(deltaTime, parentTransform); +} void Model::simulateInternal(float deltaTime) { // update the world space transforms for all joints diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 27ef808ef0..83527969b2 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -269,7 +269,7 @@ protected: void snapToRegistrationPoint(); void simulateInternal(float deltaTime); - virtual void updateRig(float deltaTime, glm::mat4 parentTransform) {}; // Subclasses may be more interesting + virtual void updateRig(float deltaTime, glm::mat4 parentTransform); /// Updates the state of the joint at the specified index. virtual void updateJointState(int index); From b46ad0c39780d475de0ed90d2b84029e2616ca96 Mon Sep 17 00:00:00 2001 From: bwent Date: Wed, 15 Jul 2015 11:50:10 -0700 Subject: [PATCH 124/242] Created subpanel panel item --- examples/utilities/tools/cookies.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/examples/utilities/tools/cookies.js b/examples/utilities/tools/cookies.js index 0fdae01c5e..8b4d739c02 100644 --- a/examples/utilities/tools/cookies.js +++ b/examples/utilities/tools/cookies.js @@ -416,6 +416,8 @@ DirectionBox = function(x,y,width,thumbSize) { this.onValueChanged = function(value) {}; } + + var textFontSize = 12; function PanelItem(name, setter, getter, displayer, x, y, textWidth, valueWidth, height) { @@ -429,7 +431,7 @@ function PanelItem(name, setter, getter, displayer, x, y, textWidth, valueWidth, } else if (value == false) { return "Off"; } - return value.toFixed(2); + return value; }; var topMargin = (height - textFontSize); @@ -616,7 +618,7 @@ Panel = function(x, y) { // print("created Item... colorBox=" + name); }; - this.newDirectionBox= function(name, setValue, getValue, displayValue) { + this.newDirectionBox = function(name, setValue, getValue, displayValue) { var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); @@ -630,6 +632,20 @@ Panel = function(x, y) { // print("created Item... directionBox=" + name); }; + this.newSubPanel = function(name) { + var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); + + var panel = new Panel(this.widgetX, this.nextY); + + item.widget = panel; + + item.widget.onValueChanged = function(value) { item.setterFromWidget(value); }; + + this.nextY += rawYDelta; + + }; + + this.destroy = function() { for (var i in this.items) { this.items[i].destroy(); From 4ae03184ecfcab1377b9ac7911f792a9b34a0061 Mon Sep 17 00:00:00 2001 From: bwent Date: Tue, 21 Jul 2015 10:23:41 -0700 Subject: [PATCH 125/242] add subpanel functionality --- examples/utilities/tools/cookies.js | 242 +++++++++++++++++++++++++--- 1 file changed, 223 insertions(+), 19 deletions(-) diff --git a/examples/utilities/tools/cookies.js b/examples/utilities/tools/cookies.js index 8b4d739c02..2949626252 100644 --- a/examples/utilities/tools/cookies.js +++ b/examples/utilities/tools/cookies.js @@ -92,7 +92,6 @@ Slider = function(x,y,width,thumbSize) { this.isMoving = false; }; - // Public members: this.setNormalizedValue = function(value) { if (value < 0.0) { @@ -113,10 +112,19 @@ Slider = function(x,y,width,thumbSize) { this.setNormalizedValue(normValue); }; + this.reset = function(resetValue) { + this.setValue(resetValue); + this.onValueChanged(resetValue); + }; + this.getValue = function() { return this.getNormalizedValue() * (this.maxValue - this.minValue) + this.minValue; }; + this.getHeight = function() { + return 1.5 * this.thumbSize; + } + this.onValueChanged = function(value) {}; this.destroy = function() { @@ -130,6 +138,7 @@ Slider = function(x,y,width,thumbSize) { this.setBackgroundColor = function(color) { Overlays.editOverlay(this.background, {backgroundColor: { red: color.x*255, green: color.y*255, blue: color.z*255 }}); }; + } // The Checkbox class @@ -158,10 +167,12 @@ Checkbox = function(x,y,width,thumbSize) { visible: true }); - + this.thumbSize = thumbSize; var checkX = x + (0.25 * thumbSize); var checkY = y + (0.25 * thumbSize); + var boxCheckStatus; + var clickedBox = false; var checkMark = Overlays.addOverlay("text", { @@ -181,22 +192,22 @@ Checkbox = function(x,y,width,thumbSize) { width: thumbSize / 2.5, height: thumbSize / 2.5, alpha: 1.0, - visible: boxCheckStatus + visible: !boxCheckStatus }); - - var boxCheckStatus; - var clickedBox = false; - this.updateThumb = function() { - if (clickedBox) { - boxCheckStatus = !boxCheckStatus; - if (boxCheckStatus) { - Overlays.editOverlay(unCheckMark, { visible: false }); - } else { - Overlays.editOverlay(unCheckMark, { visible: true }); - } + this.updateThumb = function() { + if(!clickedBox) { + return; + } + + boxCheckStatus = !boxCheckStatus; + if (boxCheckStatus) { + Overlays.editOverlay(unCheckMark, { visible: false }); + } else { + Overlays.editOverlay(unCheckMark, { visible: true }); } + }; this.isClickableOverlayItem = function(item) { @@ -215,10 +226,12 @@ Checkbox = function(x,y,width,thumbSize) { this.onValueChanged(this.getValue()); }; + this.onMouseReleaseEvent = function(event) { this.clickedBox = false; }; + // Public members: this.setNormalizedValue = function(value) { boxCheckStatus = value; @@ -232,10 +245,26 @@ Checkbox = function(x,y,width,thumbSize) { this.setNormalizedValue(value); }; + this.reset = function(resetValue) { + this.setValue(resetValue); + + this.onValueChanged(resetValue); + }; + this.getValue = function() { return boxCheckStatus; }; + this.getHeight = function() { + return 1.5 * this.thumbSize; + } + + this.setterFromWidget = function(value) { + var status = boxCheckStatus; + this.onValueChanged(boxCheckStatus); + this.updateThumb(); + }; + this.onValueChanged = function(value) {}; this.destroy = function() { @@ -267,6 +296,8 @@ ColorBox = function(x,y,width,thumbSize) { this.green.setBackgroundColor({x: 0, y: 1, z: 0}); this.blue.setBackgroundColor({x: 0, y: 0, z: 1}); + + this.isClickableOverlayItem = function(item) { return this.red.isClickableOverlayItem(item) || this.green.isClickableOverlayItem(item) @@ -293,6 +324,7 @@ ColorBox = function(x,y,width,thumbSize) { this.blue.onMousePressEvent(event, clickedOverlay); }; + this.onMouseReleaseEvent = function(event) { this.red.onMouseReleaseEvent(event); this.green.onMouseReleaseEvent(event); @@ -323,11 +355,20 @@ ColorBox = function(x,y,width,thumbSize) { this.updateRGBSliders(value); }; + this.reset = function(resetValue) { + this.setValue(resetValue); + this.onValueChanged(resetValue); + }; + this.getValue = function() { var value = {x:this.red.getValue(), y:this.green.getValue(),z:this.blue.getValue()}; return value; }; + this.getHeight = function() { + return 1.5 * this.thumbSize; + } + this.destroy = function() { this.red.destroy(); this.green.destroy(); @@ -345,6 +386,8 @@ DirectionBox = function(x,y,width,thumbSize) { var sliderWidth = width; this.yaw = new Slider(x, y, width, slideHeight); this.pitch = new Slider(x, y + slideHeight, width, slideHeight); + + this.yaw.setThumbColor({x: 1, y: 0, z: 0}); this.yaw.minValue = -180; @@ -400,6 +443,11 @@ DirectionBox = function(x,y,width,thumbSize) { this.pitch.setValue(direction.y); }; + this.reset = function(resetValue) { + this.setValue(resetValue); + this.onValueChanged(resetValue); + }; + this.getValue = function() { var dirZ = this.pitch.getValue(); var yaw = this.yaw.getValue() * Math.PI / 180; @@ -408,6 +456,10 @@ DirectionBox = function(x,y,width,thumbSize) { return value; }; + this.getHeight = function() { + return 1.5 * this.thumbSize; + } + this.destroy = function() { this.yaw.destroy(); this.pitch.destroy(); @@ -420,6 +472,48 @@ DirectionBox = function(x,y,width,thumbSize) { var textFontSize = 12; +// TODO: Make collapsable +function CollapsablePanelItem(name, x, y, textWidth, height) { + this.name = name; + this.height = height; + + var topMargin = (height - textFontSize); + + this.thumb = Overlays.addOverlay("text", { + backgroundColor: { red: 220, green: 220, blue: 220 }, + textFontSize: 10, + x: x, + y: y, + width: rawHeight, + height: rawHeight, + alpha: 1.0, + backgroundAlpha: 1.0, + visible: true + }); + + this.title = Overlays.addOverlay("text", { + backgroundColor: { red: 255, green: 255, blue: 255 }, + x: x + rawHeight, + y: y, + width: textWidth, + height: height, + alpha: 1.0, + backgroundAlpha: 0.5, + visible: true, + text: name, + font: {size: textFontSize}, + topMargin: topMargin, + }); + + this.destroy = function() { + Overlays.deleteOverlay(this.title); + Overlays.deleteOverlay(this.thumb); + if (this.widget != null) { + this.widget.destroy(); + } + } +} + function PanelItem(name, setter, getter, displayer, x, y, textWidth, valueWidth, height) { //print("creating panel item: " + name); @@ -548,15 +642,18 @@ Panel = function(x, y) { // Reset panel item upon double-clicking this.mouseDoublePressEvent = function(event) { + var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); for (var i in this.items) { var item = this.items[i]; var widget = item.widget; - if (item.title == clickedOverlay || item.value == clickedOverlay) { - widget.updateThumb(); - widget.onValueChanged(item.resetValue); + if (clickedOverlay == item.title) { + item.activeWidget = widget; + + item.activeWidget.reset(item.resetValue); + break; } } @@ -569,8 +666,56 @@ Panel = function(x, y) { this.activeWidget = null; }; - this.newSlider = function(name, minValue, maxValue, setValue, getValue, displayValue) { + this.onMousePressEvent = function(event, clickedOverlay) { + for (var i in this.items) { + var item = this.items[i]; + if(item.widget.isClickableOverlayItem(clickedOverlay)) { + item.activeWidget = item.widget; + item.activeWidget.onMousePressEvent(event,clickedOverlay); + } + } + } + this.reset = function() { + for (var i in this.items) { + var item = this.items[i]; + if (item.activeWidget) { + item.activeWidget.reset(item.resetValue); + } + } + } + + this.onMouseMoveEvent = function(event) { + for (var i in this.items) { + var item = this.items[i]; + if (item.activeWidget) { + item.activeWidget.onMouseMoveEvent(event); + } + } + } + + this.onMouseReleaseEvent = function(event, clickedOverlay) { + for (var i in this.items) { + var item = this.items[i]; + if (item.activeWidget) { + item.activeWidget.onMouseReleaseEvent(event); + } + item.activeWidget = null; + } + } + + this.onMouseDoublePressEvent = function(event, clickedOverlay) { + + for (var i in this.items) { + var item = this.items[i]; + if (item.activeWidget) { + item.activeWidget.onMouseDoublePressEvent(event); + } + } + } + + this.newSlider = function(name, minValue, maxValue, setValue, getValue, displayValue) { + this.nextY = this.y + this.getHeight(); var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); var slider = new Slider(this.widgetX, this.nextY, widgetWidth, rawHeight); @@ -591,6 +736,8 @@ Panel = function(x, y) { } else if (displayValue == false) { display = function() {return "Off";}; } + + this.nextY = this.y + this.getHeight(); var item = new PanelItem(name, setValue, getValue, display, this.x, this.nextY, textWidth, valueWidth, rawHeight); @@ -600,11 +747,12 @@ Panel = function(x, y) { item.widget.onValueChanged = function(value) { item.setterFromWidget(value); }; item.setter(getValue()); this.items[name] = item; - this.nextY += rawYDelta; + //print("created Item... checkbox=" + name); }; this.newColorBox = function(name, setValue, getValue, displayValue) { + this.nextY = this.y + this.getHeight(); var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); @@ -618,7 +766,12 @@ Panel = function(x, y) { // print("created Item... colorBox=" + name); }; +<<<<<<< Updated upstream this.newDirectionBox = function(name, setValue, getValue, displayValue) { +======= + this.newDirectionBox= function(name, setValue, getValue, displayValue) { + this.nextY = this.y + this.getHeight(); +>>>>>>> Stashed changes var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); @@ -632,6 +785,7 @@ Panel = function(x, y) { // print("created Item... directionBox=" + name); }; +<<<<<<< Updated upstream this.newSubPanel = function(name) { var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); @@ -645,6 +799,32 @@ Panel = function(x, y) { }; +======= + var indentation = 30; + + this.newSubPanel = function(name) { + //TODO: make collapsable, fix double-press event + this.nextY = this.y + this.getHeight(); + + var item = new CollapsablePanelItem(name, this.x, this.nextY, textWidth, rawHeight, panel); + item.isSubPanel = true; + + this.nextY += 1.5 * item.height; + + var subPanel = new Panel(this.x + indentation, this.nextY); + + item.widget = subPanel; + this.items[name] = item; + return subPanel; + // print("created Item... subPanel=" + name); + }; + + this.onValueChanged = function(value) { + for (var i in this.items) { + this.items[i].widget.onValueChanged(value); + } + } +>>>>>>> Stashed changes this.destroy = function() { for (var i in this.items) { @@ -652,6 +832,7 @@ Panel = function(x, y) { } } + this.set = function(name, value) { var item = this.items[name]; if (item != null) { @@ -676,6 +857,29 @@ Panel = function(x, y) { return null; } + this.isClickableOverlayItem = function(item) { + for (var i in this.items) { + if (this.items[i].widget.isClickableOverlayItem(item)) { + return true; + } + } + return false; + } + + this.getHeight = function() { + var height = 0; + + for (var i in this.items) { + height += this.items[i].widget.getHeight(); + if(this.items[i].isSubPanel) { + height += 1.5 * rawHeight; + } + } + + + return height; + } + }; From 252780dc5dcebb32866c9b151adcda2f014df260 Mon Sep 17 00:00:00 2001 From: bwent Date: Mon, 27 Jul 2015 13:44:01 -0700 Subject: [PATCH 126/242] fix checkbox class, make subpanels collapsable --- examples/utilities/tools/cookies.js | 1669 ++++++++++++++++----------- 1 file changed, 983 insertions(+), 686 deletions(-) diff --git a/examples/utilities/tools/cookies.js b/examples/utilities/tools/cookies.js index 2949626252..9d064385bf 100644 --- a/examples/utilities/tools/cookies.js +++ b/examples/utilities/tools/cookies.js @@ -10,663 +10,925 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // // The Slider class -Slider = function(x,y,width,thumbSize) { - this.background = Overlays.addOverlay("text", { - backgroundColor: { red: 200, green: 200, blue: 255 }, - x: x, - y: y, - width: width, - height: thumbSize, - alpha: 1.0, - backgroundAlpha: 0.5, - visible: true - }); - this.thumb = Overlays.addOverlay("text", { - backgroundColor: { red: 255, green: 255, blue: 255 }, - x: x, - y: y, - width: thumbSize, - height: thumbSize, - alpha: 1.0, - backgroundAlpha: 1.0, - visible: true - }); - - - this.thumbSize = thumbSize; - this.thumbHalfSize = 0.5 * thumbSize; - - this.minThumbX = x + this.thumbHalfSize; - this.maxThumbX = x + width - this.thumbHalfSize; - this.thumbX = this.minThumbX; - - this.minValue = 0.0; - this.maxValue = 1.0; - - this.clickOffsetX = 0; - this.isMoving = false; - - this.updateThumb = function() { - thumbTruePos = this.thumbX - 0.5 * this.thumbSize; - Overlays.editOverlay(this.thumb, { x: thumbTruePos } ); - }; - - this.isClickableOverlayItem = function(item) { - return (item == this.thumb) || (item == this.background); - }; - - this.onMouseMoveEvent = function(event) { - if (this.isMoving) { - newThumbX = event.x - this.clickOffsetX; - if (newThumbX < this.minThumbX) { - newThumbX = this.minThumbX; - } - if (newThumbX > this.maxThumbX) { - newThumbX = this.maxThumbX; - } - this.thumbX = newThumbX; - this.updateThumb(); - this.onValueChanged(this.getValue()); - } - }; - - this.onMousePressEvent = function(event, clickedOverlay) { - if (!this.isClickableOverlayItem(clickedOverlay)) { - this.isMoving = false; - return; - } - this.isMoving = true; - var clickOffset = event.x - this.thumbX; - if ((clickOffset > -this.thumbHalfSize) && (clickOffset < this.thumbHalfSize)) { - this.clickOffsetX = clickOffset; - } else { - this.clickOffsetX = 0; - this.thumbX = event.x; - this.updateThumb(); - this.onValueChanged(this.getValue()); - } - - }; - - this.onMouseReleaseEvent = function(event) { - this.isMoving = false; - }; - - // Public members: - this.setNormalizedValue = function(value) { - if (value < 0.0) { - this.thumbX = this.minThumbX; - } else if (value > 1.0) { - this.thumbX = this.maxThumbX; - } else { - this.thumbX = value * (this.maxThumbX - this.minThumbX) + this.minThumbX; - } - this.updateThumb(); - }; - this.getNormalizedValue = function() { - return (this.thumbX - this.minThumbX) / (this.maxThumbX - this.minThumbX); - }; - - this.setValue = function(value) { - var normValue = (value - this.minValue) / (this.maxValue - this.minValue); - this.setNormalizedValue(normValue); - }; - - this.reset = function(resetValue) { - this.setValue(resetValue); - this.onValueChanged(resetValue); - }; - - this.getValue = function() { - return this.getNormalizedValue() * (this.maxValue - this.minValue) + this.minValue; - }; - - this.getHeight = function() { - return 1.5 * this.thumbSize; - } - - this.onValueChanged = function(value) {}; - - this.destroy = function() { - Overlays.deleteOverlay(this.background); - Overlays.deleteOverlay(this.thumb); - }; - - this.setThumbColor = function(color) { - Overlays.editOverlay(this.thumb, {backgroundColor: { red: color.x*255, green: color.y*255, blue: color.z*255 }}); - }; - this.setBackgroundColor = function(color) { - Overlays.editOverlay(this.background, {backgroundColor: { red: color.x*255, green: color.y*255, blue: color.z*255 }}); - }; - -} - -// The Checkbox class -Checkbox = function(x,y,width,thumbSize) { - - this.background = Overlays.addOverlay("text", { - backgroundColor: { red: 125, green: 125, blue: 255 }, - x: x, - y: y, - width: width, - height: thumbSize, - alpha: 1.0, - backgroundAlpha: 0.5, - visible: true - }); - - this.thumb = Overlays.addOverlay("text", { - backgroundColor: { red: 255, green: 255, blue: 255 }, - textFontSize: 10, - x: x, - y: y, - width: thumbSize, - height: thumbSize, - alpha: 1.0, - backgroundAlpha: 1.0, - visible: true - }); - - - this.thumbSize = thumbSize; - var checkX = x + (0.25 * thumbSize); - var checkY = y + (0.25 * thumbSize); - var boxCheckStatus; - var clickedBox = false; - - - var checkMark = Overlays.addOverlay("text", { - backgroundColor: { red: 0, green: 255, blue: 0 }, - x: checkX, - y: checkY, - width: thumbSize / 2.0, - height: thumbSize / 2.0, - alpha: 1.0, - visible: true - }); - - var unCheckMark = Overlays.addOverlay("image", { - backgroundColor: { red: 255, green: 255, blue: 255 }, - x: checkX + 1.0, - y: checkY + 1.0, - width: thumbSize / 2.5, - height: thumbSize / 2.5, - alpha: 1.0, - visible: !boxCheckStatus - }); - - - this.updateThumb = function() { - if(!clickedBox) { - return; - } +(function () { + var Slider = function(x,y,width,thumbSize) { - boxCheckStatus = !boxCheckStatus; - if (boxCheckStatus) { - Overlays.editOverlay(unCheckMark, { visible: false }); - } else { - Overlays.editOverlay(unCheckMark, { visible: true }); - } + this.background = Overlays.addOverlay("text", { + backgroundColor: { red: 200, green: 200, blue: 255 }, + x: x, + y: y, + width: width, + height: thumbSize, + alpha: 1.0, + backgroundAlpha: 0.5, + visible: true + }); + this.thumb = Overlays.addOverlay("text", { + backgroundColor: { red: 255, green: 255, blue: 255 }, + x: x, + y: y, + width: thumbSize, + height: thumbSize, + alpha: 1.0, + backgroundAlpha: 1.0, + visible: true + }); + + this.thumbSize = thumbSize; - }; + this.thumbHalfSize = 0.5 * thumbSize; + + this.minThumbX = x + this.thumbHalfSize; + this.maxThumbX = x + width - this.thumbHalfSize; + this.thumbX = this.minThumbX; + + this.minValue = 0.0; + this.maxValue = 1.0; - this.isClickableOverlayItem = function(item) { - return (item == this.thumb) || (item == checkMark) || (item == unCheckMark); + this.y = y; + + this.clickOffsetX = 0; + this.isMoving = false; + this.visible = true; }; - this.onMousePressEvent = function(event, clickedOverlay) { - if (!this.isClickableOverlayItem(clickedOverlay)) { - this.isMoving = false; - clickedBox = false; - return; - } - - clickedBox = true; - this.updateThumb(); - this.onValueChanged(this.getValue()); - }; - - - this.onMouseReleaseEvent = function(event) { - this.clickedBox = false; - }; - - - // Public members: - this.setNormalizedValue = function(value) { - boxCheckStatus = value; - }; - - this.getNormalizedValue = function() { - return boxCheckStatus; - }; - - this.setValue = function(value) { - this.setNormalizedValue(value); - }; - - this.reset = function(resetValue) { - this.setValue(resetValue); - - this.onValueChanged(resetValue); - }; - - this.getValue = function() { - return boxCheckStatus; - }; - - this.getHeight = function() { - return 1.5 * this.thumbSize; - } - - this.setterFromWidget = function(value) { - var status = boxCheckStatus; - this.onValueChanged(boxCheckStatus); - this.updateThumb(); - }; - - this.onValueChanged = function(value) {}; - - this.destroy = function() { - Overlays.deleteOverlay(this.background); - Overlays.deleteOverlay(this.thumb); - Overlays.deleteOverlay(checkMark); - Overlays.deleteOverlay(unCheckMark); - }; - - this.setThumbColor = function(color) { - Overlays.editOverlay(this.thumb, { red: 255, green: 255, blue: 255 } ); - }; - this.setBackgroundColor = function(color) { - Overlays.editOverlay(this.background, { red: 125, green: 125, blue: 255 }); - }; - -} - -// The ColorBox class -ColorBox = function(x,y,width,thumbSize) { - var self = this; + Slider.prototype.updateThumb = function() { + var thumbTruePos = this.thumbX - 0.5 * this.thumbSize; + Overlays.editOverlay(this.thumb, { x: thumbTruePos } ); + }; - var slideHeight = thumbSize / 3; - var sliderWidth = width; - this.red = new Slider(x, y, width, slideHeight); - this.green = new Slider(x, y + slideHeight, width, slideHeight); - this.blue = new Slider(x, y + 2 * slideHeight, width, slideHeight); - this.red.setBackgroundColor({x: 1, y: 0, z: 0}); - this.green.setBackgroundColor({x: 0, y: 1, z: 0}); - this.blue.setBackgroundColor({x: 0, y: 0, z: 1}); - - - - this.isClickableOverlayItem = function(item) { - return this.red.isClickableOverlayItem(item) - || this.green.isClickableOverlayItem(item) - || this.blue.isClickableOverlayItem(item); - }; - - this.onMouseMoveEvent = function(event) { - this.red.onMouseMoveEvent(event); - this.green.onMouseMoveEvent(event); - this.blue.onMouseMoveEvent(event); - }; - - this.onMousePressEvent = function(event, clickedOverlay) { - this.red.onMousePressEvent(event, clickedOverlay); - if (this.red.isMoving) { - return; - } - - this.green.onMousePressEvent(event, clickedOverlay); - if (this.green.isMoving) { - return; - } - - this.blue.onMousePressEvent(event, clickedOverlay); - }; - - - this.onMouseReleaseEvent = function(event) { - this.red.onMouseReleaseEvent(event); - this.green.onMouseReleaseEvent(event); - this.blue.onMouseReleaseEvent(event); - }; - - this.setterFromWidget = function(value) { - var color = self.getValue(); - self.onValueChanged(color); - self.updateRGBSliders(color); - }; - - this.red.onValueChanged = this.setterFromWidget; - this.green.onValueChanged = this.setterFromWidget; - this.blue.onValueChanged = this.setterFromWidget; - - this.updateRGBSliders = function(color) { - this.red.setThumbColor({x: color.x, y: 0, z: 0}); - this.green.setThumbColor({x: 0, y: color.y, z: 0}); - this.blue.setThumbColor({x: 0, y: 0, z: color.z}); - }; - - // Public members: - this.setValue = function(value) { - this.red.setValue(value.x); - this.green.setValue(value.y); - this.blue.setValue(value.z); - this.updateRGBSliders(value); - }; - - this.reset = function(resetValue) { - this.setValue(resetValue); - this.onValueChanged(resetValue); - }; - - this.getValue = function() { - var value = {x:this.red.getValue(), y:this.green.getValue(),z:this.blue.getValue()}; - return value; - }; - - this.getHeight = function() { - return 1.5 * this.thumbSize; - } - - this.destroy = function() { - this.red.destroy(); - this.green.destroy(); - this.blue.destroy(); - }; - - this.onValueChanged = function(value) {}; -} - -// The DirectionBox class -DirectionBox = function(x,y,width,thumbSize) { - var self = this; + Slider.prototype.isClickableOverlayItem = function(item) { + return (item == this.thumb) || (item == this.background); + }; - var slideHeight = thumbSize / 2; - var sliderWidth = width; - this.yaw = new Slider(x, y, width, slideHeight); - this.pitch = new Slider(x, y + slideHeight, width, slideHeight); - - - - this.yaw.setThumbColor({x: 1, y: 0, z: 0}); - this.yaw.minValue = -180; - this.yaw.maxValue = +180; - - this.pitch.setThumbColor({x: 0, y: 0, z: 1}); - this.pitch.minValue = -1; - this.pitch.maxValue = +1; - - this.isClickableOverlayItem = function(item) { - return this.yaw.isClickableOverlayItem(item) - || this.pitch.isClickableOverlayItem(item); - }; - - this.onMouseMoveEvent = function(event) { - this.yaw.onMouseMoveEvent(event); - this.pitch.onMouseMoveEvent(event); - }; - - this.onMousePressEvent = function(event, clickedOverlay) { - this.yaw.onMousePressEvent(event, clickedOverlay); - if (this.yaw.isMoving) { - return; - } - this.pitch.onMousePressEvent(event, clickedOverlay); - }; - - this.onMouseReleaseEvent = function(event) { - this.yaw.onMouseReleaseEvent(event); - this.pitch.onMouseReleaseEvent(event); - }; - - this.setterFromWidget = function(value) { - var yawPitch = self.getValue(); - self.onValueChanged(yawPitch); - }; - - this.yaw.onValueChanged = this.setterFromWidget; - this.pitch.onValueChanged = this.setterFromWidget; - - // Public members: - this.setValue = function(direction) { - var flatXZ = Math.sqrt(direction.x * direction.x + direction.z * direction.z); - if (flatXZ > 0.0) { - var flatX = direction.x / flatXZ; - var flatZ = direction.z / flatXZ; - var yaw = Math.acos(flatX) * 180 / Math.PI; - if (flatZ < 0) { - yaw = -yaw; + Slider.prototype.onMouseMoveEvent = function(event) { + if (this.isMoving) { + var newThumbX = event.x - this.clickOffsetX; + if (newThumbX < this.minThumbX) { + newThumbX = this.minThumbX; + } + if (newThumbX > this.maxThumbX) { + newThumbX = this.maxThumbX; + } + this.thumbX = newThumbX; + this.updateThumb(); + this.onValueChanged(this.getValue()); } - this.yaw.setValue(yaw); + }; + + Slider.prototype.onMousePressEvent = function(event, clickedOverlay) { + if (!this.isClickableOverlayItem(clickedOverlay)) { + this.isMoving = false; + return; + } + this.isMoving = true; + var clickOffset = event.x - this.thumbX; + if ((clickOffset > -this.thumbHalfSize) && (clickOffset < this.thumbHalfSize)) { + this.clickOffsetX = clickOffset; + } else { + this.clickOffsetX = 0; + this.thumbX = event.x; + this.updateThumb(); + this.onValueChanged(this.getValue()); + } + + }; + + Slider.prototype.onMouseReleaseEvent = function(event) { + this.isMoving = false; + }; + + // Public members: + Slider.prototype.setNormalizedValue = function(value) { + if (value < 0.0) { + this.thumbX = this.minThumbX; + } else if (value > 1.0) { + this.thumbX = this.maxThumbX; + } else { + this.thumbX = value * (this.maxThumbX - this.minThumbX) + this.minThumbX; + } + this.updateThumb(); + }; + Slider.prototype.getNormalizedValue = function() { + return (this.thumbX - this.minThumbX) / (this.maxThumbX - this.minThumbX); + }; + + Slider.prototype.setValue = function(value) { + var normValue = (value - this.minValue) / (this.maxValue - this.minValue); + this.setNormalizedValue(normValue); + }; + + Slider.prototype.reset = function(resetValue) { + this.setValue(resetValue); + this.onValueChanged(resetValue); + }; + + Slider.prototype.getValue = function() { + return this.getNormalizedValue() * (this.maxValue - this.minValue) + this.minValue; + }; + + Slider.prototype.getHeight = function() { + if(!this.visible) { + return 0; + } + return 1.5 * this.thumbSize; + }; + + Slider.prototype.moveUp = function(newY) { + Overlays.editOverlay(this.background, {y: newY}); + Overlays.editOverlay(this.thumb, {y: newY}); + }; + + Slider.prototype.moveDown = function() { + Overlays.editOverlay(this.background, {y: this.y}); + Overlays.editOverlay(this.thumb, {y: this.y}); + }; + + Slider.prototype.onValueChanged = function(value) {}; + + Slider.prototype.hide = function() { + Overlays.editOverlay(this.background, {visible: false}); + Overlays.editOverlay(this.thumb, {visible: false}); + this.visible = false; + }; + + Slider.prototype.show = function() { + Overlays.editOverlay(this.background, {visible: true}); + Overlays.editOverlay(this.thumb, {visible: true}); + this.visible = true; + }; + + Slider.prototype.destroy = function() { + Overlays.deleteOverlay(this.background); + Overlays.deleteOverlay(this.thumb); + }; + + Slider.prototype.setThumbColor = function(color) { + Overlays.editOverlay(this.thumb, {backgroundColor: { red: color.x*255, green: color.y*255, blue: color.z*255 }}); + }; + Slider.prototype.setBackgroundColor = function(color) { + Overlays.editOverlay(this.background, {backgroundColor: { red: color.x*255, green: color.y*255, blue: color.z*255 }}); + }; + this.Slider = Slider; + + // The Checkbox class + var Checkbox = function(x,y,width,thumbSize) { + + this.background = Overlays.addOverlay("text", { + backgroundColor: { red: 125, green: 125, blue: 255 }, + x: x, + y: y, + width: width, + height: thumbSize, + alpha: 1.0, + backgroundAlpha: 0.5, + visible: true + }); + + this.thumb = Overlays.addOverlay("text", { + backgroundColor: { red: 255, green: 255, blue: 255 }, + textFontSize: 10, + x: x, + y: y, + width: thumbSize, + height: thumbSize, + alpha: 1.0, + backgroundAlpha: 1.0, + visible: true + }); + + + this.thumbSize = thumbSize; + var checkX = x + (0.25 * thumbSize); + var checkY = y + (0.25 * thumbSize); + this.y = y; + this.boxCheckStatus = true; + this.clickedBox = false; + this.visible = true; + + + this.checkMark = Overlays.addOverlay("text", { + backgroundColor: { red: 0, green: 255, blue: 0 }, + x: checkX, + y: checkY, + width: thumbSize / 2.0, + height: thumbSize / 2.0, + alpha: 1.0, + visible: true + }); + + this.unCheckMark = Overlays.addOverlay("image", { + backgroundColor: { red: 255, green: 255, blue: 255 }, + x: checkX + 1.0, + y: checkY + 1.0, + width: thumbSize / 2.5, + height: thumbSize / 2.5, + alpha: 1.0, + visible: false + }); + }; + + Checkbox.prototype.updateThumb = function() { + + Overlays.editOverlay(this.unCheckMark, { visible: !this.boxCheckStatus }); + + }; + + Checkbox.prototype.isClickableOverlayItem = function(item) { + return (item == this.thumb) || (item == this.checkMark) || (item == this.unCheckMark); + }; + + Checkbox.prototype.onMousePressEvent = function(event, clickedOverlay) { + + if (!this.isClickableOverlayItem(clickedOverlay)) { + return; + } + + this.boxCheckStatus = !this.boxCheckStatus; + this.onValueChanged(this.getValue()); + this.updateThumb(); + }; + + + Checkbox.prototype.onMouseReleaseEvent = function(event) { }; + + + // Public members: + + Checkbox.prototype.setValue = function(value) { + this.boxCheckStatus = value; + }; + + Checkbox.prototype.reset = function(resetValue) { + this.setValue(resetValue); + }; + + Checkbox.prototype.getValue = function() { + return this.boxCheckStatus; + }; + + Checkbox.prototype.getHeight = function() { + if(!this.visible) { + return 0; } - this.pitch.setValue(direction.y); + return 1.5 * this.thumbSize; + }; + + Checkbox.prototype.moveUp = function(newY) { + Overlays.editOverlay(this.background, {y: newY}); + Overlays.editOverlay(this.thumb, {y: newY}); + Overlays.editOverlay(this.checkMark, {y: newY}); + Overlays.editOverlay(this.unCheckMark, {y: newY}); }; - this.reset = function(resetValue) { - this.setValue(resetValue); - this.onValueChanged(resetValue); + Checkbox.prototype.moveDown = function() { + Overlays.editOverlay(this.background, {y: this.y}); + Overlays.editOverlay(this.thumb, {y: this.y}); + Overlays.editOverlay(this.checkMark, {y: this.y}); + Overlays.editOverlay(this.unCheckMark, {y: this.y}); }; + + Checkbox.prototype.setterFromWidget = function(value) { + this.updateThumb(); + }; + + Checkbox.prototype.onValueChanged = function(value) { }; - this.getValue = function() { - var dirZ = this.pitch.getValue(); - var yaw = this.yaw.getValue() * Math.PI / 180; - var cosY = Math.sqrt(1 - dirZ*dirZ); - var value = {x:cosY * Math.cos(yaw), y:dirZ, z: cosY * Math.sin(yaw)}; - return value; - }; - - this.getHeight = function() { - return 1.5 * this.thumbSize; + Checkbox.prototype.hide = function() { + Overlays.editOverlay(this.background, {visible: false}); + Overlays.editOverlay(this.thumb, {visible: false}); + Overlays.editOverlay(this.checkMark, {visible: false}); + Overlays.editOverlay(this.unCheckMark, {visible: false}); + this.visible = false; } - this.destroy = function() { - this.yaw.destroy(); - this.pitch.destroy(); + Checkbox.prototype.show = function() { + Overlays.editOverlay(this.background, {visible: true}); + Overlays.editOverlay(this.thumb, {visible: true}); + Overlays.editOverlay(this.checkMark, {visible: true}); + Overlays.editOverlay(this.unCheckMark, {visible: true}); + this.visible = true; + } + + Checkbox.prototype.destroy = function() { + Overlays.deleteOverlay(this.background); + Overlays.deleteOverlay(this.thumb); + Overlays.deleteOverlay(this.checkMark); + Overlays.deleteOverlay(this.unCheckMark); + }; + + Checkbox.prototype.setThumbColor = function(color) { + Overlays.editOverlay(this.thumb, { red: 255, green: 255, blue: 255 } ); + }; + Checkbox.prototype.setBackgroundColor = function(color) { + Overlays.editOverlay(this.background, { red: 125, green: 125, blue: 255 }); + }; + this.Checkbox = Checkbox; + + // The ColorBox class + var ColorBox = function(x,y,width,thumbSize) { + var self = this; + + var slideHeight = thumbSize / 3; + var sliderWidth = width; + this.red = new Slider(x, y, width, slideHeight); + this.green = new Slider(x, y + slideHeight, width, slideHeight); + this.blue = new Slider(x, y + 2 * slideHeight, width, slideHeight); + this.red.setBackgroundColor({x: 1, y: 0, z: 0}); + this.green.setBackgroundColor({x: 0, y: 1, z: 0}); + this.blue.setBackgroundColor({x: 0, y: 0, z: 1}); + + this.red.onValueChanged = this.setterFromWidget; + this.green.onValueChanged = this.setterFromWidget; + this.blue.onValueChanged = this.setterFromWidget; + + this.visible = true; + }; + + ColorBox.prototype.isClickableOverlayItem = function(item) { + return this.red.isClickableOverlayItem(item) + || this.green.isClickableOverlayItem(item) + || this.blue.isClickableOverlayItem(item); + }; + + ColorBox.prototype.onMouseMoveEvent = function(event) { + this.red.onMouseMoveEvent(event); + this.green.onMouseMoveEvent(event); + this.blue.onMouseMoveEvent(event); + }; + + ColorBox.prototype.onMousePressEvent = function(event, clickedOverlay) { + this.red.onMousePressEvent(event, clickedOverlay); + if (this.red.isMoving) { + return; + } + + this.green.onMousePressEvent(event, clickedOverlay); + if (this.green.isMoving) { + return; + } + + this.blue.onMousePressEvent(event, clickedOverlay); + }; + + + ColorBox.prototype.onMouseReleaseEvent = function(event) { + this.red.onMouseReleaseEvent(event); + this.green.onMouseReleaseEvent(event); + this.blue.onMouseReleaseEvent(event); + }; + + ColorBox.prototype.setterFromWidget = function(value) { + var color = this.getValue(); + this.onValueChanged(color); + this.updateRGBSliders(color); + }; + + ColorBox.prototype.updateRGBSliders = function(color) { + this.red.setThumbColor({x: color.x, y: 0, z: 0}); + this.green.setThumbColor({x: 0, y: color.y, z: 0}); + this.blue.setThumbColor({x: 0, y: 0, z: color.z}); + }; + + // Public members: + ColorBox.prototype.setValue = function(value) { + this.red.setValue(value.x); + this.green.setValue(value.y); + this.blue.setValue(value.z); + this.updateRGBSliders(value); + }; + + ColorBox.prototype.reset = function(resetValue) { + this.setValue(resetValue); + this.onValueChanged(resetValue); + }; + + ColorBox.prototype.getValue = function() { + var value = {x:this.red.getValue(), y:this.green.getValue(),z:this.blue.getValue()}; + return value; + }; + + ColorBox.prototype.getHeight = function() { + if(!this.visible) { + return 0; + } + return 1.5 * this.thumbSize; + }; + + ColorBox.prototype.moveUp = function(newY) { + this.red.moveUp(newY); + this.green.moveUp(newY); + this.blue.moveUp(newY); }; - this.onValueChanged = function(value) {}; -} + ColorBox.prototype.moveDown = function() { + this.red.moveDown(); + this.green.moveDown(); + this.blue.moveDown(); + }; + ColorBox.prototype.hide = function() { + this.red.hide(); + this.green.hide(); + this.blue.hide(); + this.visible = false; + } - -var textFontSize = 12; - -// TODO: Make collapsable -function CollapsablePanelItem(name, x, y, textWidth, height) { - this.name = name; - this.height = height; - - var topMargin = (height - textFontSize); - - this.thumb = Overlays.addOverlay("text", { - backgroundColor: { red: 220, green: 220, blue: 220 }, - textFontSize: 10, - x: x, - y: y, - width: rawHeight, - height: rawHeight, - alpha: 1.0, - backgroundAlpha: 1.0, - visible: true - }); + ColorBox.prototype.show = function() { + this.red.show(); + this.green.show(); + this.blue.show(); + this.visible = true; + } - this.title = Overlays.addOverlay("text", { - backgroundColor: { red: 255, green: 255, blue: 255 }, - x: x + rawHeight, - y: y, - width: textWidth, - height: height, - alpha: 1.0, - backgroundAlpha: 0.5, - visible: true, - text: name, - font: {size: textFontSize}, - topMargin: topMargin, - }); + ColorBox.prototype.destroy = function() { + this.red.destroy(); + this.green.destroy(); + this.blue.destroy(); + }; + + ColorBox.prototype.onValueChanged = function(value) {}; + this.ColorBox = ColorBox; + + // The DirectionBox class + var DirectionBox = function(x,y,width,thumbSize) { + var self = this; + + var slideHeight = thumbSize / 2; + var sliderWidth = width; + this.yaw = new Slider(x, y, width, slideHeight); + this.pitch = new Slider(x, y + slideHeight, width, slideHeight); + + + this.yaw.setThumbColor({x: 1, y: 0, z: 0}); + this.yaw.minValue = -180; + this.yaw.maxValue = +180; + + this.pitch.setThumbColor({x: 0, y: 0, z: 1}); + this.pitch.minValue = -1; + this.pitch.maxValue = +1; + + this.yaw.onValueChanged = this.setterFromWidget; + this.pitch.onValueChanged = this.setterFromWidget; - this.destroy = function() { + this.visible = true; + }; + + DirectionBox.prototype.isClickableOverlayItem = function(item) { + return this.yaw.isClickableOverlayItem(item) + || this.pitch.isClickableOverlayItem(item); + }; + + DirectionBox.prototype.onMouseMoveEvent = function(event) { + this.yaw.onMouseMoveEvent(event); + this.pitch.onMouseMoveEvent(event); + }; + + DirectionBox.prototype.onMousePressEvent = function(event, clickedOverlay) { + this.yaw.onMousePressEvent(event, clickedOverlay); + if (this.yaw.isMoving) { + return; + } + this.pitch.onMousePressEvent(event, clickedOverlay); + }; + + DirectionBox.prototype.onMouseReleaseEvent = function(event) { + this.yaw.onMouseReleaseEvent(event); + this.pitch.onMouseReleaseEvent(event); + }; + + DirectionBox.prototype.setterFromWidget = function(value) { + var yawPitch = this.getValue(); + this.onValueChanged(yawPitch); + }; + + // Public members: + DirectionBox.prototype.setValue = function(direction) { + var flatXZ = Math.sqrt(direction.x * direction.x + direction.z * direction.z); + if (flatXZ > 0.0) { + var flatX = direction.x / flatXZ; + var flatZ = direction.z / flatXZ; + var yaw = Math.acos(flatX) * 180 / Math.PI; + if (flatZ < 0) { + yaw = -yaw; + } + this.yaw.setValue(yaw); + } + this.pitch.setValue(direction.y); + }; + + DirectionBox.prototype.reset = function(resetValue) { + this.setValue(resetValue); + this.onValueChanged(resetValue); + }; + + DirectionBox.prototype.getValue = function() { + var dirZ = this.pitch.getValue(); + var yaw = this.yaw.getValue() * Math.PI / 180; + var cosY = Math.sqrt(1 - dirZ*dirZ); + var value = {x:cosY * Math.cos(yaw), y:dirZ, z: cosY * Math.sin(yaw)}; + return value; + }; + + DirectionBox.prototype.getHeight = function() { + if(!this.visible) { + return 0; + } + return 1.5 * this.thumbSize; + }; + + DirectionBox.prototype.moveUp = function(newY) { + this.pitch.moveUp(newY); + this.yaw.moveUp(newY); + }; + + DirectionBox.prototype.moveDown = function(newY) { + this.pitch.moveDown(); + this.yaw.moveDown(); + }; + + DirectionBox.prototype.hide = function() { + this.pitch.hide(); + this.yaw.hide(); + this.visible = false; + } + + DirectionBox.prototype.show = function() { + this.pitch.show(); + this.yaw.show(); + this.visible = true; + } + + DirectionBox.prototype.destroy = function() { + this.yaw.destroy(); + this.pitch.destroy(); + }; + + DirectionBox.prototype.onValueChanged = function(value) {}; + this.DirectionBox = DirectionBox; + + var textFontSize = 12; + + var CollapsablePanelItem = function (name, x, y, textWidth, height) { + this.name = name; + this.height = height; + this.y = y; + this.isCollapsable = true; + + var topMargin = (height - 1.5 * textFontSize); + + this.thumb = Overlays.addOverlay("text", { + backgroundColor: { red: 220, green: 220, blue: 220 }, + textFontSize: 10, + x: x, + y: y, + width: rawHeight, + height: rawHeight, + alpha: 1.0, + backgroundAlpha: 1.0, + visible: true + }); + + this.title = Overlays.addOverlay("text", { + backgroundColor: { red: 255, green: 255, blue: 255 }, + x: x + rawHeight * 1.5, + y: y, + width: textWidth, + height: height, + alpha: 1.0, + backgroundAlpha: 0.5, + visible: true, + text: " " + name, + font: {size: textFontSize}, + topMargin: topMargin + }); + }; + + CollapsablePanelItem.prototype.destroy = function() { Overlays.deleteOverlay(this.title); Overlays.deleteOverlay(this.thumb); - if (this.widget != null) { - this.widget.destroy(); - } - } -} - -function PanelItem(name, setter, getter, displayer, x, y, textWidth, valueWidth, height) { - //print("creating panel item: " + name); + }; - this.name = name; - - this.displayer = typeof displayer !== 'undefined' ? displayer : function(value) { - if(value == true) { - return "On"; - } else if (value == false) { - return "Off"; - } - return value; - }; - - var topMargin = (height - textFontSize); - this.title = Overlays.addOverlay("text", { - backgroundColor: { red: 255, green: 255, blue: 255 }, - x: x, - y: y, - width: textWidth, - height: height, - alpha: 1.0, - backgroundAlpha: 0.5, - visible: true, - text: name, - font: {size: textFontSize}, - topMargin: topMargin, - }); - - this.value = Overlays.addOverlay("text", { - backgroundColor: { red: 255, green: 255, blue: 255 }, - x: x + textWidth, - y: y, - width: valueWidth, - height: height, - alpha: 1.0, - backgroundAlpha: 0.5, - visible: true, - text: this.displayer(getter()), - font: {size: textFontSize}, - topMargin: topMargin - }); - - this.getter = getter; - this.resetValue = getter(); - - this.setter = function(value) { - - setter(value); - - Overlays.editOverlay(this.value, {text: this.displayer(getter())}); - - if (this.widget) { - this.widget.setValue(value); - } - - //print("successfully set value of widget to " + value); - }; - this.setterFromWidget = function(value) { - setter(value); - // ANd loop back the value after the final setter has been called - var value = getter(); - - if (this.widget) { - this.widget.setValue(value); - } - Overlays.editOverlay(this.value, {text: this.displayer(value)}); - }; - - this.widget = null; + CollapsablePanelItem.prototype.hide = function() { + Overlays.editOverlay(this.title, {visible: false}); + Overlays.editOverlay(this.thumb, {visible: false}); - this.destroy = function() { - Overlays.deleteOverlay(this.title); - Overlays.deleteOverlay(this.value); if (this.widget != null) { - this.widget.destroy(); + this.widget.hide(); + } + }; + + CollapsablePanelItem.prototype.show = function() { + Overlays.editOverlay(this.title, {visible: true}); + Overlays.editOverlay(this.thumb, {visible: true}); + + if (this.widget != null) { + this.widget.show(); + } + }; + + CollapsablePanelItem.prototype.moveUp = function(newY) { + Overlays.editOverlay(this.title, {y: newY}); + Overlays.editOverlay(this.thumb, {y: newY}); + + if (this.widget != null) { + this.widget.moveUp(newY); } } -} -var textWidth = 180; -var valueWidth = 100; -var widgetWidth = 300; -var rawHeight = 20; -var rawYDelta = rawHeight * 1.5; + CollapsablePanelItem.prototype.moveDown = function() { + Overlays.editOverlay(this.title, {y: this.y}); + Overlays.editOverlay(this.thumb, {y: this.y}); -Panel = function(x, y) { + if (this.widget != null) { + this.widget.moveDown(); + } - this.x = x; - this.y = y; - this.nextY = y; + } - this.widgetX = x + textWidth + valueWidth; + this.CollapsablePanelItem = CollapsablePanelItem; + + var PanelItem = function (name, setter, getter, displayer, x, y, textWidth, valueWidth, height) { + //print("creating panel item: " + name); + this.isCollapsable = false; + this.name = name; + this.y = y; + this.isCollapsed = false; + + this.displayer = typeof displayer !== 'undefined' ? displayer : function(value) { + if(value == true) { + return "On"; + } else if (value == false) { + return "Off"; + } + return value.toFixed(2); + }; + + var topMargin = (height - 1.5 * textFontSize); + this.title = Overlays.addOverlay("text", { + backgroundColor: { red: 255, green: 255, blue: 255 }, + x: x, + y: y, + width: textWidth, + height: height, + alpha: 1.0, + backgroundAlpha: 0.5, + visible: true, + text: " " + name, + font: {size: textFontSize}, + topMargin: topMargin + }); + + this.value = Overlays.addOverlay("text", { + backgroundColor: { red: 255, green: 255, blue: 255 }, + x: x + textWidth, + y: y, + width: valueWidth, + height: height, + alpha: 1.0, + backgroundAlpha: 0.5, + visible: true, + text: this.displayer(getter()), + font: {size: textFontSize}, + topMargin: topMargin + }); + + this.getter = getter; + this.resetValue = getter(); + + this.setter = function(value) { + + setter(value); - this.items = new Array(); - this.activeWidget = null; + Overlays.editOverlay(this.value, {text: this.displayer(getter())}); - this.mouseMoveEvent = function(event) { + if (this.widget) { + this.widget.setValue(value); + } + + //print("successfully set value of widget to " + value); + }; + this.setterFromWidget = function(value) { + setter(value); + // ANd loop back the value after the final setter has been called + var value = getter(); + + if (this.widget) { + this.widget.setValue(value); + } + Overlays.editOverlay(this.value, {text: this.displayer(value)}); + }; + + this.widget = null; + }; + + PanelItem.prototype.hide = function() { + Overlays.editOverlay(this.title, {visible: false}); + Overlays.editOverlay(this.value, {visible: false}); + + if (this.widget != null) { + this.widget.hide(); + } + }; + + + PanelItem.prototype.show = function() { + Overlays.editOverlay(this.title, {visible: true}); + Overlays.editOverlay(this.value, {visible: true}); + + if (this.widget != null) { + this.widget.show(); + } + + }; + + PanelItem.prototype.moveUp = function(newY) { + + Overlays.editOverlay(this.title, {y: newY}); + Overlays.editOverlay(this.value, {y: newY}); + + if (this.widget != null) { + this.widget.moveUp(newY); + } + + }; + + PanelItem.prototype.moveDown = function() { + + Overlays.editOverlay(this.title, {y: this.y}); + Overlays.editOverlay(this.value, {y: this.y}); + + if (this.widget != null) { + this.widget.moveDown(); + } + + }; + + PanelItem.prototype.destroy = function() { + Overlays.deleteOverlay(this.title); + Overlays.deleteOverlay(this.value); + + if (this.widget != null) { + this.widget.destroy(); + } + }; + this.PanelItem = PanelItem; + + var textWidth = 180; + var valueWidth = 100; + var widgetWidth = 300; + var rawHeight = 20; + var rawYDelta = rawHeight * 1.5; + + var Panel = function(x, y) { + + this.x = x; + this.y = y; + + this.nextY = y; print("creating panel at x: " + this.x + " y: " + this.y); + + this.widgetX = x + textWidth + valueWidth; + + this.items = new Array(); + this.activeWidget = null; + this.visible = true; + this.indentation = 30; + + }; + + Panel.prototype.mouseMoveEvent = function(event) { if (this.activeWidget) { this.activeWidget.onMouseMoveEvent(event); } }; - - this.mousePressEvent = function(event) { + + Panel.prototype.mousePressEvent = function(event) { // Make sure we quitted previous widget if (this.activeWidget) { this.activeWidget.onMouseReleaseEvent(event); } this.activeWidget = null; - + var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); - + this.handleCollapse(clickedOverlay); + // If the user clicked any of the slider background then... for (var i in this.items) { + var item = this.items[i]; var widget = this.items[i].widget; - + if (widget.isClickableOverlayItem(clickedOverlay)) { this.activeWidget = widget; this.activeWidget.onMousePressEvent(event, clickedOverlay); - break; - } + + } } }; - - // Reset panel item upon double-clicking - this.mouseDoublePressEvent = function(event) { - + + // Reset panel item upon double-clicking + Panel.prototype.mouseDoublePressEvent = function(event) { var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); + this.handleReset(clickedOverlay); + } + + Panel.prototype.handleReset = function (clickedOverlay) { for (var i in this.items) { - + var item = this.items[i]; var widget = item.widget; + + if (item.isSubPanel && widget) { + widget.handleReset(clickedOverlay); + } if (clickedOverlay == item.title) { item.activeWidget = widget; - + item.activeWidget.reset(item.resetValue); break; } - } - } + } + }; - this.mouseReleaseEvent = function(event) { + Panel.prototype.handleCollapse = function (clickedOverlay) { + + for (var i in this.items) { + + var item = this.items[i]; + var widget = item.widget; + + if (item.isSubPanel && widget) { + widget.handleCollapse(clickedOverlay); + } + + if (!item.isCollapsed && item.isCollapsable && clickedOverlay == item.thumb) { + this.collapse(clickedOverlay); + item.isCollapsed = true; + break; + } else if (item.isCollapsed && item.isCollapsable && clickedOverlay == item.thumb) { + this.expand(clickedOverlay); + item.isCollapsed = false; + } + } + }; + + Panel.prototype.collapse = function (clickedOverlay) { + var keys = Object.keys(this.items); + + for (var i = 0; i < keys.length; ++i) { + var item = this.items[keys[i]]; + + if(item.isCollapsable && clickedOverlay == item.thumb) { + var panel = item.widget; + panel.hide(); + break; + } + + } + + // Now recalculate new heights of subsequent widgets + for(var j = i + 1; j < keys.length; ++j) { + + this.items[keys[j]].moveUp(this.getCurrentY(keys[j])); + + } + + }; + + + Panel.prototype.expand = function (clickedOverlay) { + var keys = Object.keys(this.items); + + for (var i = 0; i < keys.length; ++i) { + var item = this.items[keys[i]]; + + if(item.isCollapsable && clickedOverlay == item.thumb) { + var panel = item.widget; + panel.show(); + break; + } + + } + + // Now recalculate new heights of subsequent widgets + for(var j = i + 1; j < keys.length; ++j) { + this.items[keys[j]].moveDown(); + } + + }; + + + Panel.prototype.mouseReleaseEvent = function(event) { if (this.activeWidget) { this.activeWidget.onMouseReleaseEvent(event); } this.activeWidget = null; }; - - this.onMousePressEvent = function(event, clickedOverlay) { + + Panel.prototype.onMousePressEvent = function(event, clickedOverlay) { for (var i in this.items) { var item = this.items[i]; if(item.widget.isClickableOverlayItem(clickedOverlay)) { @@ -674,27 +936,19 @@ Panel = function(x, y) { item.activeWidget.onMousePressEvent(event,clickedOverlay); } } - } - - this.reset = function() { - for (var i in this.items) { - var item = this.items[i]; - if (item.activeWidget) { - item.activeWidget.reset(item.resetValue); - } - } - } - - this.onMouseMoveEvent = function(event) { + }; + + + Panel.prototype.onMouseMoveEvent = function(event) { for (var i in this.items) { var item = this.items[i]; if (item.activeWidget) { item.activeWidget.onMouseMoveEvent(event); } } - } - - this.onMouseReleaseEvent = function(event, clickedOverlay) { + }; + + Panel.prototype.onMouseReleaseEvent = function(event, clickedOverlay) { for (var i in this.items) { var item = this.items[i]; if (item.activeWidget) { @@ -702,47 +956,46 @@ Panel = function(x, y) { } item.activeWidget = null; } - } - - this.onMouseDoublePressEvent = function(event, clickedOverlay) { - + }; + + Panel.prototype.onMouseDoublePressEvent = function(event, clickedOverlay) { for (var i in this.items) { var item = this.items[i]; if (item.activeWidget) { item.activeWidget.onMouseDoublePressEvent(event); } } - } - - this.newSlider = function(name, minValue, maxValue, setValue, getValue, displayValue) { + }; + + Panel.prototype.newSlider = function(name, minValue, maxValue, setValue, getValue, displayValue) { this.nextY = this.y + this.getHeight(); - var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); + var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); var slider = new Slider(this.widgetX, this.nextY, widgetWidth, rawHeight); slider.minValue = minValue; slider.maxValue = maxValue; - + item.widget = slider; item.widget.onValueChanged = function(value) { item.setterFromWidget(value); }; item.setter(getValue()); this.items[name] = item; - this.nextY += rawYDelta; - }; - this.newCheckbox = function(name, setValue, getValue, displayValue) { + }; + + Panel.prototype.newCheckbox = function(name, setValue, getValue, displayValue) { var display; if (displayValue == true) { display = function() {return "On";}; } else if (displayValue == false) { display = function() {return "Off";}; } - + this.nextY = this.y + this.getHeight(); - + var item = new PanelItem(name, setValue, getValue, display, this.x, this.nextY, textWidth, valueWidth, rawHeight); - + var checkbox = new Checkbox(this.widgetX, this.nextY, widgetWidth, rawHeight); - + item.widget = checkbox; item.widget.onValueChanged = function(value) { item.setterFromWidget(value); }; item.setter(getValue()); @@ -750,136 +1003,180 @@ Panel = function(x, y) { //print("created Item... checkbox=" + name); }; - - this.newColorBox = function(name, setValue, getValue, displayValue) { + + Panel.prototype.newColorBox = function(name, setValue, getValue, displayValue) { this.nextY = this.y + this.getHeight(); - + var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); - + var colorBox = new ColorBox(this.widgetX, this.nextY, widgetWidth, rawHeight); - + item.widget = colorBox; item.widget.onValueChanged = function(value) { item.setterFromWidget(value); }; item.setter(getValue()); this.items[name] = item; - this.nextY += rawYDelta; + // print("created Item... colorBox=" + name); }; - -<<<<<<< Updated upstream - this.newDirectionBox = function(name, setValue, getValue, displayValue) { -======= - this.newDirectionBox= function(name, setValue, getValue, displayValue) { + + Panel.prototype.newDirectionBox = function(name, setValue, getValue, displayValue) { this.nextY = this.y + this.getHeight(); ->>>>>>> Stashed changes - + var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); var directionBox = new DirectionBox(this.widgetX, this.nextY, widgetWidth, rawHeight); - + item.widget = directionBox; item.widget.onValueChanged = function(value) { item.setterFromWidget(value); }; item.setter(getValue()); this.items[name] = item; - this.nextY += rawYDelta; + // print("created Item... directionBox=" + name); }; - -<<<<<<< Updated upstream - this.newSubPanel = function(name) { - var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); - - var panel = new Panel(this.widgetX, this.nextY); - - item.widget = panel; - - item.widget.onValueChanged = function(value) { item.setterFromWidget(value); }; + - this.nextY += rawYDelta; - - }; - -======= - var indentation = 30; - - this.newSubPanel = function(name) { + + Panel.prototype.newSubPanel = function(name) { //TODO: make collapsable, fix double-press event this.nextY = this.y + this.getHeight(); - + var item = new CollapsablePanelItem(name, this.x, this.nextY, textWidth, rawHeight, panel); item.isSubPanel = true; + + this.nextY += 1.5 * item.height; + + var subPanel = new Panel(this.x + this.indentation, this.nextY); - this.nextY += 1.5 * item.height; - - var subPanel = new Panel(this.x + indentation, this.nextY); - item.widget = subPanel; this.items[name] = item; return subPanel; // print("created Item... subPanel=" + name); }; - - this.onValueChanged = function(value) { + + Panel.prototype.onValueChanged = function(value) { for (var i in this.items) { this.items[i].widget.onValueChanged(value); } - } ->>>>>>> Stashed changes - - this.destroy = function() { - for (var i in this.items) { - this.items[i].destroy(); - } - } - - - this.set = function(name, value) { + }; + + + Panel.prototype.set = function(name, value) { var item = this.items[name]; if (item != null) { return item.setter(value); } return null; - } - - this.get = function(name) { + }; + + Panel.prototype.get = function(name) { var item = this.items[name]; if (item != null) { return item.getter(); } return null; - } - - this.update = function(name) { + }; + + Panel.prototype.update = function(name) { var item = this.items[name]; if (item != null) { return item.setter(item.getter()); } return null; - } - - this.isClickableOverlayItem = function(item) { + }; + + Panel.prototype.isClickableOverlayItem = function(item) { for (var i in this.items) { if (this.items[i].widget.isClickableOverlayItem(item)) { return true; } } return false; - } - - this.getHeight = function() { + }; + + Panel.prototype.getHeight = function(show) { var height = 0; - + for (var i in this.items) { + height += this.items[i].widget.getHeight(); - if(this.items[i].isSubPanel) { + // if(show) { + // print("widget: " + i + " height: " + this.items[i].widget.getHeight()); + + // } + if(this.items[i].isSubPanel && this.items[i].widget.visible) { + height += 1.5 * rawHeight; - } + + } + } - return height; - } + }; -}; + Panel.prototype.moveUp = function() { + for (var i in this.items) { + this.items[i].widget.moveUp(); + } + + }; + + Panel.prototype.moveDown = function() { + for (var i in this.items) { + this.items[i].widget.moveDown(); + } + }; + + Panel.prototype.getCurrentY = function(key) { + var height = 0; + var keys = Object.keys(this.items); + + for (var i = 0; i < keys.indexOf(key); ++i) { + var item = this.items[keys[i]]; + + height += item.widget.getHeight(); + + if(item.isSubPanel) { + height += 1.5 * rawHeight; + + } + } + + return this.y + height; + }; + + + Panel.prototype.hide = function() { + for (var i in this.items) { + if(this.items[i].isSubPanel) { + this.items[i].widget.hide(); + } + this.items[i].hide(); + } + this.visible = false; + }; + + Panel.prototype.show = function() { + for (var i in this.items) { + if(this.items[i].isSubPanel) { + this.items[i].widget.show(); + } + this.items[i].show(); + } + this.visible = true; + }; + Panel.prototype.destroy = function() { + for (var i in this.items) { + + if(this.items[i].isSubPanel) { + this.items[i].widget.destroy(); + } + this.items[i].destroy(); + } + }; + + + this.Panel = Panel; +})(); \ No newline at end of file From b7d195e8173360ebb45fc1d9e7a771fc030a245c Mon Sep 17 00:00:00 2001 From: bwent Date: Mon, 27 Jul 2015 13:56:25 -0700 Subject: [PATCH 127/242] more subpanel fixes --- examples/utilities/tools/cookies.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/utilities/tools/cookies.js b/examples/utilities/tools/cookies.js index 9d064385bf..44017c66da 100644 --- a/examples/utilities/tools/cookies.js +++ b/examples/utilities/tools/cookies.js @@ -888,10 +888,10 @@ } } + // Now recalculate new heights of subsequent widgets for(var j = i + 1; j < keys.length; ++j) { - this.items[keys[j]].moveUp(this.getCurrentY(keys[j])); } From 774b4851c323f5adec20173a45f9aa3474edb9fe Mon Sep 17 00:00:00 2001 From: bwent Date: Tue, 28 Jul 2015 12:18:26 -0700 Subject: [PATCH 128/242] Add tab to highlight widgets and update using left and right arrow keys --- examples/utilities/tools/cookies.js | 453 +++++++++++++++++----------- 1 file changed, 282 insertions(+), 171 deletions(-) diff --git a/examples/utilities/tools/cookies.js b/examples/utilities/tools/cookies.js index 44017c66da..b2dc8c1da7 100644 --- a/examples/utilities/tools/cookies.js +++ b/examples/utilities/tools/cookies.js @@ -9,6 +9,26 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // + +HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; + +var SCALE = 1000; +var THUMB_COLOR = { + red: 150, + green: 150, + blue: 150 +}; +var THUMB_HIGHLIGHT = { + red: 255, + green: 255, + blue: 255 +}; +var CHECK_MARK_COLOR = { + red: 70, + green: 70, + blue: 90 +}; + // The Slider class (function () { var Slider = function(x,y,width,thumbSize) { @@ -24,7 +44,7 @@ visible: true }); this.thumb = Overlays.addOverlay("text", { - backgroundColor: { red: 255, green: 255, blue: 255 }, + backgroundColor: THUMB_COLOR, x: x, y: y, width: thumbSize, @@ -61,6 +81,25 @@ return (item == this.thumb) || (item == this.background); }; + + Slider.prototype.onMousePressEvent = function(event, clickedOverlay) { + if (!this.isClickableOverlayItem(clickedOverlay)) { + this.isMoving = false; + return; + } + this.highlight(); + this.isMoving = true; + var clickOffset = event.x - this.thumbX; + if ((clickOffset > -this.thumbHalfSize) && (clickOffset < this.thumbHalfSize)) { + this.clickOffsetX = clickOffset; + } else { + this.clickOffsetX = 0; + this.thumbX = event.x; + this.updateThumb(); + this.onValueChanged(this.getValue()); + } + }; + Slider.prototype.onMouseMoveEvent = function(event) { if (this.isMoving) { var newThumbX = event.x - this.clickOffsetX; @@ -76,34 +115,43 @@ } }; - Slider.prototype.onMousePressEvent = function(event, clickedOverlay) { - if (!this.isClickableOverlayItem(clickedOverlay)) { - this.isMoving = false; - return; - } - this.isMoving = true; - var clickOffset = event.x - this.thumbX; - if ((clickOffset > -this.thumbHalfSize) && (clickOffset < this.thumbHalfSize)) { - this.clickOffsetX = clickOffset; - } else { - this.clickOffsetX = 0; - this.thumbX = event.x; - this.updateThumb(); - this.onValueChanged(this.getValue()); - } - - }; - Slider.prototype.onMouseReleaseEvent = function(event) { this.isMoving = false; + this.unhighlight(); }; - - // Public members: + + Slider.prototype.updateWithKeys = function(direction) { + this.range = this.maxThumbX - this.minThumbX; + this.thumbX += direction * (this.range / SCALE); + this.updateThumb(); + this.onValueChanged(this.getValue()); + }; + + Slider.prototype.highlight = function() { + if(this.highlighted) { + return; + } + Overlays.editOverlay(this.thumb, { + backgroundColor: {red: 255, green: 255, blue: 255} + }); + this.highlighted = true; + }; + + Slider.prototype.unhighlight = function() { + if(!this.highlighted) { + return; + } + Overlays.editOverlay(this.thumb, { + backgroundColor: THUMB_COLOR + }); + this.highlighted = false; + }; + Slider.prototype.setNormalizedValue = function(value) { if (value < 0.0) { this.thumbX = this.minThumbX; } else if (value > 1.0) { - this.thumbX = this.maxThumbX; + this.thumbX = this.maxThsumbX; } else { this.thumbX = value * (this.maxThumbX - this.minThumbX) + this.minThumbX; } @@ -117,16 +165,17 @@ var normValue = (value - this.minValue) / (this.maxValue - this.minValue); this.setNormalizedValue(normValue); }; + Slider.prototype.getValue = function() { + return this.getNormalizedValue() * (this.maxValue - this.minValue) + this.minValue; + }; Slider.prototype.reset = function(resetValue) { this.setValue(resetValue); this.onValueChanged(resetValue); }; - Slider.prototype.getValue = function() { - return this.getNormalizedValue() * (this.maxValue - this.minValue) + this.minValue; - }; - + Slider.prototype.onValueChanged = function(value) {}; + Slider.prototype.getHeight = function() { if(!this.visible) { return 0; @@ -143,8 +192,6 @@ Overlays.editOverlay(this.background, {y: this.y}); Overlays.editOverlay(this.thumb, {y: this.y}); }; - - Slider.prototype.onValueChanged = function(value) {}; Slider.prototype.hide = function() { Overlays.editOverlay(this.background, {visible: false}); @@ -157,36 +204,19 @@ Overlays.editOverlay(this.thumb, {visible: true}); this.visible = true; }; - + Slider.prototype.destroy = function() { Overlays.deleteOverlay(this.background); Overlays.deleteOverlay(this.thumb); }; - - Slider.prototype.setThumbColor = function(color) { - Overlays.editOverlay(this.thumb, {backgroundColor: { red: color.x*255, green: color.y*255, blue: color.z*255 }}); - }; - Slider.prototype.setBackgroundColor = function(color) { - Overlays.editOverlay(this.background, {backgroundColor: { red: color.x*255, green: color.y*255, blue: color.z*255 }}); - }; + this.Slider = Slider; // The Checkbox class var Checkbox = function(x,y,width,thumbSize) { - this.background = Overlays.addOverlay("text", { - backgroundColor: { red: 125, green: 125, blue: 255 }, - x: x, - y: y, - width: width, - height: thumbSize, - alpha: 1.0, - backgroundAlpha: 0.5, - visible: true - }); - this.thumb = Overlays.addOverlay("text", { - backgroundColor: { red: 255, green: 255, blue: 255 }, + backgroundColor: THUMB_COLOR, textFontSize: 10, x: x, y: y, @@ -208,64 +238,72 @@ this.checkMark = Overlays.addOverlay("text", { - backgroundColor: { red: 0, green: 255, blue: 0 }, + backgroundColor: CHECK_MARK_COLOR, x: checkX, y: checkY, width: thumbSize / 2.0, height: thumbSize / 2.0, alpha: 1.0, visible: true - }); - - this.unCheckMark = Overlays.addOverlay("image", { - backgroundColor: { red: 255, green: 255, blue: 255 }, - x: checkX + 1.0, - y: checkY + 1.0, - width: thumbSize / 2.5, - height: thumbSize / 2.5, - alpha: 1.0, - visible: false - }); + }); }; Checkbox.prototype.updateThumb = function() { - - Overlays.editOverlay(this.unCheckMark, { visible: !this.boxCheckStatus }); - + Overlays.editOverlay(this.checkMark, { visible: this.boxCheckStatus }); }; Checkbox.prototype.isClickableOverlayItem = function(item) { - return (item == this.thumb) || (item == this.checkMark) || (item == this.unCheckMark); + return (item == this.thumb) || (item == this.checkMark); }; Checkbox.prototype.onMousePressEvent = function(event, clickedOverlay) { - if (!this.isClickableOverlayItem(clickedOverlay)) { return; - } - + } this.boxCheckStatus = !this.boxCheckStatus; this.onValueChanged(this.getValue()); this.updateThumb(); }; - - Checkbox.prototype.onMouseReleaseEvent = function(event) { }; - - - // Public members: + Checkbox.prototype.onMouseReleaseEvent = function(event) {}; + + Checkbox.prototype.updateWithKeys = function() { + this.boxCheckStatus = !this.boxCheckStatus; + this.onValueChanged(this.getValue()); + this.updateThumb(); + }; + + Checkbox.prototype.highlight = function() { + Overlays.editOverlay(this.thumb, { + backgroundColor: THUMB_HIGHLIGHT + }); + this.highlighted = true; + }; + + Checkbox.prototype.unhighlight = function() { + Overlays.editOverlay(this.thumb, { + backgroundColor: THUMB_COLOR + }); + this.highlighted = false; + }; Checkbox.prototype.setValue = function(value) { this.boxCheckStatus = value; }; + + Checkbox.prototype.setterFromWidget = function(value) { + this.updateThumb(); + }; + + Checkbox.prototype.getValue = function() { + return this.boxCheckStatus; + }; Checkbox.prototype.reset = function(resetValue) { this.setValue(resetValue); }; - - Checkbox.prototype.getValue = function() { - return this.boxCheckStatus; - }; + + Checkbox.prototype.onValueChanged = function(value) { }; Checkbox.prototype.getHeight = function() { if(!this.visible) { @@ -287,12 +325,6 @@ Overlays.editOverlay(this.checkMark, {y: this.y}); Overlays.editOverlay(this.unCheckMark, {y: this.y}); }; - - Checkbox.prototype.setterFromWidget = function(value) { - this.updateThumb(); - }; - - Checkbox.prototype.onValueChanged = function(value) { }; Checkbox.prototype.hide = function() { Overlays.editOverlay(this.background, {visible: false}); @@ -316,13 +348,7 @@ Overlays.deleteOverlay(this.checkMark); Overlays.deleteOverlay(this.unCheckMark); }; - - Checkbox.prototype.setThumbColor = function(color) { - Overlays.editOverlay(this.thumb, { red: 255, green: 255, blue: 255 } ); - }; - Checkbox.prototype.setBackgroundColor = function(color) { - Overlays.editOverlay(this.background, { red: 125, green: 125, blue: 255 }); - }; + this.Checkbox = Checkbox; // The ColorBox class @@ -351,12 +377,6 @@ || this.blue.isClickableOverlayItem(item); }; - ColorBox.prototype.onMouseMoveEvent = function(event) { - this.red.onMouseMoveEvent(event); - this.green.onMouseMoveEvent(event); - this.blue.onMouseMoveEvent(event); - }; - ColorBox.prototype.onMousePressEvent = function(event, clickedOverlay) { this.red.onMousePressEvent(event, clickedOverlay); if (this.red.isMoving) { @@ -370,19 +390,48 @@ this.blue.onMousePressEvent(event, clickedOverlay); }; - + + ColorBox.prototype.onMouseMoveEvent = function(event) { + this.red.onMouseMoveEvent(event); + this.green.onMouseMoveEvent(event); + this.blue.onMouseMoveEvent(event); + }; ColorBox.prototype.onMouseReleaseEvent = function(event) { this.red.onMouseReleaseEvent(event); this.green.onMouseReleaseEvent(event); this.blue.onMouseReleaseEvent(event); }; - + + ColorBox.prototype.updateWithKeys = function(direction) { + this.red.updateWithKeys(direction); + this.green.updateWithKeys(direction); + this.blue.updateWithKeys(direction); + } + + ColorBox.prototype.highlight = function() { + this.red.highlight(); + this.green.highlight(); + this.blue.highlight(); + + this.highlighted = true; + }; + + ColorBox.prototype.unhighlight = function() { + this.red.unhighlight(); + this.green.unhighlight(); + this.blue.unhighlight(); + + this.highlighted = false; + }; + ColorBox.prototype.setterFromWidget = function(value) { var color = this.getValue(); this.onValueChanged(color); this.updateRGBSliders(color); }; + + ColorBox.prototype.onValueChanged = function(value) {}; ColorBox.prototype.updateRGBSliders = function(color) { this.red.setThumbColor({x: color.x, y: 0, z: 0}); @@ -397,17 +446,18 @@ this.blue.setValue(value.z); this.updateRGBSliders(value); }; - - ColorBox.prototype.reset = function(resetValue) { - this.setValue(resetValue); - this.onValueChanged(resetValue); - }; - + ColorBox.prototype.getValue = function() { var value = {x:this.red.getValue(), y:this.green.getValue(),z:this.blue.getValue()}; return value; }; - + + ColorBox.prototype.reset = function(resetValue) { + this.setValue(resetValue); + this.onValueChanged(resetValue); + }; + + ColorBox.prototype.getHeight = function() { if(!this.visible) { return 0; @@ -446,8 +496,7 @@ this.green.destroy(); this.blue.destroy(); }; - - ColorBox.prototype.onValueChanged = function(value) {}; + this.ColorBox = ColorBox; // The DirectionBox class @@ -479,11 +528,6 @@ || this.pitch.isClickableOverlayItem(item); }; - DirectionBox.prototype.onMouseMoveEvent = function(event) { - this.yaw.onMouseMoveEvent(event); - this.pitch.onMouseMoveEvent(event); - }; - DirectionBox.prototype.onMousePressEvent = function(event, clickedOverlay) { this.yaw.onMousePressEvent(event, clickedOverlay); if (this.yaw.isMoving) { @@ -491,18 +535,43 @@ } this.pitch.onMousePressEvent(event, clickedOverlay); }; + + DirectionBox.prototype.onMouseMoveEvent = function(event) { + this.yaw.onMouseMoveEvent(event); + this.pitch.onMouseMoveEvent(event); + }; DirectionBox.prototype.onMouseReleaseEvent = function(event) { this.yaw.onMouseReleaseEvent(event); this.pitch.onMouseReleaseEvent(event); }; + + DirectionBox.prototype.updateWithKeys = function(direction) { + this.yaw.updateWithKeys(direction); + this.pitch.updateWithKeys(direction); + }; + + DirectionBox.prototype.highlight = function() { + this.pitch.highlight(); + this.yaw.highlight(); + + this.highlighted = true; + }; + + DirectionBox.prototype.unhighlight = function() { + this.pitch.unhighlight(); + this.yaw.unhighlight(); + + this.highlighted = false; + }; DirectionBox.prototype.setterFromWidget = function(value) { var yawPitch = this.getValue(); this.onValueChanged(yawPitch); }; - - // Public members: + + DirectionBox.prototype.onValueChanged = function(value) {}; + DirectionBox.prototype.setValue = function(direction) { var flatXZ = Math.sqrt(direction.x * direction.x + direction.z * direction.z); if (flatXZ > 0.0) { @@ -516,13 +585,8 @@ } this.pitch.setValue(direction.y); }; - - DirectionBox.prototype.reset = function(resetValue) { - this.setValue(resetValue); - this.onValueChanged(resetValue); - }; - - DirectionBox.prototype.getValue = function() { + + DirectionBox.prototype.getValue = function() { var dirZ = this.pitch.getValue(); var yaw = this.yaw.getValue() * Math.PI / 180; var cosY = Math.sqrt(1 - dirZ*dirZ); @@ -530,6 +594,11 @@ return value; }; + DirectionBox.prototype.reset = function(resetValue) { + this.setValue(resetValue); + this.onValueChanged(resetValue); + }; + DirectionBox.prototype.getHeight = function() { if(!this.visible) { return 0; @@ -564,7 +633,6 @@ this.pitch.destroy(); }; - DirectionBox.prototype.onValueChanged = function(value) {}; this.DirectionBox = DirectionBox; var textFontSize = 12; @@ -577,15 +645,14 @@ var topMargin = (height - 1.5 * textFontSize); - this.thumb = Overlays.addOverlay("text", { - backgroundColor: { red: 220, green: 220, blue: 220 }, - textFontSize: 10, + this.thumb = Overlays.addOverlay("image", { + color: {red: 255, green: 255, blue: 255}, + imageURL: HIFI_PUBLIC_BUCKET + 'images/tools/min-max-toggle.svg', x: x, y: y, width: rawHeight, height: rawHeight, alpha: 1.0, - backgroundAlpha: 1.0, visible: true }); @@ -644,13 +711,12 @@ if (this.widget != null) { this.widget.moveDown(); } - } - this.CollapsablePanelItem = CollapsablePanelItem; var PanelItem = function (name, setter, getter, displayer, x, y, textWidth, valueWidth, height) { //print("creating panel item: " + name); + this.isCollapsable = false; this.name = name; this.y = y; @@ -780,14 +846,23 @@ var widgetWidth = 300; var rawHeight = 20; var rawYDelta = rawHeight * 1.5; + var outerPanel = true; + var widgets; + var Panel = function(x, y) { - + + if (outerPanel) { + widgets = []; + } + outerPanel = false; + this.x = x; this.y = y; + this.nextY = y; - this.nextY = y; print("creating panel at x: " + this.x + " y: " + this.y); - + print("creating panel at x: " + this.x + " y: " + this.y); + this.widgetX = x + textWidth + valueWidth; this.items = new Array(); @@ -826,12 +901,20 @@ } } }; + + Panel.prototype.mouseReleaseEvent = function(event) { + if (this.activeWidget) { + this.activeWidget.onMouseReleaseEvent(event); + } + this.activeWidget = null; + }; // Reset panel item upon double-clicking Panel.prototype.mouseDoublePressEvent = function(event) { var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); this.handleReset(clickedOverlay); - } + }; + Panel.prototype.handleReset = function (clickedOverlay) { for (var i in this.items) { @@ -845,16 +928,13 @@ if (clickedOverlay == item.title) { item.activeWidget = widget; - item.activeWidget.reset(item.resetValue); - break; } } }; Panel.prototype.handleCollapse = function (clickedOverlay) { - for (var i in this.items) { var item = this.items[i]; @@ -886,19 +966,14 @@ panel.hide(); break; } - } - // Now recalculate new heights of subsequent widgets for(var j = i + 1; j < keys.length; ++j) { this.items[keys[j]].moveUp(this.getCurrentY(keys[j])); - } - }; - Panel.prototype.expand = function (clickedOverlay) { var keys = Object.keys(this.items); @@ -910,23 +985,13 @@ panel.show(); break; } - - } - - // Now recalculate new heights of subsequent widgets + } + // Now recalculate new heights of subsequent widgets for(var j = i + 1; j < keys.length; ++j) { this.items[keys[j]].moveDown(); } + }; - }; - - - Panel.prototype.mouseReleaseEvent = function(event) { - if (this.activeWidget) { - this.activeWidget.onMouseReleaseEvent(event); - } - this.activeWidget = null; - }; Panel.prototype.onMousePressEvent = function(event, clickedOverlay) { for (var i in this.items) { @@ -938,7 +1003,6 @@ } }; - Panel.prototype.onMouseMoveEvent = function(event) { for (var i in this.items) { var item = this.items[i]; @@ -966,7 +1030,63 @@ } } }; - + + var tabView = false; + var tabIndex = 0; + + Panel.prototype.keyPressEvent = function(event) { + if(event.text == "TAB" && !event.isShifted) { + tabView = true; + if(tabIndex < widgets.length) { + if(tabIndex > 0 && widgets[tabIndex - 1].highlighted) { + // Unhighlight previous widget + widgets[tabIndex - 1].unhighlight(); + } + widgets[tabIndex].highlight(); + tabIndex++; + } else { + widgets[tabIndex - 1].unhighlight(); + //Wrap around to front + tabIndex = 0; + widgets[tabIndex].highlight(); + tabIndex++; + } + } else if (tabView && event.isShifted) { + if(tabIndex > 0) { + tabIndex--; + if(tabIndex < widgets.length && widgets[tabIndex + 1].highlighted) { + // Unhighlight previous widget + widgets[tabIndex + 1].unhighlight(); + } + widgets[tabIndex].highlight(); + } else { + widgets[tabIndex].unhighlight(); + //Wrap around to end + tabIndex = widgets.length - 1; + widgets[tabIndex].highlight(); + } + } else if (event.text == "LEFT") { + for(var i = 0; i < widgets.length; i++) { + // Find the highlighted widget, allow control with arrow keys + if(widgets[i].highlighted) { + var k = -1; + widgets[i].updateWithKeys(k); + break; + } + } + } else if (event.text == "RIGHT") { + for(var i = 0; i < widgets.length; i++) { + // Find the highlighted widget, allow control with arrow keys + if(widgets[i].highlighted) { + var k = 1; + widgets[i].updateWithKeys(k); + break; + } + } + } + }; + + // Widget constructors Panel.prototype.newSlider = function(name, minValue, maxValue, setValue, getValue, displayValue) { this.nextY = this.y + this.getHeight(); @@ -974,6 +1094,7 @@ var slider = new Slider(this.widgetX, this.nextY, widgetWidth, rawHeight); slider.minValue = minValue; slider.maxValue = maxValue; + widgets.push(slider); item.widget = slider; item.widget.onValueChanged = function(value) { item.setterFromWidget(value); }; @@ -995,6 +1116,7 @@ var item = new PanelItem(name, setValue, getValue, display, this.x, this.nextY, textWidth, valueWidth, rawHeight); var checkbox = new Checkbox(this.widgetX, this.nextY, widgetWidth, rawHeight); + widgets.push(checkbox); item.widget = checkbox; item.widget.onValueChanged = function(value) { item.setterFromWidget(value); }; @@ -1010,6 +1132,7 @@ var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); var colorBox = new ColorBox(this.widgetX, this.nextY, widgetWidth, rawHeight); + widgets.push(colorBox); item.widget = colorBox; item.widget.onValueChanged = function(value) { item.setterFromWidget(value); }; @@ -1025,6 +1148,7 @@ var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); var directionBox = new DirectionBox(this.widgetX, this.nextY, widgetWidth, rawHeight); + widgets.push(directionBox); item.widget = directionBox; item.widget.onValueChanged = function(value) { item.setterFromWidget(value); }; @@ -1034,10 +1158,8 @@ // print("created Item... directionBox=" + name); }; - - Panel.prototype.newSubPanel = function(name) { - //TODO: make collapsable, fix double-press event + this.nextY = this.y + this.getHeight(); var item = new CollapsablePanelItem(name, this.x, this.nextY, textWidth, rawHeight, panel); @@ -1093,22 +1215,14 @@ return false; }; - Panel.prototype.getHeight = function(show) { + Panel.prototype.getHeight = function() { var height = 0; for (var i in this.items) { - height += this.items[i].widget.getHeight(); - // if(show) { - // print("widget: " + i + " height: " + this.items[i].widget.getHeight()); - - // } if(this.items[i].isSubPanel && this.items[i].widget.visible) { - - height += 1.5 * rawHeight; - - } - + height += 1.5 * rawHeight; + } } return height; @@ -1118,7 +1232,6 @@ for (var i in this.items) { this.items[i].widget.moveUp(); } - }; Panel.prototype.moveDown = function() { @@ -1141,7 +1254,6 @@ } } - return this.y + height; }; @@ -1177,6 +1289,5 @@ } }; - this.Panel = Panel; })(); \ No newline at end of file From d9693796bc06a77eb38c1c7fede2eb26d077f3c9 Mon Sep 17 00:00:00 2001 From: bwent Date: Tue, 28 Jul 2015 12:27:37 -0700 Subject: [PATCH 129/242] Capture key events --- examples/utilities/tools/cookies.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/examples/utilities/tools/cookies.js b/examples/utilities/tools/cookies.js index b2dc8c1da7..175d8c2393 100644 --- a/examples/utilities/tools/cookies.js +++ b/examples/utilities/tools/cookies.js @@ -1288,6 +1288,15 @@ var CHECK_MARK_COLOR = { this.items[i].destroy(); } }; - this.Panel = Panel; -})(); \ No newline at end of file +})(); + + +Script.scriptEnding.connect(function scriptEnding() { + Controller.releaseKeyEvents({text: "left"}); + Controller.releaseKeyEvents({key: "right"}); +}); + + +Controller.captureKeyEvents({text: "left"}); +Controller.captureKeyEvents({text: "right"}); \ No newline at end of file From 2be95997d16ed572aac938af45393f8b8fbcfad1 Mon Sep 17 00:00:00 2001 From: bwent Date: Tue, 28 Jul 2015 12:32:14 -0700 Subject: [PATCH 130/242] clean up formatting --- examples/utilities/tools/cookies.js | 1317 +++++++++++++++------------ 1 file changed, 749 insertions(+), 568 deletions(-) diff --git a/examples/utilities/tools/cookies.js b/examples/utilities/tools/cookies.js index 175d8c2393..24cda69ba2 100644 --- a/examples/utilities/tools/cookies.js +++ b/examples/utilities/tools/cookies.js @@ -30,204 +30,230 @@ var CHECK_MARK_COLOR = { }; // The Slider class -(function () { - var Slider = function(x,y,width,thumbSize) { - +(function() { + var Slider = function(x, y, width, thumbSize) { + this.background = Overlays.addOverlay("text", { - backgroundColor: { red: 200, green: 200, blue: 255 }, - x: x, - y: y, - width: width, - height: thumbSize, - alpha: 1.0, - backgroundAlpha: 0.5, - visible: true - }); + backgroundColor: { + red: 200, + green: 200, + blue: 255 + }, + x: x, + y: y, + width: width, + height: thumbSize, + alpha: 1.0, + backgroundAlpha: 0.5, + visible: true + }); this.thumb = Overlays.addOverlay("text", { - backgroundColor: THUMB_COLOR, - x: x, - y: y, - width: thumbSize, - height: thumbSize, - alpha: 1.0, - backgroundAlpha: 1.0, - visible: true - }); - + backgroundColor: THUMB_COLOR, + x: x, + y: y, + width: thumbSize, + height: thumbSize, + alpha: 1.0, + backgroundAlpha: 1.0, + visible: true + }); + this.thumbSize = thumbSize; - + this.thumbHalfSize = 0.5 * thumbSize; - + this.minThumbX = x + this.thumbHalfSize; this.maxThumbX = x + width - this.thumbHalfSize; this.thumbX = this.minThumbX; - + this.minValue = 0.0; this.maxValue = 1.0; this.y = y; - + this.clickOffsetX = 0; this.isMoving = false; this.visible = true; }; - - Slider.prototype.updateThumb = function() { - var thumbTruePos = this.thumbX - 0.5 * this.thumbSize; - Overlays.editOverlay(this.thumb, { x: thumbTruePos } ); - }; - - Slider.prototype.isClickableOverlayItem = function(item) { - return (item == this.thumb) || (item == this.background); - }; - - - Slider.prototype.onMousePressEvent = function(event, clickedOverlay) { - if (!this.isClickableOverlayItem(clickedOverlay)) { - this.isMoving = false; - return; - } - this.highlight(); - this.isMoving = true; - var clickOffset = event.x - this.thumbX; - if ((clickOffset > -this.thumbHalfSize) && (clickOffset < this.thumbHalfSize)) { - this.clickOffsetX = clickOffset; - } else { - this.clickOffsetX = 0; - this.thumbX = event.x; - this.updateThumb(); - this.onValueChanged(this.getValue()); - } - }; - Slider.prototype.onMouseMoveEvent = function(event) { - if (this.isMoving) { - var newThumbX = event.x - this.clickOffsetX; - if (newThumbX < this.minThumbX) { - newThumbX = this.minThumbX; - } - if (newThumbX > this.maxThumbX) { - newThumbX = this.maxThumbX; - } - this.thumbX = newThumbX; - this.updateThumb(); - this.onValueChanged(this.getValue()); - } - }; - - Slider.prototype.onMouseReleaseEvent = function(event) { + Slider.prototype.updateThumb = function() { + var thumbTruePos = this.thumbX - 0.5 * this.thumbSize; + Overlays.editOverlay(this.thumb, { + x: thumbTruePos + }); + }; + + Slider.prototype.isClickableOverlayItem = function(item) { + return (item == this.thumb) || (item == this.background); + }; + + + Slider.prototype.onMousePressEvent = function(event, clickedOverlay) { + if (!this.isClickableOverlayItem(clickedOverlay)) { this.isMoving = false; - this.unhighlight(); - }; + return; + } + this.highlight(); + this.isMoving = true; + var clickOffset = event.x - this.thumbX; + if ((clickOffset > -this.thumbHalfSize) && (clickOffset < this.thumbHalfSize)) { + this.clickOffsetX = clickOffset; + } else { + this.clickOffsetX = 0; + this.thumbX = event.x; + this.updateThumb(); + this.onValueChanged(this.getValue()); + } + }; + + Slider.prototype.onMouseMoveEvent = function(event) { + if (this.isMoving) { + var newThumbX = event.x - this.clickOffsetX; + if (newThumbX < this.minThumbX) { + newThumbX = this.minThumbX; + } + if (newThumbX > this.maxThumbX) { + newThumbX = this.maxThumbX; + } + this.thumbX = newThumbX; + this.updateThumb(); + this.onValueChanged(this.getValue()); + } + }; + + Slider.prototype.onMouseReleaseEvent = function(event) { + this.isMoving = false; + this.unhighlight(); + }; Slider.prototype.updateWithKeys = function(direction) { this.range = this.maxThumbX - this.minThumbX; this.thumbX += direction * (this.range / SCALE); this.updateThumb(); this.onValueChanged(this.getValue()); - }; + }; Slider.prototype.highlight = function() { - if(this.highlighted) { + if (this.highlighted) { return; } Overlays.editOverlay(this.thumb, { - backgroundColor: {red: 255, green: 255, blue: 255} + backgroundColor: { + red: 255, + green: 255, + blue: 255 + } }); - this.highlighted = true; + this.highlighted = true; }; Slider.prototype.unhighlight = function() { - if(!this.highlighted) { + if (!this.highlighted) { return; } Overlays.editOverlay(this.thumb, { backgroundColor: THUMB_COLOR - }); + }); this.highlighted = false; }; Slider.prototype.setNormalizedValue = function(value) { - if (value < 0.0) { - this.thumbX = this.minThumbX; - } else if (value > 1.0) { - this.thumbX = this.maxThsumbX; - } else { - this.thumbX = value * (this.maxThumbX - this.minThumbX) + this.minThumbX; - } - this.updateThumb(); - }; + if (value < 0.0) { + this.thumbX = this.minThumbX; + } else if (value > 1.0) { + this.thumbX = this.maxThsumbX; + } else { + this.thumbX = value * (this.maxThumbX - this.minThumbX) + this.minThumbX; + } + this.updateThumb(); + }; Slider.prototype.getNormalizedValue = function() { - return (this.thumbX - this.minThumbX) / (this.maxThumbX - this.minThumbX); - }; - + return (this.thumbX - this.minThumbX) / (this.maxThumbX - this.minThumbX); + }; + Slider.prototype.setValue = function(value) { - var normValue = (value - this.minValue) / (this.maxValue - this.minValue); - this.setNormalizedValue(normValue); - }; + var normValue = (value - this.minValue) / (this.maxValue - this.minValue); + this.setNormalizedValue(normValue); + }; Slider.prototype.getValue = function() { - return this.getNormalizedValue() * (this.maxValue - this.minValue) + this.minValue; - }; - + return this.getNormalizedValue() * (this.maxValue - this.minValue) + this.minValue; + }; + Slider.prototype.reset = function(resetValue) { - this.setValue(resetValue); - this.onValueChanged(resetValue); - }; + this.setValue(resetValue); + this.onValueChanged(resetValue); + }; Slider.prototype.onValueChanged = function(value) {}; Slider.prototype.getHeight = function() { - if(!this.visible) { - return 0; - } - return 1.5 * this.thumbSize; - }; + if (!this.visible) { + return 0; + } + return 1.5 * this.thumbSize; + }; Slider.prototype.moveUp = function(newY) { - Overlays.editOverlay(this.background, {y: newY}); - Overlays.editOverlay(this.thumb, {y: newY}); + Overlays.editOverlay(this.background, { + y: newY + }); + Overlays.editOverlay(this.thumb, { + y: newY + }); }; Slider.prototype.moveDown = function() { - Overlays.editOverlay(this.background, {y: this.y}); - Overlays.editOverlay(this.thumb, {y: this.y}); + Overlays.editOverlay(this.background, { + y: this.y + }); + Overlays.editOverlay(this.thumb, { + y: this.y + }); }; Slider.prototype.hide = function() { - Overlays.editOverlay(this.background, {visible: false}); - Overlays.editOverlay(this.thumb, {visible: false}); - this.visible = false; - }; + Overlays.editOverlay(this.background, { + visible: false + }); + Overlays.editOverlay(this.thumb, { + visible: false + }); + this.visible = false; + }; Slider.prototype.show = function() { - Overlays.editOverlay(this.background, {visible: true}); - Overlays.editOverlay(this.thumb, {visible: true}); - this.visible = true; - }; + Overlays.editOverlay(this.background, { + visible: true + }); + Overlays.editOverlay(this.thumb, { + visible: true + }); + this.visible = true; + }; Slider.prototype.destroy = function() { - Overlays.deleteOverlay(this.background); - Overlays.deleteOverlay(this.thumb); - }; + Overlays.deleteOverlay(this.background); + Overlays.deleteOverlay(this.thumb); + }; this.Slider = Slider; - + // The Checkbox class - var Checkbox = function(x,y,width,thumbSize) { - + var Checkbox = function(x, y, width, thumbSize) { + this.thumb = Overlays.addOverlay("text", { - backgroundColor: THUMB_COLOR, - textFontSize: 10, - x: x, - y: y, - width: thumbSize, - height: thumbSize, - alpha: 1.0, - backgroundAlpha: 1.0, - visible: true - }); - - + backgroundColor: THUMB_COLOR, + textFontSize: 10, + x: x, + y: y, + width: thumbSize, + height: thumbSize, + alpha: 1.0, + backgroundAlpha: 1.0, + visible: true + }); + + this.thumbSize = thumbSize; var checkX = x + (0.25 * thumbSize); var checkY = y + (0.25 * thumbSize); @@ -235,173 +261,217 @@ var CHECK_MARK_COLOR = { this.boxCheckStatus = true; this.clickedBox = false; this.visible = true; - - + + this.checkMark = Overlays.addOverlay("text", { - backgroundColor: CHECK_MARK_COLOR, - x: checkX, - y: checkY, - width: thumbSize / 2.0, - height: thumbSize / 2.0, - alpha: 1.0, - visible: true - }); + backgroundColor: CHECK_MARK_COLOR, + x: checkX, + y: checkY, + width: thumbSize / 2.0, + height: thumbSize / 2.0, + alpha: 1.0, + visible: true + }); }; - - Checkbox.prototype.updateThumb = function() { - Overlays.editOverlay(this.checkMark, { visible: this.boxCheckStatus }); - }; - + + Checkbox.prototype.updateThumb = function() { + Overlays.editOverlay(this.checkMark, { + visible: this.boxCheckStatus + }); + }; + Checkbox.prototype.isClickableOverlayItem = function(item) { - return (item == this.thumb) || (item == this.checkMark); - }; - + return (item == this.thumb) || (item == this.checkMark); + }; + Checkbox.prototype.onMousePressEvent = function(event, clickedOverlay) { - if (!this.isClickableOverlayItem(clickedOverlay)) { - return; - } - this.boxCheckStatus = !this.boxCheckStatus; - this.onValueChanged(this.getValue()); - this.updateThumb(); - }; - + if (!this.isClickableOverlayItem(clickedOverlay)) { + return; + } + this.boxCheckStatus = !this.boxCheckStatus; + this.onValueChanged(this.getValue()); + this.updateThumb(); + }; + Checkbox.prototype.onMouseReleaseEvent = function(event) {}; Checkbox.prototype.updateWithKeys = function() { this.boxCheckStatus = !this.boxCheckStatus; this.onValueChanged(this.getValue()); this.updateThumb(); - }; + }; Checkbox.prototype.highlight = function() { Overlays.editOverlay(this.thumb, { backgroundColor: THUMB_HIGHLIGHT }); - this.highlighted = true; - }; + this.highlighted = true; + }; Checkbox.prototype.unhighlight = function() { Overlays.editOverlay(this.thumb, { backgroundColor: THUMB_COLOR - }); + }); this.highlighted = false; - }; - + }; + Checkbox.prototype.setValue = function(value) { - this.boxCheckStatus = value; - }; + this.boxCheckStatus = value; + }; Checkbox.prototype.setterFromWidget = function(value) { - this.updateThumb(); - }; + this.updateThumb(); + }; Checkbox.prototype.getValue = function() { - return this.boxCheckStatus; - }; - - Checkbox.prototype.reset = function(resetValue) { - this.setValue(resetValue); - }; + return this.boxCheckStatus; + }; + + Checkbox.prototype.reset = function(resetValue) { + this.setValue(resetValue); + }; + + Checkbox.prototype.onValueChanged = function(value) {}; - Checkbox.prototype.onValueChanged = function(value) { }; - Checkbox.prototype.getHeight = function() { - if(!this.visible) { + if (!this.visible) { return 0; } - return 1.5 * this.thumbSize; - }; + return 1.5 * this.thumbSize; + }; Checkbox.prototype.moveUp = function(newY) { - Overlays.editOverlay(this.background, {y: newY}); - Overlays.editOverlay(this.thumb, {y: newY}); - Overlays.editOverlay(this.checkMark, {y: newY}); - Overlays.editOverlay(this.unCheckMark, {y: newY}); + Overlays.editOverlay(this.background, { + y: newY + }); + Overlays.editOverlay(this.thumb, { + y: newY + }); + Overlays.editOverlay(this.checkMark, { + y: newY + }); + Overlays.editOverlay(this.unCheckMark, { + y: newY + }); }; Checkbox.prototype.moveDown = function() { - Overlays.editOverlay(this.background, {y: this.y}); - Overlays.editOverlay(this.thumb, {y: this.y}); - Overlays.editOverlay(this.checkMark, {y: this.y}); - Overlays.editOverlay(this.unCheckMark, {y: this.y}); + Overlays.editOverlay(this.background, { + y: this.y + }); + Overlays.editOverlay(this.thumb, { + y: this.y + }); + Overlays.editOverlay(this.checkMark, { + y: this.y + }); + Overlays.editOverlay(this.unCheckMark, { + y: this.y + }); }; Checkbox.prototype.hide = function() { - Overlays.editOverlay(this.background, {visible: false}); - Overlays.editOverlay(this.thumb, {visible: false}); - Overlays.editOverlay(this.checkMark, {visible: false}); - Overlays.editOverlay(this.unCheckMark, {visible: false}); + Overlays.editOverlay(this.background, { + visible: false + }); + Overlays.editOverlay(this.thumb, { + visible: false + }); + Overlays.editOverlay(this.checkMark, { + visible: false + }); + Overlays.editOverlay(this.unCheckMark, { + visible: false + }); this.visible = false; } Checkbox.prototype.show = function() { - Overlays.editOverlay(this.background, {visible: true}); - Overlays.editOverlay(this.thumb, {visible: true}); - Overlays.editOverlay(this.checkMark, {visible: true}); - Overlays.editOverlay(this.unCheckMark, {visible: true}); + Overlays.editOverlay(this.background, { + visible: true + }); + Overlays.editOverlay(this.thumb, { + visible: true + }); + Overlays.editOverlay(this.checkMark, { + visible: true + }); + Overlays.editOverlay(this.unCheckMark, { + visible: true + }); this.visible = true; } - + Checkbox.prototype.destroy = function() { - Overlays.deleteOverlay(this.background); - Overlays.deleteOverlay(this.thumb); - Overlays.deleteOverlay(this.checkMark); - Overlays.deleteOverlay(this.unCheckMark); - }; + Overlays.deleteOverlay(this.background); + Overlays.deleteOverlay(this.thumb); + Overlays.deleteOverlay(this.checkMark); + Overlays.deleteOverlay(this.unCheckMark); + }; this.Checkbox = Checkbox; - + // The ColorBox class - var ColorBox = function(x,y,width,thumbSize) { + var ColorBox = function(x, y, width, thumbSize) { var self = this; - + var slideHeight = thumbSize / 3; var sliderWidth = width; this.red = new Slider(x, y, width, slideHeight); this.green = new Slider(x, y + slideHeight, width, slideHeight); this.blue = new Slider(x, y + 2 * slideHeight, width, slideHeight); - this.red.setBackgroundColor({x: 1, y: 0, z: 0}); - this.green.setBackgroundColor({x: 0, y: 1, z: 0}); - this.blue.setBackgroundColor({x: 0, y: 0, z: 1}); - + this.red.setBackgroundColor({ + x: 1, + y: 0, + z: 0 + }); + this.green.setBackgroundColor({ + x: 0, + y: 1, + z: 0 + }); + this.blue.setBackgroundColor({ + x: 0, + y: 0, + z: 1 + }); + this.red.onValueChanged = this.setterFromWidget; this.green.onValueChanged = this.setterFromWidget; this.blue.onValueChanged = this.setterFromWidget; this.visible = true; }; - - ColorBox.prototype.isClickableOverlayItem = function(item) { - return this.red.isClickableOverlayItem(item) - || this.green.isClickableOverlayItem(item) - || this.blue.isClickableOverlayItem(item); - }; - - ColorBox.prototype.onMousePressEvent = function(event, clickedOverlay) { - this.red.onMousePressEvent(event, clickedOverlay); - if (this.red.isMoving) { - return; - } - - this.green.onMousePressEvent(event, clickedOverlay); - if (this.green.isMoving) { - return; - } - - this.blue.onMousePressEvent(event, clickedOverlay); - }; - ColorBox.prototype.onMouseMoveEvent = function(event) { - this.red.onMouseMoveEvent(event); - this.green.onMouseMoveEvent(event); - this.blue.onMouseMoveEvent(event); - }; - + ColorBox.prototype.isClickableOverlayItem = function(item) { + return this.red.isClickableOverlayItem(item) || this.green.isClickableOverlayItem(item) || this.blue.isClickableOverlayItem(item); + }; + + ColorBox.prototype.onMousePressEvent = function(event, clickedOverlay) { + this.red.onMousePressEvent(event, clickedOverlay); + if (this.red.isMoving) { + return; + } + + this.green.onMousePressEvent(event, clickedOverlay); + if (this.green.isMoving) { + return; + } + + this.blue.onMousePressEvent(event, clickedOverlay); + }; + + ColorBox.prototype.onMouseMoveEvent = function(event) { + this.red.onMouseMoveEvent(event); + this.green.onMouseMoveEvent(event); + this.blue.onMouseMoveEvent(event); + }; + ColorBox.prototype.onMouseReleaseEvent = function(event) { - this.red.onMouseReleaseEvent(event); - this.green.onMouseReleaseEvent(event); - this.blue.onMouseReleaseEvent(event); - }; + this.red.onMouseReleaseEvent(event); + this.green.onMouseReleaseEvent(event); + this.blue.onMouseReleaseEvent(event); + }; ColorBox.prototype.updateWithKeys = function(direction) { this.red.updateWithKeys(direction); @@ -412,7 +482,7 @@ var CHECK_MARK_COLOR = { ColorBox.prototype.highlight = function() { this.red.highlight(); this.green.highlight(); - this.blue.highlight(); + this.blue.highlight(); this.highlighted = true; }; @@ -426,44 +496,60 @@ var CHECK_MARK_COLOR = { }; ColorBox.prototype.setterFromWidget = function(value) { - var color = this.getValue(); - this.onValueChanged(color); - this.updateRGBSliders(color); - }; + var color = this.getValue(); + this.onValueChanged(color); + this.updateRGBSliders(color); + }; ColorBox.prototype.onValueChanged = function(value) {}; - + ColorBox.prototype.updateRGBSliders = function(color) { - this.red.setThumbColor({x: color.x, y: 0, z: 0}); - this.green.setThumbColor({x: 0, y: color.y, z: 0}); - this.blue.setThumbColor({x: 0, y: 0, z: color.z}); - }; - - // Public members: + this.red.setThumbColor({ + x: color.x, + y: 0, + z: 0 + }); + this.green.setThumbColor({ + x: 0, + y: color.y, + z: 0 + }); + this.blue.setThumbColor({ + x: 0, + y: 0, + z: color.z + }); + }; + + // Public members: ColorBox.prototype.setValue = function(value) { - this.red.setValue(value.x); - this.green.setValue(value.y); - this.blue.setValue(value.z); - this.updateRGBSliders(value); - }; + this.red.setValue(value.x); + this.green.setValue(value.y); + this.blue.setValue(value.z); + this.updateRGBSliders(value); + }; ColorBox.prototype.getValue = function() { - var value = {x:this.red.getValue(), y:this.green.getValue(),z:this.blue.getValue()}; - return value; + var value = { + x: this.red.getValue(), + y: this.green.getValue(), + z: this.blue.getValue() }; + return value; + }; ColorBox.prototype.reset = function(resetValue) { - this.setValue(resetValue); - this.onValueChanged(resetValue); - }; + this.setValue(resetValue); + this.onValueChanged(resetValue); + }; + - ColorBox.prototype.getHeight = function() { - if(!this.visible) { + if (!this.visible) { return 0; } - return 1.5 * this.thumbSize; - }; + return 1.5 * this.thumbSize; + }; ColorBox.prototype.moveUp = function(newY) { this.red.moveUp(newY); @@ -490,121 +576,132 @@ var CHECK_MARK_COLOR = { this.blue.show(); this.visible = true; } - + ColorBox.prototype.destroy = function() { - this.red.destroy(); - this.green.destroy(); - this.blue.destroy(); - }; + this.red.destroy(); + this.green.destroy(); + this.blue.destroy(); + }; this.ColorBox = ColorBox; - + // The DirectionBox class - var DirectionBox = function(x,y,width,thumbSize) { + var DirectionBox = function(x, y, width, thumbSize) { var self = this; - + var slideHeight = thumbSize / 2; var sliderWidth = width; this.yaw = new Slider(x, y, width, slideHeight); this.pitch = new Slider(x, y + slideHeight, width, slideHeight); - - - this.yaw.setThumbColor({x: 1, y: 0, z: 0}); + + + this.yaw.setThumbColor({ + x: 1, + y: 0, + z: 0 + }); this.yaw.minValue = -180; this.yaw.maxValue = +180; - - this.pitch.setThumbColor({x: 0, y: 0, z: 1}); + + this.pitch.setThumbColor({ + x: 0, + y: 0, + z: 1 + }); this.pitch.minValue = -1; this.pitch.maxValue = +1; - + this.yaw.onValueChanged = this.setterFromWidget; this.pitch.onValueChanged = this.setterFromWidget; this.visible = true; }; - - DirectionBox.prototype.isClickableOverlayItem = function(item) { - return this.yaw.isClickableOverlayItem(item) - || this.pitch.isClickableOverlayItem(item); - }; - - DirectionBox.prototype.onMousePressEvent = function(event, clickedOverlay) { - this.yaw.onMousePressEvent(event, clickedOverlay); - if (this.yaw.isMoving) { - return; - } - this.pitch.onMousePressEvent(event, clickedOverlay); - }; - DirectionBox.prototype.onMouseMoveEvent = function(event) { - this.yaw.onMouseMoveEvent(event); - this.pitch.onMouseMoveEvent(event); - }; - + DirectionBox.prototype.isClickableOverlayItem = function(item) { + return this.yaw.isClickableOverlayItem(item) || this.pitch.isClickableOverlayItem(item); + }; + + DirectionBox.prototype.onMousePressEvent = function(event, clickedOverlay) { + this.yaw.onMousePressEvent(event, clickedOverlay); + if (this.yaw.isMoving) { + return; + } + this.pitch.onMousePressEvent(event, clickedOverlay); + }; + + DirectionBox.prototype.onMouseMoveEvent = function(event) { + this.yaw.onMouseMoveEvent(event); + this.pitch.onMouseMoveEvent(event); + }; + DirectionBox.prototype.onMouseReleaseEvent = function(event) { - this.yaw.onMouseReleaseEvent(event); - this.pitch.onMouseReleaseEvent(event); - }; + this.yaw.onMouseReleaseEvent(event); + this.pitch.onMouseReleaseEvent(event); + }; DirectionBox.prototype.updateWithKeys = function(direction) { this.yaw.updateWithKeys(direction); this.pitch.updateWithKeys(direction); - }; + }; DirectionBox.prototype.highlight = function() { this.pitch.highlight(); this.yaw.highlight(); - + this.highlighted = true; - }; + }; DirectionBox.prototype.unhighlight = function() { this.pitch.unhighlight(); this.yaw.unhighlight(); - + this.highlighted = false; - }; - + }; + DirectionBox.prototype.setterFromWidget = function(value) { - var yawPitch = this.getValue(); - this.onValueChanged(yawPitch); - }; + var yawPitch = this.getValue(); + this.onValueChanged(yawPitch); + }; DirectionBox.prototype.onValueChanged = function(value) {}; DirectionBox.prototype.setValue = function(direction) { - var flatXZ = Math.sqrt(direction.x * direction.x + direction.z * direction.z); - if (flatXZ > 0.0) { - var flatX = direction.x / flatXZ; - var flatZ = direction.z / flatXZ; - var yaw = Math.acos(flatX) * 180 / Math.PI; - if (flatZ < 0) { - yaw = -yaw; - } - this.yaw.setValue(yaw); + var flatXZ = Math.sqrt(direction.x * direction.x + direction.z * direction.z); + if (flatXZ > 0.0) { + var flatX = direction.x / flatXZ; + var flatZ = direction.z / flatXZ; + var yaw = Math.acos(flatX) * 180 / Math.PI; + if (flatZ < 0) { + yaw = -yaw; } - this.pitch.setValue(direction.y); - }; + this.yaw.setValue(yaw); + } + this.pitch.setValue(direction.y); + }; - DirectionBox.prototype.getValue = function() { - var dirZ = this.pitch.getValue(); - var yaw = this.yaw.getValue() * Math.PI / 180; - var cosY = Math.sqrt(1 - dirZ*dirZ); - var value = {x:cosY * Math.cos(yaw), y:dirZ, z: cosY * Math.sin(yaw)}; - return value; + DirectionBox.prototype.getValue = function() { + var dirZ = this.pitch.getValue(); + var yaw = this.yaw.getValue() * Math.PI / 180; + var cosY = Math.sqrt(1 - dirZ * dirZ); + var value = { + x: cosY * Math.cos(yaw), + y: dirZ, + z: cosY * Math.sin(yaw) }; - + return value; + }; + DirectionBox.prototype.reset = function(resetValue) { - this.setValue(resetValue); - this.onValueChanged(resetValue); - }; - + this.setValue(resetValue); + this.onValueChanged(resetValue); + }; + DirectionBox.prototype.getHeight = function() { - if(!this.visible) { + if (!this.visible) { return 0; } - return 1.5 * this.thumbSize; - }; + return 1.5 * this.thumbSize; + }; DirectionBox.prototype.moveUp = function(newY) { this.pitch.moveUp(newY); @@ -627,59 +724,73 @@ var CHECK_MARK_COLOR = { this.yaw.show(); this.visible = true; } - + DirectionBox.prototype.destroy = function() { - this.yaw.destroy(); - this.pitch.destroy(); - }; - + this.yaw.destroy(); + this.pitch.destroy(); + }; + this.DirectionBox = DirectionBox; - + var textFontSize = 12; - - var CollapsablePanelItem = function (name, x, y, textWidth, height) { + + var CollapsablePanelItem = function(name, x, y, textWidth, height) { this.name = name; this.height = height; this.y = y; this.isCollapsable = true; - + var topMargin = (height - 1.5 * textFontSize); - + this.thumb = Overlays.addOverlay("image", { - color: {red: 255, green: 255, blue: 255}, - imageURL: HIFI_PUBLIC_BUCKET + 'images/tools/min-max-toggle.svg', - x: x, - y: y, - width: rawHeight, - height: rawHeight, - alpha: 1.0, - visible: true - }); - + color: { + red: 255, + green: 255, + blue: 255 + }, + imageURL: HIFI_PUBLIC_BUCKET + 'images/tools/min-max-toggle.svg', + x: x, + y: y, + width: rawHeight, + height: rawHeight, + alpha: 1.0, + visible: true + }); + this.title = Overlays.addOverlay("text", { - backgroundColor: { red: 255, green: 255, blue: 255 }, - x: x + rawHeight * 1.5, - y: y, - width: textWidth, - height: height, - alpha: 1.0, - backgroundAlpha: 0.5, - visible: true, - text: " " + name, - font: {size: textFontSize}, - topMargin: topMargin - }); + backgroundColor: { + red: 255, + green: 255, + blue: 255 + }, + x: x + rawHeight * 1.5, + y: y, + width: textWidth, + height: height, + alpha: 1.0, + backgroundAlpha: 0.5, + visible: true, + text: " " + name, + font: { + size: textFontSize + }, + topMargin: topMargin + }); }; CollapsablePanelItem.prototype.destroy = function() { Overlays.deleteOverlay(this.title); Overlays.deleteOverlay(this.thumb); }; - - + + CollapsablePanelItem.prototype.hide = function() { - Overlays.editOverlay(this.title, {visible: false}); - Overlays.editOverlay(this.thumb, {visible: false}); + Overlays.editOverlay(this.title, { + visible: false + }); + Overlays.editOverlay(this.thumb, { + visible: false + }); if (this.widget != null) { this.widget.hide(); @@ -687,8 +798,12 @@ var CHECK_MARK_COLOR = { }; CollapsablePanelItem.prototype.show = function() { - Overlays.editOverlay(this.title, {visible: true}); - Overlays.editOverlay(this.thumb, {visible: true}); + Overlays.editOverlay(this.title, { + visible: true + }); + Overlays.editOverlay(this.thumb, { + visible: true + }); if (this.widget != null) { this.widget.show(); @@ -696,8 +811,12 @@ var CHECK_MARK_COLOR = { }; CollapsablePanelItem.prototype.moveUp = function(newY) { - Overlays.editOverlay(this.title, {y: newY}); - Overlays.editOverlay(this.thumb, {y: newY}); + Overlays.editOverlay(this.title, { + y: newY + }); + Overlays.editOverlay(this.thumb, { + y: newY + }); if (this.widget != null) { this.widget.moveUp(newY); @@ -705,74 +824,92 @@ var CHECK_MARK_COLOR = { } CollapsablePanelItem.prototype.moveDown = function() { - Overlays.editOverlay(this.title, {y: this.y}); - Overlays.editOverlay(this.thumb, {y: this.y}); + Overlays.editOverlay(this.title, { + y: this.y + }); + Overlays.editOverlay(this.thumb, { + y: this.y + }); if (this.widget != null) { this.widget.moveDown(); } } this.CollapsablePanelItem = CollapsablePanelItem; - - var PanelItem = function (name, setter, getter, displayer, x, y, textWidth, valueWidth, height) { + + var PanelItem = function(name, setter, getter, displayer, x, y, textWidth, valueWidth, height) { //print("creating panel item: " + name); this.isCollapsable = false; this.name = name; this.y = y; this.isCollapsed = false; - - this.displayer = typeof displayer !== 'undefined' ? displayer : function(value) { - if(value == true) { + + this.displayer = typeof displayer !== 'undefined' ? displayer : function(value) { + if (value == true) { return "On"; } else if (value == false) { return "Off"; } - return value.toFixed(2); + return value.toFixed(2); }; - + var topMargin = (height - 1.5 * textFontSize); this.title = Overlays.addOverlay("text", { - backgroundColor: { red: 255, green: 255, blue: 255 }, - x: x, - y: y, - width: textWidth, - height: height, - alpha: 1.0, - backgroundAlpha: 0.5, - visible: true, - text: " " + name, - font: {size: textFontSize}, - topMargin: topMargin - }); - + backgroundColor: { + red: 255, + green: 255, + blue: 255 + }, + x: x, + y: y, + width: textWidth, + height: height, + alpha: 1.0, + backgroundAlpha: 0.5, + visible: true, + text: " " + name, + font: { + size: textFontSize + }, + topMargin: topMargin + }); + this.value = Overlays.addOverlay("text", { - backgroundColor: { red: 255, green: 255, blue: 255 }, - x: x + textWidth, - y: y, - width: valueWidth, - height: height, - alpha: 1.0, - backgroundAlpha: 0.5, - visible: true, - text: this.displayer(getter()), - font: {size: textFontSize}, - topMargin: topMargin - }); - + backgroundColor: { + red: 255, + green: 255, + blue: 255 + }, + x: x + textWidth, + y: y, + width: valueWidth, + height: height, + alpha: 1.0, + backgroundAlpha: 0.5, + visible: true, + text: this.displayer(getter()), + font: { + size: textFontSize + }, + topMargin: topMargin + }); + this.getter = getter; this.resetValue = getter(); - + this.setter = function(value) { - + setter(value); - Overlays.editOverlay(this.value, {text: this.displayer(getter())}); + Overlays.editOverlay(this.value, { + text: this.displayer(getter()) + }); if (this.widget) { this.widget.setValue(value); - } - + } + //print("successfully set value of widget to " + value); }; this.setterFromWidget = function(value) { @@ -782,16 +919,22 @@ var CHECK_MARK_COLOR = { if (this.widget) { this.widget.setValue(value); - } - Overlays.editOverlay(this.value, {text: this.displayer(value)}); - }; - + } + Overlays.editOverlay(this.value, { + text: this.displayer(value) + }); + }; + this.widget = null; }; PanelItem.prototype.hide = function() { - Overlays.editOverlay(this.title, {visible: false}); - Overlays.editOverlay(this.value, {visible: false}); + Overlays.editOverlay(this.title, { + visible: false + }); + Overlays.editOverlay(this.value, { + visible: false + }); if (this.widget != null) { this.widget.hide(); @@ -800,8 +943,12 @@ var CHECK_MARK_COLOR = { PanelItem.prototype.show = function() { - Overlays.editOverlay(this.title, {visible: true}); - Overlays.editOverlay(this.value, {visible: true}); + Overlays.editOverlay(this.title, { + visible: true + }); + Overlays.editOverlay(this.value, { + visible: true + }); if (this.widget != null) { this.widget.show(); @@ -811,8 +958,12 @@ var CHECK_MARK_COLOR = { PanelItem.prototype.moveUp = function(newY) { - Overlays.editOverlay(this.title, {y: newY}); - Overlays.editOverlay(this.value, {y: newY}); + Overlays.editOverlay(this.title, { + y: newY + }); + Overlays.editOverlay(this.value, { + y: newY + }); if (this.widget != null) { this.widget.moveUp(newY); @@ -822,8 +973,12 @@ var CHECK_MARK_COLOR = { PanelItem.prototype.moveDown = function() { - Overlays.editOverlay(this.title, {y: this.y}); - Overlays.editOverlay(this.value, {y: this.y}); + Overlays.editOverlay(this.title, { + y: this.y + }); + Overlays.editOverlay(this.value, { + y: this.y + }); if (this.widget != null) { this.widget.moveDown(); @@ -832,15 +987,15 @@ var CHECK_MARK_COLOR = { }; PanelItem.prototype.destroy = function() { - Overlays.deleteOverlay(this.title); - Overlays.deleteOverlay(this.value); + Overlays.deleteOverlay(this.title); + Overlays.deleteOverlay(this.value); - if (this.widget != null) { - this.widget.destroy(); - } - }; + if (this.widget != null) { + this.widget.destroy(); + } + }; this.PanelItem = PanelItem; - + var textWidth = 180; var valueWidth = 100; var widgetWidth = 300; @@ -848,7 +1003,7 @@ var CHECK_MARK_COLOR = { var rawYDelta = rawHeight * 1.5; var outerPanel = true; var widgets; - + var Panel = function(x, y) { @@ -856,50 +1011,53 @@ var CHECK_MARK_COLOR = { widgets = []; } outerPanel = false; - + this.x = x; this.y = y; - this.nextY = y; + this.nextY = y; print("creating panel at x: " + this.x + " y: " + this.y); - - this.widgetX = x + textWidth + valueWidth; - + + this.widgetX = x + textWidth + valueWidth; + this.items = new Array(); this.activeWidget = null; this.visible = true; this.indentation = 30; }; - + Panel.prototype.mouseMoveEvent = function(event) { if (this.activeWidget) { this.activeWidget.onMouseMoveEvent(event); } }; - + Panel.prototype.mousePressEvent = function(event) { // Make sure we quitted previous widget if (this.activeWidget) { this.activeWidget.onMouseReleaseEvent(event); } - this.activeWidget = null; - - var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); + this.activeWidget = null; + + var clickedOverlay = Overlays.getOverlayAtPoint({ + x: event.x, + y: event.y + }); this.handleCollapse(clickedOverlay); - + // If the user clicked any of the slider background then... for (var i in this.items) { var item = this.items[i]; var widget = this.items[i].widget; - + if (widget.isClickableOverlayItem(clickedOverlay)) { this.activeWidget = widget; - this.activeWidget.onMousePressEvent(event, clickedOverlay); + this.activeWidget.onMousePressEvent(event, clickedOverlay); break; - } - } + } + } }; Panel.prototype.mouseReleaseEvent = function(event) { @@ -907,39 +1065,42 @@ var CHECK_MARK_COLOR = { this.activeWidget.onMouseReleaseEvent(event); } this.activeWidget = null; - }; - - // Reset panel item upon double-clicking + }; + + // Reset panel item upon double-clicking Panel.prototype.mouseDoublePressEvent = function(event) { - var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); + var clickedOverlay = Overlays.getOverlayAtPoint({ + x: event.x, + y: event.y + }); this.handleReset(clickedOverlay); }; - - Panel.prototype.handleReset = function (clickedOverlay) { + + Panel.prototype.handleReset = function(clickedOverlay) { for (var i in this.items) { - - var item = this.items[i]; + + var item = this.items[i]; var widget = item.widget; - + if (item.isSubPanel && widget) { widget.handleReset(clickedOverlay); } - + if (clickedOverlay == item.title) { item.activeWidget = widget; item.activeWidget.reset(item.resetValue); break; - } + } } }; - Panel.prototype.handleCollapse = function (clickedOverlay) { + Panel.prototype.handleCollapse = function(clickedOverlay) { for (var i in this.items) { - - var item = this.items[i]; + + var item = this.items[i]; var widget = item.widget; - + if (item.isSubPanel && widget) { widget.handleCollapse(clickedOverlay); } @@ -948,20 +1109,20 @@ var CHECK_MARK_COLOR = { this.collapse(clickedOverlay); item.isCollapsed = true; break; - } else if (item.isCollapsed && item.isCollapsable && clickedOverlay == item.thumb) { + } else if (item.isCollapsed && item.isCollapsable && clickedOverlay == item.thumb) { this.expand(clickedOverlay); item.isCollapsed = false; } } }; - Panel.prototype.collapse = function (clickedOverlay) { + Panel.prototype.collapse = function(clickedOverlay) { var keys = Object.keys(this.items); - + for (var i = 0; i < keys.length; ++i) { var item = this.items[keys[i]]; - if(item.isCollapsable && clickedOverlay == item.thumb) { + if (item.isCollapsable && clickedOverlay == item.thumb) { var panel = item.widget; panel.hide(); break; @@ -969,40 +1130,40 @@ var CHECK_MARK_COLOR = { } // Now recalculate new heights of subsequent widgets - for(var j = i + 1; j < keys.length; ++j) { + for (var j = i + 1; j < keys.length; ++j) { this.items[keys[j]].moveUp(this.getCurrentY(keys[j])); } }; - Panel.prototype.expand = function (clickedOverlay) { + Panel.prototype.expand = function(clickedOverlay) { var keys = Object.keys(this.items); - + for (var i = 0; i < keys.length; ++i) { var item = this.items[keys[i]]; - if(item.isCollapsable && clickedOverlay == item.thumb) { + if (item.isCollapsable && clickedOverlay == item.thumb) { var panel = item.widget; panel.show(); break; } - } - // Now recalculate new heights of subsequent widgets - for(var j = i + 1; j < keys.length; ++j) { + } + // Now recalculate new heights of subsequent widgets + for (var j = i + 1; j < keys.length; ++j) { this.items[keys[j]].moveDown(); } }; - + Panel.prototype.onMousePressEvent = function(event, clickedOverlay) { for (var i in this.items) { var item = this.items[i]; - if(item.widget.isClickableOverlayItem(clickedOverlay)) { + if (item.widget.isClickableOverlayItem(clickedOverlay)) { item.activeWidget = item.widget; - item.activeWidget.onMousePressEvent(event,clickedOverlay); + item.activeWidget.onMousePressEvent(event, clickedOverlay); } } }; - + Panel.prototype.onMouseMoveEvent = function(event) { for (var i in this.items) { var item = this.items[i]; @@ -1011,7 +1172,7 @@ var CHECK_MARK_COLOR = { } } }; - + Panel.prototype.onMouseReleaseEvent = function(event, clickedOverlay) { for (var i in this.items) { var item = this.items[i]; @@ -1021,7 +1182,7 @@ var CHECK_MARK_COLOR = { item.activeWidget = null; } }; - + Panel.prototype.onMouseDoublePressEvent = function(event, clickedOverlay) { for (var i in this.items) { var item = this.items[i]; @@ -1035,13 +1196,13 @@ var CHECK_MARK_COLOR = { var tabIndex = 0; Panel.prototype.keyPressEvent = function(event) { - if(event.text == "TAB" && !event.isShifted) { + if (event.text == "TAB" && !event.isShifted) { tabView = true; - if(tabIndex < widgets.length) { - if(tabIndex > 0 && widgets[tabIndex - 1].highlighted) { + if (tabIndex < widgets.length) { + if (tabIndex > 0 && widgets[tabIndex - 1].highlighted) { // Unhighlight previous widget widgets[tabIndex - 1].unhighlight(); - } + } widgets[tabIndex].highlight(); tabIndex++; } else { @@ -1052,32 +1213,32 @@ var CHECK_MARK_COLOR = { tabIndex++; } } else if (tabView && event.isShifted) { - if(tabIndex > 0) { + if (tabIndex > 0) { tabIndex--; - if(tabIndex < widgets.length && widgets[tabIndex + 1].highlighted) { + if (tabIndex < widgets.length && widgets[tabIndex + 1].highlighted) { // Unhighlight previous widget widgets[tabIndex + 1].unhighlight(); - } + } widgets[tabIndex].highlight(); } else { widgets[tabIndex].unhighlight(); //Wrap around to end tabIndex = widgets.length - 1; - widgets[tabIndex].highlight(); + widgets[tabIndex].highlight(); } } else if (event.text == "LEFT") { - for(var i = 0; i < widgets.length; i++) { + for (var i = 0; i < widgets.length; i++) { // Find the highlighted widget, allow control with arrow keys - if(widgets[i].highlighted) { + if (widgets[i].highlighted) { var k = -1; widgets[i].updateWithKeys(k); break; } } } else if (event.text == "RIGHT") { - for(var i = 0; i < widgets.length; i++) { + for (var i = 0; i < widgets.length; i++) { // Find the highlighted widget, allow control with arrow keys - if(widgets[i].highlighted) { + if (widgets[i].highlighted) { var k = 1; widgets[i].updateWithKeys(k); break; @@ -1095,93 +1256,105 @@ var CHECK_MARK_COLOR = { slider.minValue = minValue; slider.maxValue = maxValue; widgets.push(slider); - + item.widget = slider; - item.widget.onValueChanged = function(value) { item.setterFromWidget(value); }; - item.setter(getValue()); + item.widget.onValueChanged = function(value) { + item.setterFromWidget(value); + }; + item.setter(getValue()); this.items[name] = item; }; - + Panel.prototype.newCheckbox = function(name, setValue, getValue, displayValue) { var display; if (displayValue == true) { - display = function() {return "On";}; + display = function() { + return "On"; + }; } else if (displayValue == false) { - display = function() {return "Off";}; + display = function() { + return "Off"; + }; } - + this.nextY = this.y + this.getHeight(); - + var item = new PanelItem(name, setValue, getValue, display, this.x, this.nextY, textWidth, valueWidth, rawHeight); - + var checkbox = new Checkbox(this.widgetX, this.nextY, widgetWidth, rawHeight); widgets.push(checkbox); - + item.widget = checkbox; - item.widget.onValueChanged = function(value) { item.setterFromWidget(value); }; - item.setter(getValue()); - this.items[name] = item; - - //print("created Item... checkbox=" + name); - }; - - Panel.prototype.newColorBox = function(name, setValue, getValue, displayValue) { - this.nextY = this.y + this.getHeight(); - - var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); - - var colorBox = new ColorBox(this.widgetX, this.nextY, widgetWidth, rawHeight); - widgets.push(colorBox); - - item.widget = colorBox; - item.widget.onValueChanged = function(value) { item.setterFromWidget(value); }; - item.setter(getValue()); + item.widget.onValueChanged = function(value) { + item.setterFromWidget(value); + }; + item.setter(getValue()); this.items[name] = item; - // print("created Item... colorBox=" + name); + //print("created Item... checkbox=" + name); }; - + + Panel.prototype.newColorBox = function(name, setValue, getValue, displayValue) { + this.nextY = this.y + this.getHeight(); + + var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); + + var colorBox = new ColorBox(this.widgetX, this.nextY, widgetWidth, rawHeight); + widgets.push(colorBox); + + item.widget = colorBox; + item.widget.onValueChanged = function(value) { + item.setterFromWidget(value); + }; + item.setter(getValue()); + this.items[name] = item; + + // print("created Item... colorBox=" + name); + }; + Panel.prototype.newDirectionBox = function(name, setValue, getValue, displayValue) { this.nextY = this.y + this.getHeight(); - + var item = new PanelItem(name, setValue, getValue, displayValue, this.x, this.nextY, textWidth, valueWidth, rawHeight); var directionBox = new DirectionBox(this.widgetX, this.nextY, widgetWidth, rawHeight); widgets.push(directionBox); - + item.widget = directionBox; - item.widget.onValueChanged = function(value) { item.setterFromWidget(value); }; - item.setter(getValue()); + item.widget.onValueChanged = function(value) { + item.setterFromWidget(value); + }; + item.setter(getValue()); this.items[name] = item; - - // print("created Item... directionBox=" + name); + + // print("created Item... directionBox=" + name); }; - + Panel.prototype.newSubPanel = function(name) { - + this.nextY = this.y + this.getHeight(); - + var item = new CollapsablePanelItem(name, this.x, this.nextY, textWidth, rawHeight, panel); item.isSubPanel = true; - + this.nextY += 1.5 * item.height; - + var subPanel = new Panel(this.x + this.indentation, this.nextY); - + item.widget = subPanel; this.items[name] = item; return subPanel; - // print("created Item... subPanel=" + name); + // print("created Item... subPanel=" + name); }; - + Panel.prototype.onValueChanged = function(value) { for (var i in this.items) { this.items[i].widget.onValueChanged(value); } }; - - + + Panel.prototype.set = function(name, value) { var item = this.items[name]; if (item != null) { @@ -1189,7 +1362,7 @@ var CHECK_MARK_COLOR = { } return null; }; - + Panel.prototype.get = function(name) { var item = this.items[name]; if (item != null) { @@ -1197,7 +1370,7 @@ var CHECK_MARK_COLOR = { } return null; }; - + Panel.prototype.update = function(name) { var item = this.items[name]; if (item != null) { @@ -1205,7 +1378,7 @@ var CHECK_MARK_COLOR = { } return null; }; - + Panel.prototype.isClickableOverlayItem = function(item) { for (var i in this.items) { if (this.items[i].widget.isClickableOverlayItem(item)) { @@ -1214,30 +1387,30 @@ var CHECK_MARK_COLOR = { } return false; }; - + Panel.prototype.getHeight = function() { var height = 0; - + for (var i in this.items) { - height += this.items[i].widget.getHeight(); - if(this.items[i].isSubPanel && this.items[i].widget.visible) { - height += 1.5 * rawHeight; - } + height += this.items[i].widget.getHeight(); + if (this.items[i].isSubPanel && this.items[i].widget.visible) { + height += 1.5 * rawHeight; + } } return height; }; Panel.prototype.moveUp = function() { - for (var i in this.items) { + for (var i in this.items) { this.items[i].widget.moveUp(); - } + } }; Panel.prototype.moveDown = function() { for (var i in this.items) { this.items[i].widget.moveDown(); - } + } }; Panel.prototype.getCurrentY = function(key) { @@ -1247,34 +1420,34 @@ var CHECK_MARK_COLOR = { for (var i = 0; i < keys.indexOf(key); ++i) { var item = this.items[keys[i]]; - height += item.widget.getHeight(); - - if(item.isSubPanel) { + height += item.widget.getHeight(); + + if (item.isSubPanel) { height += 1.5 * rawHeight; - - } + + } } return this.y + height; }; - + Panel.prototype.hide = function() { for (var i in this.items) { - if(this.items[i].isSubPanel) { + if (this.items[i].isSubPanel) { this.items[i].widget.hide(); } this.items[i].hide(); - } + } this.visible = false; }; Panel.prototype.show = function() { for (var i in this.items) { - if(this.items[i].isSubPanel) { + if (this.items[i].isSubPanel) { this.items[i].widget.show(); } this.items[i].show(); - } + } this.visible = true; }; @@ -1282,21 +1455,29 @@ var CHECK_MARK_COLOR = { Panel.prototype.destroy = function() { for (var i in this.items) { - if(this.items[i].isSubPanel) { + if (this.items[i].isSubPanel) { this.items[i].widget.destroy(); } this.items[i].destroy(); - } + } }; this.Panel = Panel; })(); Script.scriptEnding.connect(function scriptEnding() { - Controller.releaseKeyEvents({text: "left"}); - Controller.releaseKeyEvents({key: "right"}); + Controller.releaseKeyEvents({ + text: "left" + }); + Controller.releaseKeyEvents({ + key: "right" + }); }); -Controller.captureKeyEvents({text: "left"}); -Controller.captureKeyEvents({text: "right"}); \ No newline at end of file +Controller.captureKeyEvents({ + text: "left" +}); +Controller.captureKeyEvents({ + text: "right" +}); \ No newline at end of file From ccb3d433afb69e6ce391c7d302092143515a96f1 Mon Sep 17 00:00:00 2001 From: bwent Date: Fri, 24 Jul 2015 09:43:49 -0700 Subject: [PATCH 131/242] Example script solarsystem.js with orbiting satellite game --- examples/example/games/satellite.js | 279 +++++++++++++ examples/example/games/solarsystem.js | 554 ++++++++++++++++++++++++++ examples/utilities/tools/vector.js | 197 +++++++++ 3 files changed, 1030 insertions(+) create mode 100644 examples/example/games/satellite.js create mode 100644 examples/example/games/solarsystem.js create mode 100644 examples/utilities/tools/vector.js diff --git a/examples/example/games/satellite.js b/examples/example/games/satellite.js new file mode 100644 index 0000000000..d2e4dd2a99 --- /dev/null +++ b/examples/example/games/satellite.js @@ -0,0 +1,279 @@ +// +// satellite.js +// games +// +// Created by Bridget Went 7/1/2015. +// Copyright 2015 High Fidelity, Inc. +// +// A game to bring a satellite model into orbit around an animated earth model . +// - Double click to create a new satellite +// - Click on the satellite, drag a vector arrow to specify initial velocity +// - Release mouse to launch the active satellite +// - Orbital movement is calculated using equations of gravitational physics +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include('../../utilities/tools/vector.js'); + +var URL = "https://s3.amazonaws.com/hifi-public/marketplace/hificontent/Scripts/planets/"; + +SatelliteGame = function() { + var MAX_RANGE = 50.0; + var Y_AXIS = { + x: 0, + y: 1, + z: 0 + } + var LIFETIME = 6000; + + var v0; + var T = 4.0; + var M = 16000.0; + var m = M * 0.000000333; + var ERROR_THRESH = 20.0; + + // Create the spinning earth model + var EARTH_SIZE = 20.0; + var CLOUDS_OFFSET = 0.5; + var SPIN = 0.1; + var ZONE_DIM = 10.0; + var LIGHT_INTENSITY = 1.2; + + Earth = function(position, size) { + this.earth = Entities.addEntity({ + type: "Model", + shapeType: 'sphere', + modelURL: URL + "earth.fbx", + position: position, + dimensions: { + x: size, + y: size, + z: size + }, + rotation: Quat.angleAxis(180, {x: 1, y: 0, z: 0}), + angularVelocity: { x: 0.00, y: 0.5 * SPIN, z: 0.00 }, + angularDamping: 0.0, + damping: 0.0, + ignoreCollisions: false, + lifetime: 6000, + collisionsWillMove: false, + visible: true + }); + + this.clouds = Entities.addEntity({ + type: "Model", + shapeType: 'sphere', + modelURL: URL + "clouds.fbx?", + position: position, + dimensions: { + x: size + CLOUDS_OFFSET, + y: size + CLOUDS_OFFSET, + z: size + CLOUDS_OFFSET + }, + angularVelocity: { x: 0.00, y: SPIN, z: 0.00 }, + angularDamping: 0.0, + damping: 0.0, + ignoreCollisions: false, + lifetime: LIFETIME, + collisionsWillMove: false, + visible: true + }); + + this.zone = Entities.addEntity({ + type: "Zone", + position: position, + dimensions: { + x: ZONE_DIM, + y: ZONE_DIM, + z: ZONE_DIM + }, + keyLightDirection: Vec3.normalize(Vec3.subtract(position, Camera.getPosition())), + keyLightIntensity: LIGHT_INTENSITY + }); + + this.cleanup = function() { + Entities.deleteEntity(this.clouds); + Entities.deleteEntity(this.earth); + Entities.deleteEntity(this.zone); + } + } + + // Create earth model + var center = Vec3.sum(Camera.getPosition(), Vec3.multiply(MAX_RANGE, Quat.getFront(Camera.getOrientation()))); + var distance = Vec3.length(Vec3.subtract(center, Camera.getPosition())); + var earth = new Earth(center, EARTH_SIZE); + + var satellites = []; + var SATELLITE_SIZE = 2.0; + var launched = false; + var activeSatellite; + + Satellite = function(position, planetCenter) { + // The Satellite class + + this.launched = false; + this.startPosition = position; + this.readyToLaunch = false; + this.radius = Vec3.length(Vec3.subtract(position, planetCenter)); + + this.satellite = Entities.addEntity({ + type: "Model", + modelURL: "https://s3.amazonaws.com/hifi-public/marketplace/hificontent/Scripts/planets/satellite/satellite.fbx", + position: this.startPosition, + dimensions: { + x: SATELLITE_SIZE, + y: SATELLITE_SIZE, + z: SATELLITE_SIZE + }, + angularDamping: 0.0, + damping: 0.0, + ignoreCollisions: false, + lifetime: LIFETIME, + collisionsWillMove: false, + }); + + this.getProperties = function() { + return Entities.getEntityProperties(this.satellite); + } + + this.launch = function() { + var prop = Entities.getEntityProperties(this.satellite); + var between = Vec3.subtract(planetCenter, prop.position); + var radius = Vec3.length(between); + this.G = (4.0 * Math.PI * Math.PI * Math.pow(radius, 3.0)) / (M * T * T); + + var v0 = Vec3.normalize(Vec3.cross(between, Y_AXIS)); + v0 = Vec3.multiply(Math.sqrt((this.G * M) / radius), v0); + v0 = Vec3.multiply(this.arrow.magnitude, v0); + v0 = Vec3.multiply(Vec3.length(v0), this.arrow.direction); + + Entities.editEntity(this.satellite, { velocity: v0 }); + this.launched = true; + }; + + + this.update = function(deltaTime) { + var prop = Entities.getEntityProperties(this.satellite); + var between = Vec3.subtract(prop.position, planetCenter); + var radius = Vec3.length(between); + var a = -(this.G * M) * Math.pow(radius, (-2.0)); + var speed = a * deltaTime; + var vel = Vec3.multiply(speed, Vec3.normalize(between)); + + var newVelocity = Vec3.sum(prop.velocity, vel); + var newPos = Vec3.sum(prop.position, Vec3.multiply(newVelocity, deltaTime)); + + Entities.editEntity(this.satellite, { + velocity: newVelocity, + position: newPos + }); + }; + } + + function mouseDoublePressEvent(event) { + var pickRay = Camera.computePickRay(event.x, event.y); + var addVector = Vec3.multiply(pickRay.direction, distance); + var point = Vec3.sum(Camera.getPosition(), addVector); + + // Create a new satellite + activeSatellite = new Satellite(point, center); + satellites.push(activeSatellite); + } + + function mousePressEvent(event) { + if (!activeSatellite) { + return; + } + // Reset label + if (activeSatellite.arrow) { + activeSatellite.arrow.deleteLabel(); + } + var statsPosition = Vec3.sum(Camera.getPosition(), Vec3.multiply(MAX_RANGE * 0.5, Quat.getFront(Camera.getOrientation()))); + var pickRay = Camera.computePickRay(event.x, event.y) + var rayPickResult = Entities.findRayIntersection(pickRay, true); + if (rayPickResult.entityID === activeSatellite.satellite) { + // Create a draggable vector arrow at satellite position + activeSatellite.arrow = new VectorArrow(distance, true, "INITIAL VELOCITY", statsPosition); + activeSatellite.arrow.onMousePressEvent(event); + activeSatellite.arrow.isDragging = true; + } + } + + function mouseMoveEvent(event) { + if(!activeSatellite || !activeSatellite.arrow || !activeSatellite.arrow.isDragging) { + return; + } + activeSatellite.arrow.onMouseMoveEvent(event); + } + + function mouseReleaseEvent(event) { + if(!activeSatellite || !activeSatellite.arrow || !activeSatellite.arrow.isDragging) { + return; + } + activeSatellite.arrow.onMouseReleaseEvent(event); + activeSatellite.launch(); + activeSatellite.arrow.cleanup(); + } + + var counter = 0.0; + var TIME = 500; + + function update(deltaTime) { + if(!activeSatellite) { + return; + } + // Update all satellites + for (var i = 0; i < satellites.length; i++) { + if (!satellites[i].launched) { + return; + } + satellites[i].update(deltaTime); + } + + counter++; + if (counter % TIME == 0) { + var prop = activeSatellite.getProperties(); + var error = calcEnergyError(prop.position, Vec3.length(prop.velocity)); + if (Math.abs(error) <= ERROR_THRESH) { + activeSatellite.arrow.editLabel("Nice job! The satellite has reached a stable orbit."); + } else { + activeSatellite.arrow.editLabel("Try again! The satellite is in an unstable orbit."); + } + } + } + + this.endGame = function() { + print("ending game"); + for(var i = 0; i < satellites.length; i++) { + Entities.deleteEntitiy(satellites[i].satellite); + satellites[i].arrow.cleanup(); + } + earth.cleanup(); + } + + + function calcEnergyError(pos, vel) { + //Calculate total energy error for active satellite's orbital motion + var radius = activeSatellite.radius; + var G = (4.0 * Math.PI * Math.PI * Math.pow(radius, 3.0)) / (M * T * T); + var v0 = Math.sqrt((G * M) / radius); + + var totalEnergy = 0.5 * M * Math.pow(v0, 2.0) - ((G * M * m) / radius); + var measuredEnergy = 0.5 * M * Math.pow(vel, 2.0) - ((G * M * m) / Vec3.length(Vec3.subtract(pos, center))); + var error = ((measuredEnergy - totalEnergy) / totalEnergy) * 100; + return error; + } + + Controller.mousePressEvent.connect(mousePressEvent); + Controller.mouseDoublePressEvent.connect(mouseDoublePressEvent); + Controller.mouseMoveEvent.connect(mouseMoveEvent); + Controller.mouseReleaseEvent.connect(mouseReleaseEvent); + Script.update.connect(update); + Script.scriptEnding.connect(this.endGame); + +} + + + diff --git a/examples/example/games/solarsystem.js b/examples/example/games/solarsystem.js new file mode 100644 index 0000000000..b651e551f0 --- /dev/null +++ b/examples/example/games/solarsystem.js @@ -0,0 +1,554 @@ +// +// solarsystem.js +// games +// +// Created by Bridget Went, 5/28/15. +// Copyright 2015 High Fidelity, Inc. +// +// The start to a project to build a virtual physics classroom to simulate the solar system, gravity, and orbital physics. +// A sun with oribiting planets is created in front of the user. UI elements allow for adjusting the period, gravity, trails, and energy recalculations. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include('../../utilities/tools/cookies.js'); +Script.include('satellite.js'); + +var BASE_URL = "https://s3.amazonaws.com/hifi-public/marketplace/hificontent/Scripts/planets/planets/"; + +var NUM_PLANETS = 8; + +var trailsEnabled = true; +var energyConserved = true; +var planetView = false; +var earthView = false; +var satelliteGame; + +var PANEL_X = 850; +var PANEL_Y = 600; +var BUTTON_SIZE = 20; +var PADDING = 20; + +var DAMPING = 0.0; +var LIFETIME = 6000; +var ERROR_THRESH = 2.0; +var TIME_STEP = 70.0; + +var MAX_POINTS_PER_LINE = 5; +var LINE_DIM = 10; +var LINE_WIDTH = 3.0; +var line; +var planetLines = []; +var trails = []; + +var BOUNDS = 200; + + +// Alert user to move if they are too close to domain bounds +if (MyAvatar.position.x < BOUNDS || MyAvatar.position.x > TREE_SCALE - BOUNDS + || MyAvatar.position.y < BOUNDS || MyAvatar.position.y > TREE_SCALE - BOUNDS + || MyAvatar.position.z < BOUNDS || MyAvatar.position.z > TREE_SCALE - BOUNDS) { + Window.alert("Please move at least 200m away from domain bounds."); + return; +} + +// Save intiial avatar and camera position +var startingPosition = MyAvatar.position; +var startFrame = Window.location.href; + +// Place the sun +var MAX_RANGE = 80.0; +var SUN_SIZE = 8.0; +var center = Vec3.sum(startingPosition, Vec3.multiply(MAX_RANGE, Quat.getFront(Camera.getOrientation()))); + +var theSun = Entities.addEntity({ + type: "Model", + modelURL: BASE_URL + "sun.fbx", + position: center, + dimensions: { + x: SUN_SIZE, + y: SUN_SIZE, + z: SUN_SIZE + }, + angularDamping: DAMPING, + damping: DAMPING, + ignoreCollisions: false, + lifetime: LIFETIME, + collisionsWillMove: false +}); + +var planets = []; +var planet_properties = []; + +// Reference values +var radius = 7.0; +var T_ref = 1.0; +var size = 1.0; +var M = 250.0; +var m = M * 0.000000333; +var G = (Math.pow(radius, 3.0) / Math.pow((T_ref / (2.0 * Math.PI)), 2.0)) / M; +var G_ref = G; + +// Adjust size and distance as number of planets increases +var DELTA_RADIUS = 1.8; +var DELTA_SIZE = 0.2; + +function initPlanets() { + for (var i = 0; i < NUM_PLANETS; ++i) { + var v0 = Math.sqrt((G * M) / radius); + var T = (2.0 * Math.PI) * Math.sqrt(Math.pow(radius, 3.0) / (G * M)); + + if (i == 0) { + var color = {red: 255, green: 255, blue: 255}; + } else if (i == 1) { + var color = {red: 255, green: 160, blue: 110}; + } else if (i == 2) { + var color = {red: 10, green: 150, blue: 160}; + } else if (i == 3) { + var color = {red: 180, green: 70, blue: 10}; + } else if (i == 4) { + var color = {red: 250, green: 140, blue: 0}; + } else if (i == 5) { + var color = {red: 235, green: 215, blue: 0}; + } else if (i == 6) { + var color = {red:135, green: 205, blue: 240}; + } else if (i == 7) { + var color = {red:30, green: 140, blue: 255}; + } + + var prop = { + radius: radius, + position: Vec3.sum(center, {x: radius, y: 0.0, z: 0.0}), + lineColor: color, + period: T, + dimensions: size, + velocity: Vec3.multiply(v0, Vec3.normalize({x: 0, y: -0.2, z: 0.9})) + }; + planet_properties.push(prop); + + planets.push(Entities.addEntity({ + type: "Model", + modelURL: BASE_URL + (i + 1) + ".fbx", + position: prop.position, + dimensions: { + x: prop.dimensions, + y: prop.dimensions, + z: prop.dimensions + }, + velocity: prop.velocity, + angularDamping: DAMPING, + damping: DAMPING, + ignoreCollisions: false, + lifetime: LIFETIME, + collisionsWillMove: true, + })); + + radius *= DELTA_RADIUS; + size += DELTA_SIZE; + } +} + +// Initialize planets +initPlanets(); + + +var labels = []; +var labelLines = []; +var labelsShowing = false; +var LABEL_X = 8.0; +var LABEL_Y = 3.0; +var LABEL_Z = 1.0; +var LABEL_DIST = 8.0; +var TEXT_HEIGHT = 1.0; +var sunLabel; + +function showLabels() { + labelsShowing = true; + for (var i = 0; i < NUM_PLANETS; i++) { + var properties = planet_properties[i]; + var text; + if (i == 0) { + text = "Mercury"; + } else if (i == 1) { + text = "Venus"; + } else if (i == 2) { + text = "Earth"; + } else if (i == 3) { + text = "Mars"; + } else if (i == 4) { + text = "Jupiter"; + } else if (i == 5) { + text = "Saturn"; + } else if (i == 6) { + text = "Uranus"; + } else if (i == 7) { + text = "Neptune"; + } + + text = text + " Speed: " + Vec3.length(properties.velocity).toFixed(2); + + var labelPos = Vec3.sum(planet_properties[i].position, {x: 0.0, y: LABEL_DIST, z: LABEL_DIST}); + var linePos = planet_properties[i].position; + labelLines.push(Entities.addEntity( { + type: "Line", + position: linePos, + dimensions: {x: 20, y: 20, z: 20}, + lineWidth: 3.0, + color: {red: 255, green: 255, blue: 255}, + linePoints: [{x: 0, y: 0, z: 0}, computeLocalPoint(linePos, labelPos)] + })); + + labels.push(Entities.addEntity( { + type: "Text", + text: text, + lineHeight: TEXT_HEIGHT, + dimensions: {x: LABEL_X, y: LABEL_Y, z: LABEL_Z}, + position: labelPos, + backgroundColor: {red: 10, green: 10, blue: 10}, + textColor: {red: 255, green: 255, blue: 255}, + faceCamera: true + })); + } +} + +function hideLabels() { + labelsShowing = false; + Entities.deleteEntity(sunLabel); + + for (var i = 0; i < NUM_PLANETS; ++i) { + Entities.deleteEntity(labelLines[i]); + Entities.deleteEntity(labels[i]); + } + labels = []; + labelLines = []; +} + +var time = 0.0; +var elapsed; +var counter = 0; +var dt = 1.0 / TIME_STEP; + +function update(deltaTime) { + if (paused) { + return; + } + deltaTime = dt; + time++; + + for (var i = 0; i < NUM_PLANETS; ++i) { + var properties = planet_properties[i]; + var between = Vec3.subtract(properties.position, center); + var speed = getAcceleration(properties.radius) * deltaTime; + var vel = Vec3.multiply(speed, Vec3.normalize(between)); + + // Update velocity and position + properties.velocity = Vec3.sum(properties.velocity, vel); + properties.position = Vec3.sum(properties.position, Vec3.multiply(properties.velocity, deltaTime)); + Entities.editEntity(planets[i], { + velocity: properties.velocity, + position: properties.position + }); + + + // Create new or update current trail + if (trailsEnabled) { + var lineStack = planetLines[i]; + var point = properties.position; + var prop = Entities.getEntityProperties(lineStack[lineStack.length - 1]); + var linePos = prop.position; + + trails[i].push(computeLocalPoint(linePos, point)); + + Entities.editEntity(lineStack[lineStack.length - 1], { + linePoints: trails[i] + }); + if (trails[i].length === MAX_POINTS_PER_LINE) { + trails[i] = newLine(lineStack, point, properties.period, properties.lineColor); + } + } + + // Measure total energy every 10 updates, recalibrate velocity if necessary + if (energyConserved) { + if (counter % 10 === 0) { + var error = calcEnergyError(planets[i], properties.radius, properties.v0, properties.velocity, properties.position); + if (Math.abs(error) >= ERROR_THRESH) { + var speed = adjustVelocity(planets[i], properties.position); + properties.velocity = Vec3.multiply(speed, Vec3.normalize(properties.velocity)); + } + } + } + } + + counter++; + if (time % TIME_STEP == 0) { + elapsed++; + } +} + +function computeLocalPoint(linePos, worldPoint) { + var localPoint = Vec3.subtract(worldPoint, linePos); + return localPoint; +} + +function getAcceleration(radius) { + var acc = -(G * M) * Math.pow(radius, (-2.0)); + return acc; +} + +// Create a new trail +function resetTrails(planetIndex) { + elapsed = 0.0; + var properties = planet_properties[planetIndex]; + + var trail = []; + var lineStack = []; + + //add the first line to both the line entity stack and the trail + trails.push(newLine(lineStack, properties.position, properties.period, properties.lineColor)); + planetLines.push(lineStack); +} + +// Create a new line +function newLine(lineStack, point, period, color) { + if (elapsed < period) { + var line = Entities.addEntity({ + position: point, + type: "Line", + color: color, + dimensions: { + x: LINE_DIM, + y: LINE_DIM, + z: LINE_DIM + }, + lifetime: LIFETIME, + lineWidth: LINE_WIDTH + }); + lineStack.push(line); + } else { + // Begin overwriting first lines after one full revolution (one period) + var firstLine = lineStack.shift(); + Entities.editEntity(firstLine, { + position: point, + linePoints: [{x: 0.0, y: 0.0, z: 0.0}] + }); + lineStack.push(firstLine); + + } + var points = []; + points.push(computeLocalPoint(point, point)); + return points; +} + +// Measure energy error, recalculate velocity to return to initial net energy +var totalEnergy; +var measuredEnergy; +var measuredPE; + +function calcEnergyError(planet, radius, v0, v, pos) { + totalEnergy = 0.5 * M * Math.pow(v0, 2.0) - ((G * M * m) / radius); + measuredEnergy = 0.5 * M * Math.pow(v, 2.0) - ((G * M * m) / Vec3.length(Vec3.subtract(center, pos))); + var error = ((measuredEnergy - totalEnergy) / totalEnergy) * 100; + return error; +} +function adjustVelocity(planet, pos) { + var measuredPE = -(G * M * m) / Vec3.length(Vec3.subtract(center, pos)); + return Math.sqrt(2 * (totalEnergy - measuredPE) / M); +} + + +// Allow user to toggle pausing the model, switch to planet view +var pauseButton = Overlays.addOverlay("text", { + backgroundColor: { red: 200, green: 200, blue: 255 }, + text: "Pause", + x: PANEL_X, + y: PANEL_Y - 30, + width: 70, + height: 20, + alpha: 1.0, + backgroundAlpha: 0.5, + visible: true +}); + +var paused = false; + +function mousePressEvent(event) { + var clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + if(clickedOverlay == pauseButton) { + paused = !paused; + for (var i = 0; i < NUM_PLANETS; ++i) { + Entities.editEntity(planets[i], { velocity: {x: 0.0, y: 0.0, z: 0.0} }); + } + if (paused && !labelsShowing) { + Overlays.editOverlay(pauseButton, { text: "Paused", backgroundColor: {red: 255, green: 50, blue: 50} } ); + showLabels(); + } + if (paused == false && labelsShowing) { + Overlays.editOverlay(pauseButton, { text: "Pause", backgroundColor: {red: 200, green: 200, blue: 255}}); + hideLabels(); + } + planetView = false; + } +} + +function keyPressEvent(event) { + // Jump back to solar system view + if (event.text == "TAB" && planetView) { + if (earthView) { + satelliteGame.endGame(); + earthView = false; + } + MyAvatar.position = startingPosition; + } +} + +function mouseDoublePressEvent(event) { + if(earthView) { + return; + } + var pickRay = Camera.computePickRay(event.x, event.y) + var rayPickResult = Entities.findRayIntersection(pickRay, true); + + for (var i = 0; i < NUM_PLANETS; ++i) { + if (rayPickResult.entityID === labels[i]) { + planetView = true; + if (i == 2) { + MyAvatar.position = Vec3.sum(center, {x: 200, y: 200, z: 200}); + Camera.setPosition(Vec3.sum(center, {x: 200, y: 200, z: 200})); + earthView = true; + satelliteGame = new SatelliteGame(); + + } else { + MyAvatar.position = Vec3.sum({x: 0.0, y: 0.0, z: 3.0}, planet_properties[i].position); + Camera.lookAt(planet_properties[i].position); + } + break; + } + } +} + + + + +// Create UI panel +var panel = new Panel(PANEL_X, PANEL_Y); +var panelItems = []; + +var g_multiplier = 1.0; +panelItems.push(panel.newSlider("Adjust Gravitational Force: ", 0.1, 5.0, + function (value) { + g_multiplier = value; + G = G_ref * g_multiplier; + }, + + function () { + return g_multiplier; + }, + function (value) { + return value.toFixed(1) + "x"; + })); + +var period_multiplier = 1.0; +var last_alpha = period_multiplier; +panelItems.push(panel.newSlider("Adjust Orbital Period: ", 0.1, 3.0, + function (value) { + period_multiplier = value; + changePeriod(period_multiplier); + }, + function () { + return period_multiplier; + }, + function (value) { + return (value).toFixed(2) + "x"; + })); + +panelItems.push(panel.newCheckbox("Leave Trails: ", + function (value) { + trailsEnabled = value; + if (trailsEnabled) { + for (var i = 0; i < NUM_PLANETS; ++i) { + resetTrails(i); + } + //if trails are off and we've already created trails, remove existing trails + } else if (planetLines.length != 0) { + for (var i = 0; i < NUM_PLANETS; ++i) { + for (var j = 0; j < planetLines[i].length; ++j) { + Entities.deleteEntity(planetLines[i][j]); + } + planetLines[i] = []; + } + } + }, + function () { + return trailsEnabled; + }, + function (value) { + return value; + })); + +panelItems.push(panel.newCheckbox("Energy Error Calculations: ", + function (value) { + energyConserved = value; + }, + function () { + return energyConserved; + }, + function (value) { + return value; + })); + +// Update global G constant, period, poke velocity to new value +function changePeriod(alpha) { + var ratio = last_alpha / alpha; + G = Math.pow(ratio, 2.0) * G; + for (var i = 0; i < NUM_PLANETS; ++i) { + var properties = planet_properties[i]; + properties.period = ratio * properties.period; + properties.velocity = Vec3.multiply(ratio, properties.velocity); + + } + last_alpha = alpha; +} + + +// Clean up models, UI panels, lines, and button overlays +function scriptEnding() { + + satelliteGame.endGame(); + + Entities.deleteEntity(theSun); + for (var i = 0; i < NUM_PLANETS; ++i) { + Entities.deleteEntity(planets[i]); + } + Menu.removeMenu("Developer > Scene"); + panel.destroy(); + Overlays.deleteOverlay(pauseButton); + + var e = Entities.findEntities(MyAvatar.position, 16000); + for (i = 0; i < e.length; i++) { + var props = Entities.getEntityProperties(e[i]); + if (props.type === "Line" || props.type === "Text") { + Entities.deleteEntity(e[i]); + } + } +}; + + +Controller.mouseMoveEvent.connect(function panelMouseMoveEvent(event) { + return panel.mouseMoveEvent(event); +}); +Controller.mousePressEvent.connect(function panelMousePressEvent(event) { + return panel.mousePressEvent(event); +}); +Controller.mouseDoublePressEvent.connect(function panelMouseDoublePressEvent(event) { + return panel.mouseDoublePressEvent(event); +}); +Controller.mouseReleaseEvent.connect(function (event) { + return panel.mouseReleaseEvent(event); +}); +Controller.mousePressEvent.connect(mousePressEvent); +Controller.mouseDoublePressEvent.connect(mouseDoublePressEvent); +Controller.keyPressEvent.connect(keyPressEvent); + +Script.scriptEnding.connect(scriptEnding); +Script.update.connect(update); \ No newline at end of file diff --git a/examples/utilities/tools/vector.js b/examples/utilities/tools/vector.js new file mode 100644 index 0000000000..ab2e8cf200 --- /dev/null +++ b/examples/utilities/tools/vector.js @@ -0,0 +1,197 @@ +// +// vector.js +// examples +// +// Created by Bridget Went on 7/1/15. +// Copyright 2015 High Fidelity, Inc. +// +// A template for creating vector arrows using line entities. A VectorArrow object creates a +// draggable vector arrow where the user clicked at a specified distance from the viewer. +// The relative magnitude and direction of the vector may be displayed. +// +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// + +var LINE_DIMENSIONS = 100; +var LIFETIME = 6000; +var RAD_TO_DEG = 180.0 / Math.PI; + +var LINE_WIDTH = 4; +var ARROW_WIDTH = 6; +var line, linePosition; +var arrow1, arrow2; + +var SCALE = 0.15; +var ANGLE = 150.0; + + +VectorArrow = function(distance, showStats, statsTitle, statsPosition) { + this.magnitude = 0; + this.direction = {x: 0, y: 0, z: 0}; + + this.showStats = showStats; + this.isDragging = false; + + this.newLine = function(position) { + linePosition = position; + var points = []; + + line = Entities.addEntity({ + position: linePosition, + type: "Line", + color: {red: 255, green: 255, blue: 255}, + dimensions: { + x: LINE_DIMENSIONS, + y: LINE_DIMENSIONS, + z: LINE_DIMENSIONS + }, + lineWidth: LINE_WIDTH, + lifetime: LIFETIME, + linePoints: [] + }); + + arrow1 = Entities.addEntity({ + position: {x: 0, y: 0, z: 0}, + type: "Line", + dimensions: { + x: LINE_DIMENSIONS, + y: LINE_DIMENSIONS, + z: LINE_DIMENSIONS + }, + color: {red: 255, green: 255, blue: 255}, + lineWidth: ARROW_WIDTH, + linePoints: [], + }); + + arrow2 = Entities.addEntity({ + position: {x: 0, y: 0, z: 0}, + type: "Line", + dimensions: { + x: LINE_DIMENSIONS, + y: LINE_DIMENSIONS, + z: LINE_DIMENSIONS + }, + color: {red: 255, green: 255, blue: 255}, + lineWidth: ARROW_WIDTH, + linePoints: [], + }); + + } + + + this.onMousePressEvent = function(event) { + + this.newLine(computeWorldPoint(event)); + + if (this.showStats) { + this.label = Entities.addEntity({ + type: "Text", + position: statsPosition, + dimensions: { + x: 4.0, + y: 1.5, + z: 0.1 + }, + lineHeight: 0.3, + faceCamera: true + }); + } + + this.isDragging = true; + + } + + + this.onMouseMoveEvent = function(event) { + + if (!this.isDragging) { + return; + } + + var worldPoint = computeWorldPoint(event); + var localPoint = computeLocalPoint(event, linePosition); + points = [{x: 0, y: 0, z: 0}, localPoint]; + Entities.editEntity(line, { linePoints: points }); + + var nextOffset = Vec3.multiply(SCALE, localPoint); + var normOffset = Vec3.normalize(localPoint); + var axis = Vec3.cross(normOffset, Quat.getFront(Camera.getOrientation()) ); + axis = Vec3.cross(axis, normOffset); + var rotate1 = Quat.angleAxis(ANGLE, axis); + var rotate2 = Quat.angleAxis(-ANGLE, axis); + + // Rotate arrow head to follow direction of the line + Entities.editEntity(arrow1, { + visible: true, + position: worldPoint, + linePoints: [{x: 0, y: 0, z: 0}, nextOffset], + rotation: rotate1 + }); + Entities.editEntity(arrow2, { + visible: true, + position: worldPoint, + linePoints: [{x: 0, y: 0, z: 0}, nextOffset], + rotation: rotate2 + }); + + this.magnitude = Vec3.length(localPoint) / 10.0; + this.direction = Vec3.normalize(Vec3.subtract(worldPoint, linePosition)); + + if (this.showStats) { + this.editLabel(statsTitle + " Magnitude " + this.magnitude.toFixed(2) + ", Direction: " + + this.direction.x.toFixed(2) + ", " + this.direction.y.toFixed(2) + ", " + this.direction.z.toFixed(2)); + } + } + + this.onMouseReleaseEvent = function() { + this.isDragging = false; + } + + this.cleanup = function() { + Entities.deleteEntity(line); + Entities.deleteEntity(arrow1); + Entities.deleteEntity(arrow2); + } + + this.deleteLabel = function() { + Entities.deleteEntity(this.label); + } + + this.editLabel = function(str) { + if(!this.showStats) { + return; + } + Entities.editEntity(this.label, { + text: str + }); + } + + function computeWorldPoint(event) { + var pickRay = Camera.computePickRay(event.x, event.y); + var addVector = Vec3.multiply(pickRay.direction, distance); + return Vec3.sum(Camera.getPosition(), addVector); + } + + function computeLocalPoint(event, linePosition) { + var localPoint = Vec3.subtract(computeWorldPoint(event), linePosition); + return localPoint; + } + + + +} + + + + + + + + + + + + From e0d6609a99b198f84a7a54445b2f9abb6465f047 Mon Sep 17 00:00:00 2001 From: bwent Date: Fri, 24 Jul 2015 10:18:33 -0700 Subject: [PATCH 132/242] resolve file path issue --- examples/example/games/satellite.js | 13 ++++++------- examples/example/{games => }/solarsystem.js | 12 +++++++++--- 2 files changed, 15 insertions(+), 10 deletions(-) rename examples/example/{games => }/solarsystem.js (96%) diff --git a/examples/example/games/satellite.js b/examples/example/games/satellite.js index d2e4dd2a99..e82285622b 100644 --- a/examples/example/games/satellite.js +++ b/examples/example/games/satellite.js @@ -15,7 +15,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -Script.include('../../utilities/tools/vector.js'); +Script.include('../utilities/tools/vector.js'); var URL = "https://s3.amazonaws.com/hifi-public/marketplace/hificontent/Scripts/planets/"; @@ -38,8 +38,8 @@ SatelliteGame = function() { var EARTH_SIZE = 20.0; var CLOUDS_OFFSET = 0.5; var SPIN = 0.1; - var ZONE_DIM = 10.0; - var LIGHT_INTENSITY = 1.2; + var ZONE_DIM = 100.0; + var LIGHT_INTENSITY = 1.5; Earth = function(position, size) { this.earth = Entities.addEntity({ @@ -65,7 +65,7 @@ SatelliteGame = function() { this.clouds = Entities.addEntity({ type: "Model", shapeType: 'sphere', - modelURL: URL + "clouds.fbx?", + modelURL: URL + "clouds.fbx?i=2", position: position, dimensions: { x: size + CLOUDS_OFFSET, @@ -190,7 +190,7 @@ SatelliteGame = function() { if (activeSatellite.arrow) { activeSatellite.arrow.deleteLabel(); } - var statsPosition = Vec3.sum(Camera.getPosition(), Vec3.multiply(MAX_RANGE * 0.5, Quat.getFront(Camera.getOrientation()))); + var statsPosition = Vec3.sum(Camera.getPosition(), Vec3.multiply(MAX_RANGE * 0.4, Quat.getFront(Camera.getOrientation()))); var pickRay = Camera.computePickRay(event.x, event.y) var rayPickResult = Entities.findRayIntersection(pickRay, true); if (rayPickResult.entityID === activeSatellite.satellite) { @@ -245,9 +245,8 @@ SatelliteGame = function() { } this.endGame = function() { - print("ending game"); for(var i = 0; i < satellites.length; i++) { - Entities.deleteEntitiy(satellites[i].satellite); + Entities.deleteEntity(satellites[i].satellite); satellites[i].arrow.cleanup(); } earth.cleanup(); diff --git a/examples/example/games/solarsystem.js b/examples/example/solarsystem.js similarity index 96% rename from examples/example/games/solarsystem.js rename to examples/example/solarsystem.js index b651e551f0..09ba2eec8d 100644 --- a/examples/example/games/solarsystem.js +++ b/examples/example/solarsystem.js @@ -6,14 +6,20 @@ // Copyright 2015 High Fidelity, Inc. // // The start to a project to build a virtual physics classroom to simulate the solar system, gravity, and orbital physics. -// A sun with oribiting planets is created in front of the user. UI elements allow for adjusting the period, gravity, trails, and energy recalculations. +// - A sun with oribiting planets is created in front of the user +// - UI elements allow for adjusting the period, gravity, trails, and energy recalculations +// - Click "PAUSE" to pause the animation and show planet labels +// - In this mode, double-click a planet label to zoom in on that planet +// -Double-clicking on earth label initiates satellite orbiter game +// -Press "TAB" to toggle back to solar system view +// // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -Script.include('../../utilities/tools/cookies.js'); -Script.include('satellite.js'); +Script.include('../utilities/tools/cookies.js'); +Script.include('games/satellite.js'); var BASE_URL = "https://s3.amazonaws.com/hifi-public/marketplace/hificontent/Scripts/planets/planets/"; From 5272a1d6e74a23b310b765ef51242a9ea81e0cc7 Mon Sep 17 00:00:00 2001 From: bwent Date: Mon, 27 Jul 2015 14:28:43 -0700 Subject: [PATCH 133/242] refactoring variables and constants, fix update loop to continue over unlaunched satellites --- examples/example/games/satellite.js | 212 +++++++++++++------------ examples/example/solarsystem.js | 229 ++++++++++++++++++++-------- examples/utilities/tools/vector.js | 31 ++-- 3 files changed, 290 insertions(+), 182 deletions(-) diff --git a/examples/example/games/satellite.js b/examples/example/games/satellite.js index e82285622b..3ddce96aa5 100644 --- a/examples/example/games/satellite.js +++ b/examples/example/games/satellite.js @@ -27,11 +27,6 @@ SatelliteGame = function() { z: 0 } var LIFETIME = 6000; - - var v0; - var T = 4.0; - var M = 16000.0; - var m = M * 0.000000333; var ERROR_THRESH = 20.0; // Create the spinning earth model @@ -43,42 +38,54 @@ SatelliteGame = function() { Earth = function(position, size) { this.earth = Entities.addEntity({ - type: "Model", - shapeType: 'sphere', - modelURL: URL + "earth.fbx", - position: position, - dimensions: { - x: size, - y: size, - z: size - }, - rotation: Quat.angleAxis(180, {x: 1, y: 0, z: 0}), - angularVelocity: { x: 0.00, y: 0.5 * SPIN, z: 0.00 }, - angularDamping: 0.0, - damping: 0.0, - ignoreCollisions: false, - lifetime: 6000, - collisionsWillMove: false, - visible: true + type: "Model", + shapeType: 'sphere', + modelURL: URL + "earth.fbx", + position: position, + dimensions: { + x: size, + y: size, + z: size + }, + rotation: Quat.angleAxis(180, { + x: 1, + y: 0, + z: 0 + }), + angularVelocity: { + x: 0.00, + y: 0.5 * SPIN, + z: 0.00 + }, + angularDamping: 0.0, + damping: 0.0, + ignoreCollisions: false, + lifetime: 6000, + collisionsWillMove: false, + visible: true }); this.clouds = Entities.addEntity({ - type: "Model", - shapeType: 'sphere', - modelURL: URL + "clouds.fbx?i=2", - position: position, - dimensions: { - x: size + CLOUDS_OFFSET, - y: size + CLOUDS_OFFSET, - z: size + CLOUDS_OFFSET - }, - angularVelocity: { x: 0.00, y: SPIN, z: 0.00 }, - angularDamping: 0.0, - damping: 0.0, - ignoreCollisions: false, - lifetime: LIFETIME, - collisionsWillMove: false, - visible: true + type: "Model", + shapeType: 'sphere', + modelURL: URL + "clouds.fbx?i=2", + position: position, + dimensions: { + x: size + CLOUDS_OFFSET, + y: size + CLOUDS_OFFSET, + z: size + CLOUDS_OFFSET + }, + angularVelocity: { + x: 0.00, + y: SPIN, + z: 0.00 + }, + angularDamping: 0.0, + damping: 0.0, + ignoreCollisions: false, + lifetime: LIFETIME, + collisionsWillMove: false, + visible: true }); this.zone = Entities.addEntity({ @@ -93,11 +100,11 @@ SatelliteGame = function() { keyLightIntensity: LIGHT_INTENSITY }); - this.cleanup = function() { - Entities.deleteEntity(this.clouds); + this.cleanup = function() { + Entities.deleteEntity(this.clouds); Entities.deleteEntity(this.earth); Entities.deleteEntity(this.zone); - } + } } // Create earth model @@ -110,6 +117,10 @@ SatelliteGame = function() { var launched = false; var activeSatellite; + var PERIOD = 4.0; + var LARGE_BODY_MASS = 16000.0; + var SMALL_BODY_MASS = LARGE_BODY_MASS * 0.000000333; + Satellite = function(position, planetCenter) { // The Satellite class @@ -118,20 +129,20 @@ SatelliteGame = function() { this.readyToLaunch = false; this.radius = Vec3.length(Vec3.subtract(position, planetCenter)); - this.satellite = Entities.addEntity({ + this.satellite = Entities.addEntity({ type: "Model", - modelURL: "https://s3.amazonaws.com/hifi-public/marketplace/hificontent/Scripts/planets/satellite/satellite.fbx", - position: this.startPosition, - dimensions: { - x: SATELLITE_SIZE, - y: SATELLITE_SIZE, - z: SATELLITE_SIZE - }, - angularDamping: 0.0, - damping: 0.0, - ignoreCollisions: false, - lifetime: LIFETIME, - collisionsWillMove: false, + modelURL: "https://s3.amazonaws.com/hifi-public/marketplace/hificontent/Scripts/planets/satellite/satellite.fbx", + position: this.startPosition, + dimensions: { + x: SATELLITE_SIZE, + y: SATELLITE_SIZE, + z: SATELLITE_SIZE + }, + angularDamping: 0.0, + damping: 0.0, + ignoreCollisions: false, + lifetime: LIFETIME, + collisionsWillMove: false, }); this.getProperties = function() { @@ -142,14 +153,16 @@ SatelliteGame = function() { var prop = Entities.getEntityProperties(this.satellite); var between = Vec3.subtract(planetCenter, prop.position); var radius = Vec3.length(between); - this.G = (4.0 * Math.PI * Math.PI * Math.pow(radius, 3.0)) / (M * T * T); + this.gravity = (4.0 * Math.PI * Math.PI * Math.pow(radius, 3.0)) / (LARGE_BODY_MASS * PERIOD * PERIOD); - var v0 = Vec3.normalize(Vec3.cross(between, Y_AXIS)); - v0 = Vec3.multiply(Math.sqrt((this.G * M) / radius), v0); - v0 = Vec3.multiply(this.arrow.magnitude, v0); - v0 = Vec3.multiply(Vec3.length(v0), this.arrow.direction); + var initialVelocity = Vec3.normalize(Vec3.cross(between, Y_AXIS)); + initialVelocity = Vec3.multiply(Math.sqrt((this.gravity * LARGE_BODY_MASS) / radius), initialVelocity); + initialVelocity = Vec3.multiply(this.arrow.magnitude, initialVelocity); + initialVelocity = Vec3.multiply(Vec3.length(initialVelocity), this.arrow.direction); - Entities.editEntity(this.satellite, { velocity: v0 }); + Entities.editEntity(this.satellite, { + velocity: initialVelocity + }); this.launched = true; }; @@ -158,12 +171,12 @@ SatelliteGame = function() { var prop = Entities.getEntityProperties(this.satellite); var between = Vec3.subtract(prop.position, planetCenter); var radius = Vec3.length(between); - var a = -(this.G * M) * Math.pow(radius, (-2.0)); - var speed = a * deltaTime; - var vel = Vec3.multiply(speed, Vec3.normalize(between)); + var acceleration = -(this.gravity * LARGE_BODY_MASS) * Math.pow(radius, (-2.0)); + var speed = acceleration * deltaTime; + var vel = Vec3.multiply(speed, Vec3.normalize(between)); - var newVelocity = Vec3.sum(prop.velocity, vel); - var newPos = Vec3.sum(prop.position, Vec3.multiply(newVelocity, deltaTime)); + var newVelocity = Vec3.sum(prop.velocity, vel); + var newPos = Vec3.sum(prop.position, Vec3.multiply(newVelocity, deltaTime)); Entities.editEntity(this.satellite, { velocity: newVelocity, @@ -173,11 +186,11 @@ SatelliteGame = function() { } function mouseDoublePressEvent(event) { - var pickRay = Camera.computePickRay(event.x, event.y); - var addVector = Vec3.multiply(pickRay.direction, distance); - var point = Vec3.sum(Camera.getPosition(), addVector); + var pickRay = Camera.computePickRay(event.x, event.y); + var addVector = Vec3.multiply(pickRay.direction, distance); + var point = Vec3.sum(Camera.getPosition(), addVector); - // Create a new satellite + // Create a new satellite activeSatellite = new Satellite(point, center); satellites.push(activeSatellite); } @@ -188,11 +201,11 @@ SatelliteGame = function() { } // Reset label if (activeSatellite.arrow) { - activeSatellite.arrow.deleteLabel(); - } + activeSatellite.arrow.deleteLabel(); + } var statsPosition = Vec3.sum(Camera.getPosition(), Vec3.multiply(MAX_RANGE * 0.4, Quat.getFront(Camera.getOrientation()))); var pickRay = Camera.computePickRay(event.x, event.y) - var rayPickResult = Entities.findRayIntersection(pickRay, true); + var rayPickResult = Entities.findRayIntersection(pickRay, true); if (rayPickResult.entityID === activeSatellite.satellite) { // Create a draggable vector arrow at satellite position activeSatellite.arrow = new VectorArrow(distance, true, "INITIAL VELOCITY", statsPosition); @@ -202,50 +215,50 @@ SatelliteGame = function() { } function mouseMoveEvent(event) { - if(!activeSatellite || !activeSatellite.arrow || !activeSatellite.arrow.isDragging) { + if (!activeSatellite || !activeSatellite.arrow || !activeSatellite.arrow.isDragging) { return; } activeSatellite.arrow.onMouseMoveEvent(event); } function mouseReleaseEvent(event) { - if(!activeSatellite || !activeSatellite.arrow || !activeSatellite.arrow.isDragging) { + if (!activeSatellite || !activeSatellite.arrow || !activeSatellite.arrow.isDragging) { return; } activeSatellite.arrow.onMouseReleaseEvent(event); - activeSatellite.launch(); - activeSatellite.arrow.cleanup(); + activeSatellite.launch(); + activeSatellite.arrow.cleanup(); } var counter = 0.0; - var TIME = 500; + var CHECK_ENERGY_PERIOD = 500; function update(deltaTime) { - if(!activeSatellite) { + if (!activeSatellite) { return; } // Update all satellites for (var i = 0; i < satellites.length; i++) { if (!satellites[i].launched) { - return; + continue; } - satellites[i].update(deltaTime); + satellites[i].update(deltaTime); } - + counter++; - if (counter % TIME == 0) { + if (counter % CHECK_ENERGY_PERIOD == 0) { var prop = activeSatellite.getProperties(); - var error = calcEnergyError(prop.position, Vec3.length(prop.velocity)); - if (Math.abs(error) <= ERROR_THRESH) { - activeSatellite.arrow.editLabel("Nice job! The satellite has reached a stable orbit."); - } else { - activeSatellite.arrow.editLabel("Try again! The satellite is in an unstable orbit."); - } - } + var error = calcEnergyError(prop.position, Vec3.length(prop.velocity)); + if (Math.abs(error) <= ERROR_THRESH) { + activeSatellite.arrow.editLabel("Nice job! The satellite has reached a stable orbit."); + } else { + activeSatellite.arrow.editLabel("Try again! The satellite is in an unstable orbit."); + } + } } this.endGame = function() { - for(var i = 0; i < satellites.length; i++) { + for (var i = 0; i < satellites.length; i++) { Entities.deleteEntity(satellites[i].satellite); satellites[i].arrow.cleanup(); } @@ -256,13 +269,13 @@ SatelliteGame = function() { function calcEnergyError(pos, vel) { //Calculate total energy error for active satellite's orbital motion var radius = activeSatellite.radius; - var G = (4.0 * Math.PI * Math.PI * Math.pow(radius, 3.0)) / (M * T * T); - var v0 = Math.sqrt((G * M) / radius); - - var totalEnergy = 0.5 * M * Math.pow(v0, 2.0) - ((G * M * m) / radius); - var measuredEnergy = 0.5 * M * Math.pow(vel, 2.0) - ((G * M * m) / Vec3.length(Vec3.subtract(pos, center))); - var error = ((measuredEnergy - totalEnergy) / totalEnergy) * 100; - return error; + var gravity = (4.0 * Math.PI * Math.PI * Math.pow(radius, 3.0)) / (LARGE_BODY_MASS * PERIOD * PERIOD); + var initialVelocityCalculated = Math.sqrt((gravity * LARGE_BODY_MASS) / radius); + + var totalEnergy = 0.5 * LARGE_BODY_MASS * Math.pow(initialVelocityCalculated, 2.0) - ((gravity * LARGE_BODY_MASS * SMALL_BODY_MASS) / radius); + var measuredEnergy = 0.5 * LARGE_BODY_MASS * Math.pow(vel, 2.0) - ((gravity * LARGE_BODY_MASS * SMALL_BODY_MASS) / Vec3.length(Vec3.subtract(pos, center))); + var error = ((measuredEnergy - totalEnergy) / totalEnergy) * 100; + return error; } Controller.mousePressEvent.connect(mousePressEvent); @@ -272,7 +285,4 @@ SatelliteGame = function() { Script.update.connect(update); Script.scriptEnding.connect(this.endGame); -} - - - +} \ No newline at end of file diff --git a/examples/example/solarsystem.js b/examples/example/solarsystem.js index 09ba2eec8d..8d1f0c81e3 100644 --- a/examples/example/solarsystem.js +++ b/examples/example/solarsystem.js @@ -52,9 +52,7 @@ var BOUNDS = 200; // Alert user to move if they are too close to domain bounds -if (MyAvatar.position.x < BOUNDS || MyAvatar.position.x > TREE_SCALE - BOUNDS - || MyAvatar.position.y < BOUNDS || MyAvatar.position.y > TREE_SCALE - BOUNDS - || MyAvatar.position.z < BOUNDS || MyAvatar.position.z > TREE_SCALE - BOUNDS) { +if (MyAvatar.position.x < BOUNDS || MyAvatar.position.x > TREE_SCALE - BOUNDS || MyAvatar.position.y < BOUNDS || MyAvatar.position.y > TREE_SCALE - BOUNDS || MyAvatar.position.z < BOUNDS || MyAvatar.position.z > TREE_SCALE - BOUNDS) { Window.alert("Please move at least 200m away from domain bounds."); return; } @@ -106,30 +104,70 @@ function initPlanets() { var T = (2.0 * Math.PI) * Math.sqrt(Math.pow(radius, 3.0) / (G * M)); if (i == 0) { - var color = {red: 255, green: 255, blue: 255}; + var color = { + red: 255, + green: 255, + blue: 255 + }; } else if (i == 1) { - var color = {red: 255, green: 160, blue: 110}; + var color = { + red: 255, + green: 160, + blue: 110 + }; } else if (i == 2) { - var color = {red: 10, green: 150, blue: 160}; + var color = { + red: 10, + green: 150, + blue: 160 + }; } else if (i == 3) { - var color = {red: 180, green: 70, blue: 10}; + var color = { + red: 180, + green: 70, + blue: 10 + }; } else if (i == 4) { - var color = {red: 250, green: 140, blue: 0}; + var color = { + red: 250, + green: 140, + blue: 0 + }; } else if (i == 5) { - var color = {red: 235, green: 215, blue: 0}; + var color = { + red: 235, + green: 215, + blue: 0 + }; } else if (i == 6) { - var color = {red:135, green: 205, blue: 240}; + var color = { + red: 135, + green: 205, + blue: 240 + }; } else if (i == 7) { - var color = {red:30, green: 140, blue: 255}; + var color = { + red: 30, + green: 140, + blue: 255 + }; } var prop = { radius: radius, - position: Vec3.sum(center, {x: radius, y: 0.0, z: 0.0}), + position: Vec3.sum(center, { + x: radius, + y: 0.0, + z: 0.0 + }), lineColor: color, period: T, dimensions: size, - velocity: Vec3.multiply(v0, Vec3.normalize({x: 0, y: -0.2, z: 0.9})) + velocity: Vec3.multiply(v0, Vec3.normalize({ + x: 0, + y: -0.2, + z: 0.9 + })) }; planet_properties.push(prop); @@ -194,27 +232,55 @@ function showLabels() { text = text + " Speed: " + Vec3.length(properties.velocity).toFixed(2); - var labelPos = Vec3.sum(planet_properties[i].position, {x: 0.0, y: LABEL_DIST, z: LABEL_DIST}); + var labelPos = Vec3.sum(planet_properties[i].position, { + x: 0.0, + y: LABEL_DIST, + z: LABEL_DIST + }); var linePos = planet_properties[i].position; - labelLines.push(Entities.addEntity( { + labelLines.push(Entities.addEntity({ type: "Line", position: linePos, - dimensions: {x: 20, y: 20, z: 20}, + dimensions: { + x: 20, + y: 20, + z: 20 + }, lineWidth: 3.0, - color: {red: 255, green: 255, blue: 255}, - linePoints: [{x: 0, y: 0, z: 0}, computeLocalPoint(linePos, labelPos)] + color: { + red: 255, + green: 255, + blue: 255 + }, + linePoints: [{ + x: 0, + y: 0, + z: 0 + }, computeLocalPoint(linePos, labelPos)] })); - labels.push(Entities.addEntity( { + labels.push(Entities.addEntity({ type: "Text", text: text, lineHeight: TEXT_HEIGHT, - dimensions: {x: LABEL_X, y: LABEL_Y, z: LABEL_Z}, + dimensions: { + x: LABEL_X, + y: LABEL_Y, + z: LABEL_Z + }, position: labelPos, - backgroundColor: {red: 10, green: 10, blue: 10}, - textColor: {red: 255, green: 255, blue: 255}, + backgroundColor: { + red: 10, + green: 10, + blue: 10 + }, + textColor: { + red: 255, + green: 255, + blue: 255 + }, faceCamera: true - })); + })); } } @@ -255,7 +321,7 @@ function update(deltaTime) { velocity: properties.velocity, position: properties.position }); - + // Create new or update current trail if (trailsEnabled) { @@ -285,11 +351,11 @@ function update(deltaTime) { } } } - + counter++; if (time % TIME_STEP == 0) { elapsed++; - } + } } function computeLocalPoint(linePos, worldPoint) { @@ -306,7 +372,7 @@ function getAcceleration(radius) { function resetTrails(planetIndex) { elapsed = 0.0; var properties = planet_properties[planetIndex]; - + var trail = []; var lineStack = []; @@ -333,10 +399,14 @@ function newLine(lineStack, point, period, color) { lineStack.push(line); } else { // Begin overwriting first lines after one full revolution (one period) - var firstLine = lineStack.shift(); + var firstLine = lineStack.shift(); Entities.editEntity(firstLine, { position: point, - linePoints: [{x: 0.0, y: 0.0, z: 0.0}] + linePoints: [{ + x: 0.0, + y: 0.0, + z: 0.0 + }] }); lineStack.push(firstLine); @@ -357,6 +427,7 @@ function calcEnergyError(planet, radius, v0, v, pos) { var error = ((measuredEnergy - totalEnergy) / totalEnergy) * 100; return error; } + function adjustVelocity(planet, pos) { var measuredPE = -(G * M * m) / Vec3.length(Vec3.subtract(center, pos)); return Math.sqrt(2 * (totalEnergy - measuredPE) / M); @@ -365,7 +436,11 @@ function adjustVelocity(planet, pos) { // Allow user to toggle pausing the model, switch to planet view var pauseButton = Overlays.addOverlay("text", { - backgroundColor: { red: 200, green: 200, blue: 255 }, + backgroundColor: { + red: 200, + green: 200, + blue: 255 + }, text: "Pause", x: PANEL_X, y: PANEL_Y - 30, @@ -379,22 +454,45 @@ var pauseButton = Overlays.addOverlay("text", { var paused = false; function mousePressEvent(event) { - var clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); - if(clickedOverlay == pauseButton) { + var clickedOverlay = Overlays.getOverlayAtPoint({ + x: event.x, + y: event.y + }); + if (clickedOverlay == pauseButton) { paused = !paused; for (var i = 0; i < NUM_PLANETS; ++i) { - Entities.editEntity(planets[i], { velocity: {x: 0.0, y: 0.0, z: 0.0} }); + Entities.editEntity(planets[i], { + velocity: { + x: 0.0, + y: 0.0, + z: 0.0 + } + }); } if (paused && !labelsShowing) { - Overlays.editOverlay(pauseButton, { text: "Paused", backgroundColor: {red: 255, green: 50, blue: 50} } ); + Overlays.editOverlay(pauseButton, { + text: "Paused", + backgroundColor: { + red: 255, + green: 50, + blue: 50 + } + }); showLabels(); } if (paused == false && labelsShowing) { - Overlays.editOverlay(pauseButton, { text: "Pause", backgroundColor: {red: 200, green: 200, blue: 255}}); + Overlays.editOverlay(pauseButton, { + text: "Pause", + backgroundColor: { + red: 200, + green: 200, + blue: 255 + } + }); hideLabels(); } planetView = false; - } + } } function keyPressEvent(event) { @@ -409,7 +507,7 @@ function keyPressEvent(event) { } function mouseDoublePressEvent(event) { - if(earthView) { + if (earthView) { return; } var pickRay = Camera.computePickRay(event.x, event.y) @@ -419,57 +517,68 @@ function mouseDoublePressEvent(event) { if (rayPickResult.entityID === labels[i]) { planetView = true; if (i == 2) { - MyAvatar.position = Vec3.sum(center, {x: 200, y: 200, z: 200}); - Camera.setPosition(Vec3.sum(center, {x: 200, y: 200, z: 200})); + MyAvatar.position = Vec3.sum(center, { + x: 200, + y: 200, + z: 200 + }); + Camera.setPosition(Vec3.sum(center, { + x: 200, + y: 200, + z: 200 + })); earthView = true; satelliteGame = new SatelliteGame(); - + } else { - MyAvatar.position = Vec3.sum({x: 0.0, y: 0.0, z: 3.0}, planet_properties[i].position); + MyAvatar.position = Vec3.sum({ + x: 0.0, + y: 0.0, + z: 3.0 + }, planet_properties[i].position); Camera.lookAt(planet_properties[i].position); } - break; - } - } + break; + } + } } - // Create UI panel var panel = new Panel(PANEL_X, PANEL_Y); var panelItems = []; var g_multiplier = 1.0; panelItems.push(panel.newSlider("Adjust Gravitational Force: ", 0.1, 5.0, - function (value) { + function(value) { g_multiplier = value; G = G_ref * g_multiplier; }, - function () { + function() { return g_multiplier; }, - function (value) { + function(value) { return value.toFixed(1) + "x"; })); var period_multiplier = 1.0; var last_alpha = period_multiplier; panelItems.push(panel.newSlider("Adjust Orbital Period: ", 0.1, 3.0, - function (value) { + function(value) { period_multiplier = value; changePeriod(period_multiplier); }, - function () { + function() { return period_multiplier; }, - function (value) { + function(value) { return (value).toFixed(2) + "x"; })); panelItems.push(panel.newCheckbox("Leave Trails: ", - function (value) { + function(value) { trailsEnabled = value; if (trailsEnabled) { for (var i = 0; i < NUM_PLANETS; ++i) { @@ -485,21 +594,21 @@ panelItems.push(panel.newCheckbox("Leave Trails: ", } } }, - function () { + function() { return trailsEnabled; }, - function (value) { + function(value) { return value; })); panelItems.push(panel.newCheckbox("Energy Error Calculations: ", - function (value) { + function(value) { energyConserved = value; }, - function () { + function() { return energyConserved; }, - function (value) { + function(value) { return value; })); @@ -519,7 +628,7 @@ function changePeriod(alpha) { // Clean up models, UI panels, lines, and button overlays function scriptEnding() { - + satelliteGame.endGame(); Entities.deleteEntity(theSun); @@ -536,7 +645,7 @@ function scriptEnding() { if (props.type === "Line" || props.type === "Text") { Entities.deleteEntity(e[i]); } - } + } }; @@ -549,7 +658,7 @@ Controller.mousePressEvent.connect(function panelMousePressEvent(event) { Controller.mouseDoublePressEvent.connect(function panelMouseDoublePressEvent(event) { return panel.mouseDoublePressEvent(event); }); -Controller.mouseReleaseEvent.connect(function (event) { +Controller.mouseReleaseEvent.connect(function(event) { return panel.mouseReleaseEvent(event); }); Controller.mousePressEvent.connect(mousePressEvent); diff --git a/examples/utilities/tools/vector.js b/examples/utilities/tools/vector.js index ab2e8cf200..0635b6cbc7 100644 --- a/examples/utilities/tools/vector.js +++ b/examples/utilities/tools/vector.js @@ -82,6 +82,13 @@ VectorArrow = function(distance, showStats, statsTitle, statsPosition) { } + var STATS_DIMENSIONS = { + x: 4.0, + y: 1.5, + z: 0.1 + }; + var TEXT_HEIGHT = 0.3; + this.onMousePressEvent = function(event) { this.newLine(computeWorldPoint(event)); @@ -90,12 +97,8 @@ VectorArrow = function(distance, showStats, statsTitle, statsPosition) { this.label = Entities.addEntity({ type: "Text", position: statsPosition, - dimensions: { - x: 4.0, - y: 1.5, - z: 0.1 - }, - lineHeight: 0.3, + dimensions: STATS_DIMENSIONS, + lineHeight: TEXT_HEIGHT, faceCamera: true }); } @@ -137,7 +140,7 @@ VectorArrow = function(distance, showStats, statsTitle, statsPosition) { rotation: rotate2 }); - this.magnitude = Vec3.length(localPoint) / 10.0; + this.magnitude = Vec3.length(localPoint) * 0.1; this.direction = Vec3.normalize(Vec3.subtract(worldPoint, linePosition)); if (this.showStats) { @@ -180,18 +183,4 @@ VectorArrow = function(distance, showStats, statsTitle, statsPosition) { return localPoint; } - - } - - - - - - - - - - - - From 1107742188a8cd878a65c2983f68d458648d0598 Mon Sep 17 00:00:00 2001 From: bwent Date: Tue, 28 Jul 2015 12:39:04 -0700 Subject: [PATCH 134/242] Clean up formatting for satellite.js --- examples/example/games/satellite.js | 468 ++++++++++++++-------------- 1 file changed, 234 insertions(+), 234 deletions(-) diff --git a/examples/example/games/satellite.js b/examples/example/games/satellite.js index 3ddce96aa5..f362c0c1e4 100644 --- a/examples/example/games/satellite.js +++ b/examples/example/games/satellite.js @@ -3,13 +3,13 @@ // games // // Created by Bridget Went 7/1/2015. -// Copyright 2015 High Fidelity, Inc. +// Copyright 2015 High Fidelity, Inc. // // A game to bring a satellite model into orbit around an animated earth model . -// - Double click to create a new satellite -// - Click on the satellite, drag a vector arrow to specify initial velocity -// - Release mouse to launch the active satellite -// - Orbital movement is calculated using equations of gravitational physics +// - Double click to create a new satellite +// - Click on the satellite, drag a vector arrow to specify initial velocity +// - Release mouse to launch the active satellite +// - Orbital movement is calculated using equations of gravitational physics // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -20,269 +20,269 @@ Script.include('../utilities/tools/vector.js'); var URL = "https://s3.amazonaws.com/hifi-public/marketplace/hificontent/Scripts/planets/"; SatelliteGame = function() { - var MAX_RANGE = 50.0; - var Y_AXIS = { - x: 0, - y: 1, - z: 0 - } - var LIFETIME = 6000; - var ERROR_THRESH = 20.0; + var MAX_RANGE = 50.0; + var Y_AXIS = { + x: 0, + y: 1, + z: 0 + } + var LIFETIME = 6000; + var ERROR_THRESH = 20.0; - // Create the spinning earth model - var EARTH_SIZE = 20.0; - var CLOUDS_OFFSET = 0.5; - var SPIN = 0.1; - var ZONE_DIM = 100.0; - var LIGHT_INTENSITY = 1.5; + // Create the spinning earth model + var EARTH_SIZE = 20.0; + var CLOUDS_OFFSET = 0.5; + var SPIN = 0.1; + var ZONE_DIM = 100.0; + var LIGHT_INTENSITY = 1.5; - Earth = function(position, size) { - this.earth = Entities.addEntity({ - type: "Model", - shapeType: 'sphere', - modelURL: URL + "earth.fbx", - position: position, - dimensions: { - x: size, - y: size, - z: size - }, - rotation: Quat.angleAxis(180, { - x: 1, - y: 0, - z: 0 - }), - angularVelocity: { - x: 0.00, - y: 0.5 * SPIN, - z: 0.00 - }, - angularDamping: 0.0, - damping: 0.0, - ignoreCollisions: false, - lifetime: 6000, - collisionsWillMove: false, - visible: true - }); + Earth = function(position, size) { + this.earth = Entities.addEntity({ + type: "Model", + shapeType: 'sphere', + modelURL: URL + "earth.fbx", + position: position, + dimensions: { + x: size, + y: size, + z: size + }, + rotation: Quat.angleAxis(180, { + x: 1, + y: 0, + z: 0 + }), + angularVelocity: { + x: 0.00, + y: 0.5 * SPIN, + z: 0.00 + }, + angularDamping: 0.0, + damping: 0.0, + ignoreCollisions: false, + lifetime: 6000, + collisionsWillMove: false, + visible: true + }); - this.clouds = Entities.addEntity({ - type: "Model", - shapeType: 'sphere', - modelURL: URL + "clouds.fbx?i=2", - position: position, - dimensions: { - x: size + CLOUDS_OFFSET, - y: size + CLOUDS_OFFSET, - z: size + CLOUDS_OFFSET - }, - angularVelocity: { - x: 0.00, - y: SPIN, - z: 0.00 - }, - angularDamping: 0.0, - damping: 0.0, - ignoreCollisions: false, - lifetime: LIFETIME, - collisionsWillMove: false, - visible: true - }); + this.clouds = Entities.addEntity({ + type: "Model", + shapeType: 'sphere', + modelURL: URL + "clouds.fbx?i=2", + position: position, + dimensions: { + x: size + CLOUDS_OFFSET, + y: size + CLOUDS_OFFSET, + z: size + CLOUDS_OFFSET + }, + angularVelocity: { + x: 0.00, + y: SPIN, + z: 0.00 + }, + angularDamping: 0.0, + damping: 0.0, + ignoreCollisions: false, + lifetime: LIFETIME, + collisionsWillMove: false, + visible: true + }); - this.zone = Entities.addEntity({ - type: "Zone", - position: position, - dimensions: { - x: ZONE_DIM, - y: ZONE_DIM, - z: ZONE_DIM - }, - keyLightDirection: Vec3.normalize(Vec3.subtract(position, Camera.getPosition())), - keyLightIntensity: LIGHT_INTENSITY - }); + this.zone = Entities.addEntity({ + type: "Zone", + position: position, + dimensions: { + x: ZONE_DIM, + y: ZONE_DIM, + z: ZONE_DIM + }, + keyLightDirection: Vec3.normalize(Vec3.subtract(position, Camera.getPosition())), + keyLightIntensity: LIGHT_INTENSITY + }); - this.cleanup = function() { - Entities.deleteEntity(this.clouds); - Entities.deleteEntity(this.earth); - Entities.deleteEntity(this.zone); - } - } + this.cleanup = function() { + Entities.deleteEntity(this.clouds); + Entities.deleteEntity(this.earth); + Entities.deleteEntity(this.zone); + } + } - // Create earth model - var center = Vec3.sum(Camera.getPosition(), Vec3.multiply(MAX_RANGE, Quat.getFront(Camera.getOrientation()))); - var distance = Vec3.length(Vec3.subtract(center, Camera.getPosition())); - var earth = new Earth(center, EARTH_SIZE); + // Create earth model + var center = Vec3.sum(Camera.getPosition(), Vec3.multiply(MAX_RANGE, Quat.getFront(Camera.getOrientation()))); + var distance = Vec3.length(Vec3.subtract(center, Camera.getPosition())); + var earth = new Earth(center, EARTH_SIZE); - var satellites = []; - var SATELLITE_SIZE = 2.0; - var launched = false; - var activeSatellite; + var satellites = []; + var SATELLITE_SIZE = 2.0; + var launched = false; + var activeSatellite; var PERIOD = 4.0; var LARGE_BODY_MASS = 16000.0; var SMALL_BODY_MASS = LARGE_BODY_MASS * 0.000000333; - Satellite = function(position, planetCenter) { - // The Satellite class + Satellite = function(position, planetCenter) { + // The Satellite class - this.launched = false; - this.startPosition = position; - this.readyToLaunch = false; - this.radius = Vec3.length(Vec3.subtract(position, planetCenter)); + this.launched = false; + this.startPosition = position; + this.readyToLaunch = false; + this.radius = Vec3.length(Vec3.subtract(position, planetCenter)); - this.satellite = Entities.addEntity({ - type: "Model", - modelURL: "https://s3.amazonaws.com/hifi-public/marketplace/hificontent/Scripts/planets/satellite/satellite.fbx", - position: this.startPosition, - dimensions: { - x: SATELLITE_SIZE, - y: SATELLITE_SIZE, - z: SATELLITE_SIZE - }, - angularDamping: 0.0, - damping: 0.0, - ignoreCollisions: false, - lifetime: LIFETIME, - collisionsWillMove: false, - }); + this.satellite = Entities.addEntity({ + type: "Model", + modelURL: "https://s3.amazonaws.com/hifi-public/marketplace/hificontent/Scripts/planets/satellite/satellite.fbx", + position: this.startPosition, + dimensions: { + x: SATELLITE_SIZE, + y: SATELLITE_SIZE, + z: SATELLITE_SIZE + }, + angularDamping: 0.0, + damping: 0.0, + ignoreCollisions: false, + lifetime: LIFETIME, + collisionsWillMove: false, + }); - this.getProperties = function() { - return Entities.getEntityProperties(this.satellite); - } + this.getProperties = function() { + return Entities.getEntityProperties(this.satellite); + } - this.launch = function() { - var prop = Entities.getEntityProperties(this.satellite); - var between = Vec3.subtract(planetCenter, prop.position); - var radius = Vec3.length(between); - this.gravity = (4.0 * Math.PI * Math.PI * Math.pow(radius, 3.0)) / (LARGE_BODY_MASS * PERIOD * PERIOD); + this.launch = function() { + var prop = Entities.getEntityProperties(this.satellite); + var between = Vec3.subtract(planetCenter, prop.position); + var radius = Vec3.length(between); + this.gravity = (4.0 * Math.PI * Math.PI * Math.pow(radius, 3.0)) / (LARGE_BODY_MASS * PERIOD * PERIOD); - var initialVelocity = Vec3.normalize(Vec3.cross(between, Y_AXIS)); - initialVelocity = Vec3.multiply(Math.sqrt((this.gravity * LARGE_BODY_MASS) / radius), initialVelocity); - initialVelocity = Vec3.multiply(this.arrow.magnitude, initialVelocity); - initialVelocity = Vec3.multiply(Vec3.length(initialVelocity), this.arrow.direction); + var initialVelocity = Vec3.normalize(Vec3.cross(between, Y_AXIS)); + initialVelocity = Vec3.multiply(Math.sqrt((this.gravity * LARGE_BODY_MASS) / radius), initialVelocity); + initialVelocity = Vec3.multiply(this.arrow.magnitude, initialVelocity); + initialVelocity = Vec3.multiply(Vec3.length(initialVelocity), this.arrow.direction); - Entities.editEntity(this.satellite, { - velocity: initialVelocity - }); - this.launched = true; - }; + Entities.editEntity(this.satellite, { + velocity: initialVelocity + }); + this.launched = true; + }; - this.update = function(deltaTime) { - var prop = Entities.getEntityProperties(this.satellite); - var between = Vec3.subtract(prop.position, planetCenter); - var radius = Vec3.length(between); - var acceleration = -(this.gravity * LARGE_BODY_MASS) * Math.pow(radius, (-2.0)); - var speed = acceleration * deltaTime; - var vel = Vec3.multiply(speed, Vec3.normalize(between)); + this.update = function(deltaTime) { + var prop = Entities.getEntityProperties(this.satellite); + var between = Vec3.subtract(prop.position, planetCenter); + var radius = Vec3.length(between); + var acceleration = -(this.gravity * LARGE_BODY_MASS) * Math.pow(radius, (-2.0)); + var speed = acceleration * deltaTime; + var vel = Vec3.multiply(speed, Vec3.normalize(between)); - var newVelocity = Vec3.sum(prop.velocity, vel); - var newPos = Vec3.sum(prop.position, Vec3.multiply(newVelocity, deltaTime)); + var newVelocity = Vec3.sum(prop.velocity, vel); + var newPos = Vec3.sum(prop.position, Vec3.multiply(newVelocity, deltaTime)); - Entities.editEntity(this.satellite, { - velocity: newVelocity, - position: newPos - }); - }; - } + Entities.editEntity(this.satellite, { + velocity: newVelocity, + position: newPos + }); + }; + } - function mouseDoublePressEvent(event) { - var pickRay = Camera.computePickRay(event.x, event.y); - var addVector = Vec3.multiply(pickRay.direction, distance); - var point = Vec3.sum(Camera.getPosition(), addVector); + function mouseDoublePressEvent(event) { + var pickRay = Camera.computePickRay(event.x, event.y); + var addVector = Vec3.multiply(pickRay.direction, distance); + var point = Vec3.sum(Camera.getPosition(), addVector); - // Create a new satellite - activeSatellite = new Satellite(point, center); - satellites.push(activeSatellite); - } + // Create a new satellite + activeSatellite = new Satellite(point, center); + satellites.push(activeSatellite); + } - function mousePressEvent(event) { - if (!activeSatellite) { - return; - } - // Reset label - if (activeSatellite.arrow) { - activeSatellite.arrow.deleteLabel(); - } - var statsPosition = Vec3.sum(Camera.getPosition(), Vec3.multiply(MAX_RANGE * 0.4, Quat.getFront(Camera.getOrientation()))); - var pickRay = Camera.computePickRay(event.x, event.y) - var rayPickResult = Entities.findRayIntersection(pickRay, true); - if (rayPickResult.entityID === activeSatellite.satellite) { - // Create a draggable vector arrow at satellite position - activeSatellite.arrow = new VectorArrow(distance, true, "INITIAL VELOCITY", statsPosition); - activeSatellite.arrow.onMousePressEvent(event); - activeSatellite.arrow.isDragging = true; - } - } + function mousePressEvent(event) { + if (!activeSatellite) { + return; + } + // Reset label + if (activeSatellite.arrow) { + activeSatellite.arrow.deleteLabel(); + } + var statsPosition = Vec3.sum(Camera.getPosition(), Vec3.multiply(MAX_RANGE * 0.4, Quat.getFront(Camera.getOrientation()))); + var pickRay = Camera.computePickRay(event.x, event.y) + var rayPickResult = Entities.findRayIntersection(pickRay, true); + if (rayPickResult.entityID === activeSatellite.satellite) { + // Create a draggable vector arrow at satellite position + activeSatellite.arrow = new VectorArrow(distance, true, "INITIAL VELOCITY", statsPosition); + activeSatellite.arrow.onMousePressEvent(event); + activeSatellite.arrow.isDragging = true; + } + } - function mouseMoveEvent(event) { - if (!activeSatellite || !activeSatellite.arrow || !activeSatellite.arrow.isDragging) { - return; - } - activeSatellite.arrow.onMouseMoveEvent(event); - } + function mouseMoveEvent(event) { + if (!activeSatellite || !activeSatellite.arrow || !activeSatellite.arrow.isDragging) { + return; + } + activeSatellite.arrow.onMouseMoveEvent(event); + } - function mouseReleaseEvent(event) { - if (!activeSatellite || !activeSatellite.arrow || !activeSatellite.arrow.isDragging) { - return; - } - activeSatellite.arrow.onMouseReleaseEvent(event); - activeSatellite.launch(); - activeSatellite.arrow.cleanup(); - } + function mouseReleaseEvent(event) { + if (!activeSatellite || !activeSatellite.arrow || !activeSatellite.arrow.isDragging) { + return; + } + activeSatellite.arrow.onMouseReleaseEvent(event); + activeSatellite.launch(); + activeSatellite.arrow.cleanup(); + } - var counter = 0.0; - var CHECK_ENERGY_PERIOD = 500; + var counter = 0.0; + var CHECK_ENERGY_PERIOD = 500; - function update(deltaTime) { - if (!activeSatellite) { - return; - } - // Update all satellites - for (var i = 0; i < satellites.length; i++) { - if (!satellites[i].launched) { - continue; - } - satellites[i].update(deltaTime); - } + function update(deltaTime) { + if (!activeSatellite) { + return; + } + // Update all satellites + for (var i = 0; i < satellites.length; i++) { + if (!satellites[i].launched) { + continue; + } + satellites[i].update(deltaTime); + } - counter++; - if (counter % CHECK_ENERGY_PERIOD == 0) { - var prop = activeSatellite.getProperties(); - var error = calcEnergyError(prop.position, Vec3.length(prop.velocity)); - if (Math.abs(error) <= ERROR_THRESH) { - activeSatellite.arrow.editLabel("Nice job! The satellite has reached a stable orbit."); - } else { - activeSatellite.arrow.editLabel("Try again! The satellite is in an unstable orbit."); - } - } - } + counter++; + if (counter % CHECK_ENERGY_PERIOD == 0) { + var prop = activeSatellite.getProperties(); + var error = calcEnergyError(prop.position, Vec3.length(prop.velocity)); + if (Math.abs(error) <= ERROR_THRESH) { + activeSatellite.arrow.editLabel("Nice job! The satellite has reached a stable orbit."); + } else { + activeSatellite.arrow.editLabel("Try again! The satellite is in an unstable orbit."); + } + } + } - this.endGame = function() { - for (var i = 0; i < satellites.length; i++) { - Entities.deleteEntity(satellites[i].satellite); - satellites[i].arrow.cleanup(); - } - earth.cleanup(); - } + this.endGame = function() { + for (var i = 0; i < satellites.length; i++) { + Entities.deleteEntity(satellites[i].satellite); + satellites[i].arrow.cleanup(); + } + earth.cleanup(); + } - function calcEnergyError(pos, vel) { - //Calculate total energy error for active satellite's orbital motion - var radius = activeSatellite.radius; - var gravity = (4.0 * Math.PI * Math.PI * Math.pow(radius, 3.0)) / (LARGE_BODY_MASS * PERIOD * PERIOD); - var initialVelocityCalculated = Math.sqrt((gravity * LARGE_BODY_MASS) / radius); + function calcEnergyError(pos, vel) { + //Calculate total energy error for active satellite's orbital motion + var radius = activeSatellite.radius; + var gravity = (4.0 * Math.PI * Math.PI * Math.pow(radius, 3.0)) / (LARGE_BODY_MASS * PERIOD * PERIOD); + var initialVelocityCalculated = Math.sqrt((gravity * LARGE_BODY_MASS) / radius); - var totalEnergy = 0.5 * LARGE_BODY_MASS * Math.pow(initialVelocityCalculated, 2.0) - ((gravity * LARGE_BODY_MASS * SMALL_BODY_MASS) / radius); - var measuredEnergy = 0.5 * LARGE_BODY_MASS * Math.pow(vel, 2.0) - ((gravity * LARGE_BODY_MASS * SMALL_BODY_MASS) / Vec3.length(Vec3.subtract(pos, center))); - var error = ((measuredEnergy - totalEnergy) / totalEnergy) * 100; - return error; - } + var totalEnergy = 0.5 * LARGE_BODY_MASS * Math.pow(initialVelocityCalculated, 2.0) - ((gravity * LARGE_BODY_MASS * SMALL_BODY_MASS) / radius); + var measuredEnergy = 0.5 * LARGE_BODY_MASS * Math.pow(vel, 2.0) - ((gravity * LARGE_BODY_MASS * SMALL_BODY_MASS) / Vec3.length(Vec3.subtract(pos, center))); + var error = ((measuredEnergy - totalEnergy) / totalEnergy) * 100; + return error; + } - Controller.mousePressEvent.connect(mousePressEvent); - Controller.mouseDoublePressEvent.connect(mouseDoublePressEvent); - Controller.mouseMoveEvent.connect(mouseMoveEvent); - Controller.mouseReleaseEvent.connect(mouseReleaseEvent); - Script.update.connect(update); - Script.scriptEnding.connect(this.endGame); + Controller.mousePressEvent.connect(mousePressEvent); + Controller.mouseDoublePressEvent.connect(mouseDoublePressEvent); + Controller.mouseMoveEvent.connect(mouseMoveEvent); + Controller.mouseReleaseEvent.connect(mouseReleaseEvent); + Script.update.connect(update); + Script.scriptEnding.connect(this.endGame); } \ No newline at end of file From 34d63cdb43eceef33bd8ed4b66e06aae698ec2d1 Mon Sep 17 00:00:00 2001 From: Marcel Verhagen Date: Tue, 28 Jul 2015 22:02:36 +0200 Subject: [PATCH 135/242] Removed double "" --- interface/src/Menu.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 0edd93c5a6..6487e6cac5 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -161,7 +161,7 @@ namespace MenuOption { const QString CenterPlayerInView = "Center Player In View"; const QString Chat = "Chat..."; const QString Collisions = "Collisions"; - const QString Connexion = "Activate 3D Connexion Devices""; + const QString Connexion = "Activate 3D Connexion Devices"; const QString Console = "Console..."; const QString ControlWithSpeech = "Control With Speech"; const QString CopyAddress = "Copy Address to Clipboard"; From 0b25fc335e84e43b75e9ed61ed6998b46ed0fec8 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 28 Jul 2015 13:21:29 -0700 Subject: [PATCH 136/242] Cleanup edit.js entity creation --- examples/edit.js | 228 ++++++++++++++++++++++------------------------- 1 file changed, 106 insertions(+), 122 deletions(-) diff --git a/examples/edit.js b/examples/edit.js index a07779c19d..0f9c9fc4dd 100644 --- a/examples/edit.js +++ b/examples/edit.js @@ -291,22 +291,18 @@ var toolBar = (function () { var RESIZE_TIMEOUT = 120000; // 2 minutes var RESIZE_MAX_CHECKS = RESIZE_TIMEOUT / RESIZE_INTERVAL; function addModel(url) { - var position; + var entityID = createNewEntity({ + type: "Model", + dimensions: DEFAULT_DIMENSIONS, + modelURL: url + }, false); - position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), SPAWN_DISTANCE)); - - if (position.x > 0 && position.y > 0 && position.z > 0) { - var entityId = Entities.addEntity({ - type: "Model", - position: grid.snapToSurface(grid.snapToGrid(position, false, DEFAULT_DIMENSIONS), DEFAULT_DIMENSIONS), - dimensions: DEFAULT_DIMENSIONS, - modelURL: url - }); + if (entityID) { print("Model added: " + url); var checkCount = 0; function resize() { - var entityProperties = Entities.getEntityProperties(entityId); + var entityProperties = Entities.getEntityProperties(entityID); var naturalDimensions = entityProperties.naturalDimensions; checkCount++; @@ -318,21 +314,41 @@ var toolBar = (function () { print("Resize failed: timed out waiting for model (" + url + ") to load"); } } else { - Entities.editEntity(entityId, { dimensions: naturalDimensions }); + Entities.editEntity(entityID, { dimensions: naturalDimensions }); // Reset selection so that the selection overlays will be updated - selectionManager.setSelections([entityId]); + selectionManager.setSelections([entityID]); } } - selectionManager.setSelections([entityId]); + selectionManager.setSelections([entityID]); Script.setTimeout(resize, RESIZE_INTERVAL); - } else { - Window.alert("Can't add model: Model would be out of bounds."); } } + function createNewEntity(properties, dragOnCreate) { + // Default to true if not passed in + dragOnCreate = dragOnCreate == undefined ? true : dragOnCreate; + + var dimensions = properties.dimensions ? properties.dimensions : DEFAULT_DIMENSIONS; + var position = getPositionToCreateEntity(); + var entityID = null; + if (position != null) { + position = grid.snapToSurface(grid.snapToGrid(position, false, dimensions), dimensions), + properties.position = position; + + entityID = Entities.addEntity(properties); + if (dragOnCreate) { + placingEntityID = entityID; + } + } else { + Window.alert("Can't create " + properties.type + ": " + properties.type + " would be out of bounds."); + } + + return entityID; + } + var newModelButtonDown = false; var browseMarketplaceButtonDown = false; that.mousePressEvent = function (event) { @@ -363,127 +379,82 @@ var toolBar = (function () { } if (newCubeButton === toolBar.clicked(clickedOverlay)) { - var position = getPositionToCreateEntity(); + createNewEntity({ + type: "Box", + dimensions: DEFAULT_DIMENSIONS, + color: { red: 255, green: 0, blue: 0 } + }); - if (position.x > 0 && position.y > 0 && position.z > 0) { - placingEntityID = Entities.addEntity({ - type: "Box", - position: grid.snapToSurface(grid.snapToGrid(position, false, DEFAULT_DIMENSIONS), DEFAULT_DIMENSIONS), - dimensions: DEFAULT_DIMENSIONS, - color: { red: 255, green: 0, blue: 0 } - - }); - } else { - Window.alert("Can't create box: Box would be out of bounds."); - } return true; } if (newSphereButton === toolBar.clicked(clickedOverlay)) { - var position = getPositionToCreateEntity(); + createNewEntity({ + type: "Sphere", + dimensions: DEFAULT_DIMENSIONS, + color: { red: 255, green: 0, blue: 0 } + }); - if (position.x > 0 && position.y > 0 && position.z > 0) { - placingEntityID = Entities.addEntity({ - type: "Sphere", - position: grid.snapToSurface(grid.snapToGrid(position, false, DEFAULT_DIMENSIONS), DEFAULT_DIMENSIONS), - dimensions: DEFAULT_DIMENSIONS, - color: { red: 255, green: 0, blue: 0 } - }); - } else { - Window.alert("Can't create sphere: Sphere would be out of bounds."); - } return true; } if (newLightButton === toolBar.clicked(clickedOverlay)) { - var position = getPositionToCreateEntity(); + createNewEntity({ + type: "Light", + dimensions: DEFAULT_LIGHT_DIMENSIONS, + isSpotlight: false, + color: { red: 150, green: 150, blue: 150 }, - if (position.x > 0 && position.y > 0 && position.z > 0) { - placingEntityID = Entities.addEntity({ - type: "Light", - position: grid.snapToSurface(grid.snapToGrid(position, false, DEFAULT_LIGHT_DIMENSIONS), DEFAULT_LIGHT_DIMENSIONS), - dimensions: DEFAULT_LIGHT_DIMENSIONS, - isSpotlight: false, - color: { red: 150, green: 150, blue: 150 }, + constantAttenuation: 1, + linearAttenuation: 0, + quadraticAttenuation: 0, + exponent: 0, + cutoff: 180, // in degrees + }); - constantAttenuation: 1, - linearAttenuation: 0, - quadraticAttenuation: 0, - exponent: 0, - cutoff: 180, // in degrees - }); - } else { - Window.alert("Can't create Light: Light would be out of bounds."); - } return true; } - if (newTextButton === toolBar.clicked(clickedOverlay)) { - var position = getPositionToCreateEntity(); + createNewEntity({ + type: "Text", + dimensions: { x: 0.65, y: 0.3, z: 0.01 }, + backgroundColor: { red: 64, green: 64, blue: 64 }, + textColor: { red: 255, green: 255, blue: 255 }, + text: "some text", + lineHeight: 0.06 + }); - if (position.x > 0 && position.y > 0 && position.z > 0) { - placingEntityID = Entities.addEntity({ - type: "Text", - position: grid.snapToSurface(grid.snapToGrid(position, false, DEFAULT_DIMENSIONS), DEFAULT_DIMENSIONS), - dimensions: { x: 0.65, y: 0.3, z: 0.01 }, - backgroundColor: { red: 64, green: 64, blue: 64 }, - textColor: { red: 255, green: 255, blue: 255 }, - text: "some text", - lineHeight: 0.06 - }); - } else { - Window.alert("Can't create box: Text would be out of bounds."); - } return true; } if (newWebButton === toolBar.clicked(clickedOverlay)) { - var position = getPositionToCreateEntity(); + createNewEntity({ + type: "Web", + dimensions: { x: 1.6, y: 0.9, z: 0.01 }, + sourceUrl: "https://highfidelity.com/", + }); - if (position.x > 0 && position.y > 0 && position.z > 0) { - placingEntityID = Entities.addEntity({ - type: "Web", - position: grid.snapToSurface(grid.snapToGrid(position, false, DEFAULT_DIMENSIONS), DEFAULT_DIMENSIONS), - dimensions: { x: 1.6, y: 0.9, z: 0.01 }, - sourceUrl: "https://highfidelity.com/", - }); - } else { - Window.alert("Can't create Web Entity: would be out of bounds."); - } return true; } if (newZoneButton === toolBar.clicked(clickedOverlay)) { - var position = getPositionToCreateEntity(); + createNewEntity({ + type: "Zone", + dimensions: { x: 10, y: 10, z: 10 }, + }); - if (position.x > 0 && position.y > 0 && position.z > 0) { - placingEntityID = Entities.addEntity({ - type: "Zone", - position: grid.snapToSurface(grid.snapToGrid(position, false, DEFAULT_DIMENSIONS), DEFAULT_DIMENSIONS), - dimensions: { x: 10, y: 10, z: 10 }, - }); - } else { - Window.alert("Can't create box: Text would be out of bounds."); - } return true; } if (newPolyVoxButton === toolBar.clicked(clickedOverlay)) { - var position = getPositionToCreateEntity(); + createNewEntity({ + type: "PolyVox", + dimensions: { x: 10, y: 10, z: 10 }, + voxelVolumeSize: {x:16, y:16, z:16}, + voxelSurfaceStyle: 1 + }); - if (position.x > 0 && position.y > 0 && position.z > 0) { - placingEntityID = Entities.addEntity({ - type: "PolyVox", - position: grid.snapToSurface(grid.snapToGrid(position, false, DEFAULT_DIMENSIONS), - DEFAULT_DIMENSIONS), - dimensions: { x: 10, y: 10, z: 10 }, - voxelVolumeSize: {x:16, y:16, z:16}, - voxelSurfaceStyle: 1 - }); - } else { - Window.alert("Can't create PolyVox: would be out of bounds."); - } return true; } @@ -666,7 +637,7 @@ function handleIdleMouse() { idleMouseTimerId = null; if (isActive) { highlightEntityUnderCursor(lastMousePosition, true); - } + } } function highlightEntityUnderCursor(position, accurateRay) { @@ -837,15 +808,15 @@ function setupModelMenus() { } Menu.addMenuItem({ menuName: "Edit", menuItemName: "Entity List...", shortcutKey: "CTRL+META+L", afterItem: "Models" }); - Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Selecting of Large Models", shortcutKey: "CTRL+META+L", + Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Selecting of Large Models", shortcutKey: "CTRL+META+L", afterItem: "Entity List...", isCheckable: true, isChecked: true }); - Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Selecting of Small Models", shortcutKey: "CTRL+META+S", + Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Selecting of Small Models", shortcutKey: "CTRL+META+S", afterItem: "Allow Selecting of Large Models", isCheckable: true, isChecked: true }); - Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Selecting of Lights", shortcutKey: "CTRL+SHIFT+META+L", + Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Selecting of Lights", shortcutKey: "CTRL+SHIFT+META+L", afterItem: "Allow Selecting of Small Models", isCheckable: true }); - Menu.addMenuItem({ menuName: "Edit", menuItemName: "Select All Entities In Box", shortcutKey: "CTRL+SHIFT+META+A", + Menu.addMenuItem({ menuName: "Edit", menuItemName: "Select All Entities In Box", shortcutKey: "CTRL+SHIFT+META+A", afterItem: "Allow Selecting of Lights" }); - Menu.addMenuItem({ menuName: "Edit", menuItemName: "Select All Entities Touching Box", shortcutKey: "CTRL+SHIFT+META+T", + Menu.addMenuItem({ menuName: "Edit", menuItemName: "Select All Entities Touching Box", shortcutKey: "CTRL+SHIFT+META+T", afterItem: "Select All Entities In Box" }); Menu.addMenuItem({ menuName: "File", menuItemName: "Models", isSeparator: true, beforeItem: "Settings" }); @@ -962,7 +933,7 @@ function selectAllEtitiesInCurrentSelectionBox(keepIfTouching) { entities.splice(i, 1); --i; } - } + } } selectionManager.setSelections(entities); } @@ -1038,17 +1009,29 @@ function handeMenuEvent(menuItem) { tooltip.show(false); } +// This function tries to find a reasonable position to place a new entity based on the camera +// position. If a reasonable position within the world bounds can't be found, `null` will +// be returned. The returned position will also take into account grid snapping settings. function getPositionToCreateEntity() { var distance = cameraManager.enabled ? cameraManager.zoomDistance : DEFAULT_ENTITY_DRAG_DROP_DISTANCE; var direction = Quat.getFront(Camera.orientation); var offset = Vec3.multiply(distance, direction); - var position = Vec3.sum(Camera.position, offset); + var placementPosition = Vec3.sum(Camera.position, offset); - position.x = Math.max(0, position.x); - position.y = Math.max(0, position.y); - position.z = Math.max(0, position.z); + var cameraPosition = Camera.position; - return position; + var cameraOutOfBounds = cameraPosition.x < 0 || cameraPosition.y < 0 || cameraPosition.z < 0; + var placementOutOfBounds = placementPosition.x < 0 || placementPosition.y < 0 || placementPosition.z < 0; + + if (cameraOutOfBounds && placementOutOfBounds) { + return null; + } + + placementPosition.x = Math.max(0, placementPosition.x); + placementPosition.y = Math.max(0, placementPosition.y); + placementPosition.z = Math.max(0, placementPosition.z); + + return placementPosition; } function importSVO(importURL) { @@ -1064,16 +1047,17 @@ function importSVO(importURL) { if (success) { var VERY_LARGE = 10000; - var position = { x: 0.01, y: 0.01, z: 0.01}; + var position = { x: 0, y: 0, z: 0 }; if (Clipboard.getClipboardContentsLargestDimension() < VERY_LARGE) { position = getPositionToCreateEntity(); } - if (position.x > 0 && position.y > 0 && position.z > 0) { + if (position != null) { var pastedEntityIDs = Clipboard.pasteEntities(position); if (isActive) { selectionManager.setSelections(pastedEntityIDs); } + Window.raiseMainWindow(); } else { Window.alert("Can't import objects: objects would be out of bounds."); @@ -1264,7 +1248,7 @@ PropertiesTool = function(opts) { if (data.properties.keyLightDirection !== undefined) { data.properties.keyLightDirection = Vec3.fromPolar( data.properties.keyLightDirection.x * DEGREES_TO_RADIANS, data.properties.keyLightDirection.y * DEGREES_TO_RADIANS); - } + } Entities.editEntity(selectionManager.selections[0], data.properties); if (data.properties.name != undefined) { entityListTool.sendUpdate(); @@ -1360,8 +1344,8 @@ PropertiesTool = function(opts) { var properties = selectionManager.savedProperties[selectionManager.selections[i]]; if (properties.type == "Zone") { var centerOfZone = properties.boundingBox.center; - var atmosphereCenter = { x: centerOfZone.x, - y: centerOfZone.y - properties.atmosphere.innerRadius, + var atmosphereCenter = { x: centerOfZone.x, + y: centerOfZone.y - properties.atmosphere.innerRadius, z: centerOfZone.z }; Entities.editEntity(selectionManager.selections[i], { From c83bcf77c67dcf20d4149db58db2b9aba86f36ff Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Tue, 28 Jul 2015 13:45:42 -0700 Subject: [PATCH 137/242] test script to load test with lots of entities --- examples/gridTest.js | 71 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 examples/gridTest.js diff --git a/examples/gridTest.js b/examples/gridTest.js new file mode 100644 index 0000000000..a29f2be7bc --- /dev/null +++ b/examples/gridTest.js @@ -0,0 +1,71 @@ +// +// Created by Philip Rosedale on July 28, 2015 +// 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 +// +// Creates a rectangular grid of objects, starting at the origin and proceeding along the X/Z plane. +// Useful for testing the rendering, LOD, and octree storage aspects of the system. +// + +var SIZE = 10.0; +var SEPARATION = 20.0; +var ROWS_X = 30; +var ROWS_Z = 30; +var TYPE = "Sphere"; // Right now this can be "Box" or "Model" or "Sphere" +var MODEL_URL = "https://hifi-public.s3.amazonaws.com/models/props/LowPolyIsland/CypressTreeGroup.fbx"; +var MODEL_DIMENSION = { x: 33, y: 16, z: 49 }; +var RATE_PER_SECOND = 1000; +var SCRIPT_INTERVAL = 100; + +var addRandom = false; + +var x = 0; +var z = 0; +var totalCreated = 0; + +Script.setInterval(function () { + if (!Entities.serversExist() || !Entities.canRez()) { + return; + } + + var numToCreate = RATE_PER_SECOND * (SCRIPT_INTERVAL / 1000.0); + for (var i = 0; i < numToCreate; i++) { + var position = { x: SIZE + (x * SEPARATION), y: SIZE, z: SIZE + (z * SEPARATION) }; + if (TYPE == "Model") { + Entities.addEntity({ + type: TYPE, + name: "gridTest", + modelURL: MODEL_URL, + position: position, + dimensions: MODEL_DIMENSION, + ignoreCollisions: true, + collisionsWillMove: false + }); + } else { + Entities.addEntity({ + type: TYPE, + name: "gridTest", + position: position, + dimensions: { x: SIZE, y: SIZE, z: SIZE }, + color: { red: x / ROWS_X * 255, green: 50, blue: z / ROWS_Z * 255 }, + ignoreCollisions: true, + collisionsWillMove: false + }); + } + + totalCreated++; + + x++; + if (x == ROWS_X) { + x = 0; + z++; + print("Created: " + totalCreated); + } + if (z == ROWS_Z) { + Script.stop(); + } + } +}, SCRIPT_INTERVAL); + From df9b66d2679c3740c8d885d5d02c2e37ea098e22 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Tue, 28 Jul 2015 15:08:52 -0700 Subject: [PATCH 138/242] Implement the uniform buffer and resource texture cache and their reset --- libraries/gpu/src/gpu/GLBackend.h | 30 +++++-- libraries/gpu/src/gpu/GLBackendPipeline.cpp | 86 +++++++++++++++++++-- 2 files changed, 103 insertions(+), 13 deletions(-) diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index 843e5d2006..215e01689b 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -197,6 +197,15 @@ public: uint32 getNumInputBuffers() const { return _input._invalidBuffers.size(); } + // this is the maximum per shader stage on the low end apple + // TODO make it platform dependant at init time + static const int MAX_NUM_UNIFORM_BUFFERS = 12; + uint32 getMaxNumUniformBuffers() const { return MAX_NUM_UNIFORM_BUFFERS; } + + // this is the maximum per shader stage on the low end apple + // TODO make it platform dependant at init time + static const int MAX_NUM_RESOURCE_TEXTURES = 16; + uint32 getMaxNumResourceTextures() const { return MAX_NUM_RESOURCE_TEXTURES; } // The State setters called by the GLState::Commands when a new state is assigned void do_setStateFillMode(int32 mode); @@ -332,19 +341,30 @@ protected: // Uniform Stage void do_setUniformBuffer(Batch& batch, uint32 paramOffset); - + + void releaseUniformBuffer(int slot); void resetUniformStage(); struct UniformStageState { - - }; + Buffers _buffers; + + UniformStageState(): + _buffers(MAX_NUM_UNIFORM_BUFFERS, nullptr) + {} + } _uniform; // Resource Stage void do_setResourceTexture(Batch& batch, uint32 paramOffset); + void releaseResourceTexture(int slot); void resetResourceStage(); struct ResourceStageState { - - }; + Textures _textures; + + ResourceStageState(): + _textures(MAX_NUM_RESOURCE_TEXTURES, nullptr) + {} + + } _resource; // Pipeline Stage void do_setPipeline(Batch& batch, uint32 paramOffset); diff --git a/libraries/gpu/src/gpu/GLBackendPipeline.cpp b/libraries/gpu/src/gpu/GLBackendPipeline.cpp index 4a54753af8..090dd4ad31 100755 --- a/libraries/gpu/src/gpu/GLBackendPipeline.cpp +++ b/libraries/gpu/src/gpu/GLBackendPipeline.cpp @@ -178,8 +178,27 @@ void GLBackend::resetPipelineStage() { glUseProgram(0); } + +void GLBackend::releaseUniformBuffer(int slot) { +#if (GPU_FEATURE_PROFILE == GPU_CORE) + auto& buf = _uniform._buffers[slot]; + if (buf) { + auto* object = Backend::getGPUObject(*buf); + if (object) { + GLuint bo = object->_buffer; + glBindBufferBase(GL_UNIFORM_BUFFER, slot, 0); // RELEASE + + (void) CHECK_GL_ERROR(); + } + buf.reset(); + } +#endif +} + void GLBackend::resetUniformStage() { - + for (int i = 0; i < _uniform._buffers.size(); i++) { + releaseUniformBuffer(i); + } } void GLBackend::do_setUniformBuffer(Batch& batch, uint32 paramOffset) { @@ -188,9 +207,31 @@ void GLBackend::do_setUniformBuffer(Batch& batch, uint32 paramOffset) { GLintptr rangeStart = batch._params[paramOffset + 1]._uint; GLsizeiptr rangeSize = batch._params[paramOffset + 0]._uint; + + + #if (GPU_FEATURE_PROFILE == GPU_CORE) - GLuint bo = getBufferID(*uniformBuffer); - glBindBufferRange(GL_UNIFORM_BUFFER, slot, bo, rangeStart, rangeSize); + if (!uniformBuffer) { + releaseUniformBuffer(slot); + return; + } + + // check cache before thinking + if (_uniform._buffers[slot] == uniformBuffer) { + return; + } + + // Sync BufferObject + auto* object = GLBackend::syncGPUObject(*uniformBuffer); + if (object) { + glBindBufferRange(GL_UNIFORM_BUFFER, slot, object->_buffer, rangeStart, rangeSize); + + _uniform._buffers[slot] = uniformBuffer; + (void) CHECK_GL_ERROR(); + } else { + releaseResourceTexture(slot); + return; + } #else // because we rely on the program uniform mechanism we need to have // the program bound, thank you MacOSX Legacy profile. @@ -202,23 +243,49 @@ void GLBackend::do_setUniformBuffer(Batch& batch, uint32 paramOffset) { // NOT working so we ll stick to the uniform float array until we move to core profile // GLuint bo = getBufferID(*uniformBuffer); //glUniformBufferEXT(_shader._program, slot, bo); -#endif + (void) CHECK_GL_ERROR(); + +#endif +} + +void GLBackend::releaseResourceTexture(int slot) { + auto& tex = _resource._textures[slot]; + if (tex) { + auto* object = Backend::getGPUObject(*tex); + if (object) { + GLuint to = object->_texture; + GLuint target = object->_target; + glActiveTexture(GL_TEXTURE0 + slot); + glBindTexture(target, 0); // RELEASE + + (void) CHECK_GL_ERROR(); + } + tex.reset(); + } } void GLBackend::resetResourceStage() { - + for (int i = 0; i < _resource._textures.size(); i++) { + releaseResourceTexture(i); + } } void GLBackend::do_setResourceTexture(Batch& batch, uint32 paramOffset) { GLuint slot = batch._params[paramOffset + 1]._uint; - TexturePointer uniformTexture = batch._textures.get(batch._params[paramOffset + 0]._uint); + TexturePointer resourceTexture = batch._textures.get(batch._params[paramOffset + 0]._uint); - if (!uniformTexture) { + if (!resourceTexture) { + releaseResourceTexture(slot); + return; + } + // check cache before thinking + if (_resource._textures[slot] == resourceTexture) { return; } - GLTexture* object = GLBackend::syncGPUObject(*uniformTexture); + // Always make sure the GLObject is in sync + GLTexture* object = GLBackend::syncGPUObject(*resourceTexture); if (object) { GLuint to = object->_texture; GLuint target = object->_target; @@ -227,7 +294,10 @@ void GLBackend::do_setResourceTexture(Batch& batch, uint32 paramOffset) { (void) CHECK_GL_ERROR(); + _resource._textures[slot] = resourceTexture; + } else { + releaseResourceTexture(slot); return; } } From 469bace7ca4ec8d00ce2f6453f2d1d61df613e65 Mon Sep 17 00:00:00 2001 From: ericrius1 Date: Tue, 28 Jul 2015 15:15:34 -0700 Subject: [PATCH 139/242] removed unnessary computations from hit effect fragment shader --- libraries/render-utils/src/hit_effect.slf | 18 ++++++------------ libraries/render-utils/src/hit_effect.slv | 5 ++++- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/libraries/render-utils/src/hit_effect.slf b/libraries/render-utils/src/hit_effect.slf index 2c308f3711..6c0dcd7a70 100644 --- a/libraries/render-utils/src/hit_effect.slf +++ b/libraries/render-utils/src/hit_effect.slf @@ -11,20 +11,14 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -<@include gpu/Transform.slh@> -<$declareStandardTransform()$> + <@include DeferredBufferWrite.slh@> -void main(void) { +varying vec2 varQuadPosition; - TransformCamera cam = getTransformCamera(); - vec4 myViewport; - <$transformCameraViewport(cam, myViewport)$> - vec2 center = vec2(myViewport.z/2.0, myViewport.w/2.0); - float distFromCenter = distance(center, gl_FragCoord.xy); - //normalize distance from center based on average of screen width and height - float normalizationFactor = (myViewport.z + myViewport.w)/2.0; - distFromCenter = distFromCenter/normalizationFactor; - float alpha = mix(0.0, 1.0, pow(distFromCenter, 1.5)); +void main(void) { + vec2 center = vec2(0.0, 0.0); + float distFromCenter = distance( vec2(0.0, 0.0), varQuadPosition); + float alpha = mix(0.0, 0.5, pow(distFromCenter,5.)); gl_FragColor = vec4(1.0, 0.0, 0.0, alpha); } \ No newline at end of file diff --git a/libraries/render-utils/src/hit_effect.slv b/libraries/render-utils/src/hit_effect.slv index e7c3062667..d1efdebc18 100644 --- a/libraries/render-utils/src/hit_effect.slv +++ b/libraries/render-utils/src/hit_effect.slv @@ -16,6 +16,9 @@ <$declareStandardTransform()$> +varying vec2 varQuadPosition; + void main(void) { - gl_Position = gl_Vertex; + varQuadPosition = gl_Vertex.xy; + gl_Position = gl_Vertex; } \ No newline at end of file From ef5e7e9ccc2fa113a7c57597dfdb7742f32ca773 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 28 Jul 2015 15:45:48 -0700 Subject: [PATCH 140/242] remove PlaneShape dependency from GeometryUtils also add unit tests for indRayRectangleIntersection() --- libraries/shared/src/GeometryUtil.cpp | 58 +++++---- tests/shared/src/GeometryUtilTests.cpp | 160 +++++++++++++++++++++++++ tests/shared/src/GeometryUtilTests.h | 28 +++++ 3 files changed, 224 insertions(+), 22 deletions(-) create mode 100644 tests/shared/src/GeometryUtilTests.cpp create mode 100644 tests/shared/src/GeometryUtilTests.h diff --git a/libraries/shared/src/GeometryUtil.cpp b/libraries/shared/src/GeometryUtil.cpp index afb5890d05..b88166995a 100644 --- a/libraries/shared/src/GeometryUtil.cpp +++ b/libraries/shared/src/GeometryUtil.cpp @@ -11,10 +11,10 @@ #include #include +#include #include "GeometryUtil.h" #include "NumericalConstants.h" -#include "PlaneShape.h" #include "RayIntersectionInfo.h" glm::vec3 computeVectorFromPointToSegment(const glm::vec3& point, const glm::vec3& start, const glm::vec3& end) { @@ -496,31 +496,45 @@ void PolygonClip::copyCleanArray(int& lengthA, glm::vec2* vertexArrayA, int& len bool findRayRectangleIntersection(const glm::vec3& origin, const glm::vec3& direction, const glm::quat& rotation, const glm::vec3& position, const glm::vec2& dimensions, float& distance) { - RayIntersectionInfo rayInfo; - rayInfo._rayStart = origin; - rayInfo._rayDirection = direction; - rayInfo._rayLength = std::numeric_limits::max(); - - PlaneShape plane; - const glm::vec3 UNROTATED_NORMAL(0.0f, 0.0f, -1.0f); glm::vec3 normal = rotation * UNROTATED_NORMAL; - plane.setNormal(normal); - plane.setPoint(position); // the position is definitely a point on our plane - bool intersects = plane.findRayIntersection(rayInfo); + bool maybeIntersects = false; + float denominator = glm::dot(normal, direction); + glm::vec3 offset = origin - position; + float normDotOffset = glm::dot(offset, normal); + float d = 0.0f; + if (fabsf(denominator) < EPSILON) { + // line is perpendicular to plane + if (fabsf(normDotOffset) < EPSILON) { + // ray starts on the plane + maybeIntersects = true; - if (intersects) { - distance = rayInfo._hitDistance; - - glm::vec3 hitPosition = origin + (distance * direction); - glm::vec3 localHitPosition = glm::inverse(rotation) * (hitPosition - position); - - glm::vec2 halfDimensions = 0.5f * dimensions; - - intersects = -halfDimensions.x <= localHitPosition.x && localHitPosition.x <= halfDimensions.x - && -halfDimensions.y <= localHitPosition.y && localHitPosition.y <= halfDimensions.y; + // compute distance to closest approach + d = - glm::dot(offset, direction); // distance to closest approach of center of rectangle + if (d < 0.0f) { + // ray points away from center of rectangle, so ray's start is the closest approach + d = 0.0f; + } + } + } else { + d = - normDotOffset / denominator; + if (d > 0.0f) { + // ray points toward plane + maybeIntersects = true; + } } - return intersects; + if (maybeIntersects) { + glm::vec3 hitPosition = origin + (d * direction); + glm::vec3 localHitPosition = glm::inverse(rotation) * (hitPosition - position); + glm::vec2 halfDimensions = 0.5f * dimensions; + if (fabsf(localHitPosition.x) < halfDimensions.x && fabsf(localHitPosition.y) < halfDimensions.y) { + // only update distance on intersection + distance = d; + return true; + } + } + + return false; } diff --git a/tests/shared/src/GeometryUtilTests.cpp b/tests/shared/src/GeometryUtilTests.cpp new file mode 100644 index 0000000000..798951e304 --- /dev/null +++ b/tests/shared/src/GeometryUtilTests.cpp @@ -0,0 +1,160 @@ +// +// GeometryUtilTests.cpp +// tests/physics/src +// +// Created by Andrew Meadows on 2015.07.27 +// 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 "GeometryUtilTests.h" + +#include +#include +#include + +#include <../QTestExtensions.h> + + +QTEST_MAIN(GeometryUtilTests) + +float getErrorDifference(const float& a, const float& b) { + return fabsf(a - b); +} + +float getErrorDifference(const glm::vec3& a, const glm::vec3& b) { + return glm::distance(a, b); +} + +void GeometryUtilTests::testLocalRayRectangleIntersection() { + glm::vec3 xAxis(1.0f, 0.0f, 0.0f); + glm::vec3 yAxis(0.0f, 1.0f, 0.0f); + glm::vec3 zAxis(0.0f, 0.0f, 1.0f); + + // create a rectangle in the local frame on the XY plane with normal along -zAxis + // (this is the assumed local orientation of the rectangle in findRayRectangleIntersection()) + glm::vec2 rectDimensions(3.0f, 5.0f); + glm::vec3 rectCenter(0.0f, 0.0f, 0.0f); + glm::quat rectRotation = glm::quat(); // identity + + // create points for generating rays that hit the plane and don't + glm::vec3 rayStart(1.0f, 2.0f, 3.0f); + float delta = 0.1f; + + { // verify hit + glm::vec3 rayEnd = rectCenter + rectRotation * ((0.5f * rectDimensions.x - delta) * xAxis); + glm::vec3 rayHitDirection = glm::normalize(rayEnd - rayStart); + float expectedDistance = glm::length(rayEnd - rayStart); + + float distance = FLT_MAX; + bool hit = findRayRectangleIntersection(rayStart, rayHitDirection, rectRotation, rectCenter, rectDimensions, distance); + QCOMPARE(hit, true); + QCOMPARE_WITH_ABS_ERROR(distance, expectedDistance, EPSILON); + } + + { // verify miss + glm::vec3 rayEnd = rectCenter + rectRotation * ((0.5f * rectDimensions.y + delta) * yAxis); + glm::vec3 rayMissDirection = glm::normalize(rayEnd - rayStart); + float distance = FLT_MAX; + bool hit = findRayRectangleIntersection(rayStart, rayMissDirection, rectRotation, rectCenter, rectDimensions, distance); + QCOMPARE(hit, false); + QCOMPARE(distance, FLT_MAX); // distance should be unchanged + } + + { // hit with co-planer line + float yFraction = 0.25f; + rayStart = rectCenter + rectRotation * (rectDimensions.x * xAxis + yFraction * rectDimensions.y * yAxis); + + glm::vec3 rayEnd = rectCenter - rectRotation * (rectDimensions.x * xAxis + yFraction * rectDimensions.y * yAxis); + glm::vec3 rayHitDirection = glm::normalize(rayEnd - rayStart); + float expectedDistance = rectDimensions.x; + + float distance = FLT_MAX; + bool hit = findRayRectangleIntersection(rayStart, rayHitDirection, rectRotation, rectCenter, rectDimensions, distance); + QCOMPARE(hit, true); + QCOMPARE_WITH_ABS_ERROR(distance, expectedDistance, EPSILON); + } + + { // miss with co-planer line + float yFraction = 0.75f; + rayStart = rectCenter + rectRotation * (rectDimensions.x * xAxis + (yFraction * rectDimensions.y) * yAxis); + + glm::vec3 rayEnd = rectCenter + rectRotation * (- rectDimensions.x * xAxis + (yFraction * rectDimensions.y) * yAxis); + glm::vec3 rayHitDirection = glm::normalize(rayEnd - rayStart); + + float distance = FLT_MAX; + bool hit = findRayRectangleIntersection(rayStart, rayHitDirection, rectRotation, rectCenter, rectDimensions, distance); + QCOMPARE(hit, false); + QCOMPARE(distance, FLT_MAX); // distance should be unchanged + } +} + +void GeometryUtilTests::testWorldRayRectangleIntersection() { + glm::vec3 xAxis(1.0f, 0.0f, 0.0f); + glm::vec3 yAxis(0.0f, 1.0f, 0.0f); + glm::vec3 zAxis(0.0f, 0.0f, 1.0f); + + // create a rectangle in the local frame on the XY plane with normal along -zAxis + // (this is the assumed local orientation of the rectangle in findRayRectangleIntersection()) + // and then rotate and shift everything into the world frame + glm::vec2 rectDimensions(3.0f, 5.0f); + glm::vec3 rectCenter(0.7f, 0.5f, -0.3f); + glm::quat rectRotation = glm::quat(); // identity + + + // create points for generating rays that hit the plane and don't + glm::vec3 rayStart(1.0f, 2.0f, 3.0f); + float delta = 0.1f; + + { // verify hit + glm::vec3 rayEnd = rectCenter + rectRotation * ((0.5f * rectDimensions.x - delta) * xAxis); + glm::vec3 rayHitDirection = glm::normalize(rayEnd - rayStart); + float expectedDistance = glm::length(rayEnd - rayStart); + + float distance = FLT_MAX; + bool hit = findRayRectangleIntersection(rayStart, rayHitDirection, rectRotation, rectCenter, rectDimensions, distance); + QCOMPARE(hit, true); + QCOMPARE_WITH_ABS_ERROR(distance, expectedDistance, EPSILON); + } + + { // verify miss + glm::vec3 rayEnd = rectCenter + rectRotation * ((0.5f * rectDimensions.y + delta) * yAxis); + glm::vec3 rayMissDirection = glm::normalize(rayEnd - rayStart); + float distance = FLT_MAX; + bool hit = findRayRectangleIntersection(rayStart, rayMissDirection, rectRotation, rectCenter, rectDimensions, distance); + QCOMPARE(hit, false); + QCOMPARE(distance, FLT_MAX); // distance should be unchanged + } + + { // hit with co-planer line + float yFraction = 0.25f; + rayStart = rectCenter + rectRotation * (rectDimensions.x * xAxis + (yFraction * rectDimensions.y) * yAxis); + + glm::vec3 rayEnd = rectCenter - rectRotation * (rectDimensions.x * xAxis + (yFraction * rectDimensions.y) * yAxis); + glm::vec3 rayHitDirection = glm::normalize(rayEnd - rayStart); + float expectedDistance = rectDimensions.x; + + float distance = FLT_MAX; + bool hit = findRayRectangleIntersection(rayStart, rayHitDirection, rectRotation, rectCenter, rectDimensions, distance); + QCOMPARE(hit, true); + QCOMPARE_WITH_ABS_ERROR(distance, expectedDistance, EPSILON); + } + + { // miss with co-planer line + float yFraction = 0.75f; + rayStart = rectCenter + rectRotation * (rectDimensions.x * xAxis + (yFraction * rectDimensions.y) * yAxis); + + glm::vec3 rayEnd = rectCenter + rectRotation * (-rectDimensions.x * xAxis + (yFraction * rectDimensions.y) * yAxis); + glm::vec3 rayHitDirection = glm::normalize(rayEnd - rayStart); + + float distance = FLT_MAX; + bool hit = findRayRectangleIntersection(rayStart, rayHitDirection, rectRotation, rectCenter, rectDimensions, distance); + QCOMPARE(hit, false); + QCOMPARE(distance, FLT_MAX); // distance should be unchanged + } +} + diff --git a/tests/shared/src/GeometryUtilTests.h b/tests/shared/src/GeometryUtilTests.h new file mode 100644 index 0000000000..d75fce556e --- /dev/null +++ b/tests/shared/src/GeometryUtilTests.h @@ -0,0 +1,28 @@ +// +// GeometryUtilTests.h +// tests/physics/src +// +// Created by Andrew Meadows on 2014.05.30 +// 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_AngularConstraintTests_h +#define hifi_AngularConstraintTests_h + +#include +#include + +class GeometryUtilTests : public QObject { + Q_OBJECT +private slots: + void testLocalRayRectangleIntersection(); + void testWorldRayRectangleIntersection(); +}; + +float getErrorDifference(const float& a, const float& b); +float getErrorDifference(const glm::vec3& a, const glm::vec3& b); + +#endif // hifi_AngularConstraintTests_h From 8b20f9d3a68a31450c95a6dbb90f51aa9fca8837 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Tue, 28 Jul 2015 15:48:01 -0700 Subject: [PATCH 141/242] do the minimum include to use glew on linux --- libraries/gpu/src/gpu/GLBackend.cpp | 7 +++++++ libraries/gpu/src/gpu/GPUConfig.h | 7 +++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/libraries/gpu/src/gpu/GLBackend.cpp b/libraries/gpu/src/gpu/GLBackend.cpp index 6b1d552be9..3942af4f84 100644 --- a/libraries/gpu/src/gpu/GLBackend.cpp +++ b/libraries/gpu/src/gpu/GLBackend.cpp @@ -115,6 +115,13 @@ GLBackend::GLBackend() : #endif #if defined(Q_OS_LINUX) + GLenum err = glewInit(); + if (GLEW_OK != err) { + /* Problem: glewInit failed, something is seriously wrong. */ + qCDebug(gpulogging, "Error: %s\n", glewGetErrorString(err)); + } + qCDebug(gpulogging, "Status: Using GLEW %s\n", glewGetString(GLEW_VERSION)); + // TODO: Write the correct code for Linux... /* if (wglewGetExtension("WGL_EXT_swap_control")) { int swapInterval = wglGetSwapIntervalEXT(); diff --git a/libraries/gpu/src/gpu/GPUConfig.h b/libraries/gpu/src/gpu/GPUConfig.h index 1d092dbc6a..a9657076fa 100644 --- a/libraries/gpu/src/gpu/GPUConfig.h +++ b/libraries/gpu/src/gpu/GPUConfig.h @@ -34,8 +34,11 @@ #elif defined(ANDROID) #else -#include -#include + +#include +#include +//#include +//#include #define GPU_FEATURE_PROFILE GPU_LEGACY #define GPU_TRANSFORM_PROFILE GPU_LEGACY From 9cf3dfd3f7ef47469f87773daac5de87fba2b90f Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 28 Jul 2015 15:49:39 -0700 Subject: [PATCH 142/242] test this on windows ? --- interface/src/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b7522633ba..8f0a0e864f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1014,7 +1014,7 @@ void Application::paintGL() { // Back to the default framebuffer; gpu::Batch batch; batch.resetStages(); - renderArgs._context->render(batch); + // renderArgs._context->render(batch); } void Application::runTests() { From 27d3d3f450cc904636cfe55e13a4a1d053923c82 Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 28 Jul 2015 15:51:15 -0700 Subject: [PATCH 143/242] fix w to x --- libraries/gpu/src/gpu/GPUConfig.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/gpu/src/gpu/GPUConfig.h b/libraries/gpu/src/gpu/GPUConfig.h index a9657076fa..2e3149919c 100644 --- a/libraries/gpu/src/gpu/GPUConfig.h +++ b/libraries/gpu/src/gpu/GPUConfig.h @@ -36,7 +36,7 @@ #else #include -#include +#include //#include //#include From b06485c266e783a1088d2f597a91ac1a22d17dd2 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 28 Jul 2015 17:41:45 -0700 Subject: [PATCH 144/242] Remove cauterize code from Rig and move it back into Model. * cauterize code is used as at render time and is not dependent on the jointStates. * MyAvatar now initialize the bone set used for cauterization and makes the decision to perform cauterization or not in preRender. --- interface/src/avatar/MyAvatar.cpp | 42 +++++++++++--- interface/src/avatar/MyAvatar.h | 4 +- interface/src/avatar/SkeletonModel.cpp | 10 +--- libraries/animation/src/Rig.cpp | 79 +++++--------------------- libraries/animation/src/Rig.h | 13 +---- libraries/render-utils/src/Model.cpp | 41 ++++++++++--- libraries/render-utils/src/Model.h | 16 +++++- 7 files changed, 98 insertions(+), 107 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 5f0b5f03da..c995131427 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -969,12 +969,8 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { Avatar::setSkeletonModelURL(skeletonModelURL); render::ScenePointer scene = Application::getInstance()->getMain3DScene(); _billboardValid = false; - - if (_useFullAvatar) { - _skeletonModel.setVisibleInScene(_prevShouldDrawHead, scene); - } else { - _skeletonModel.setVisibleInScene(true, scene); - } + _skeletonModel.setVisibleInScene(true, scene); + _headBoneSet.clear(); } void MyAvatar::useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName) { @@ -1184,17 +1180,45 @@ void MyAvatar::setVisibleInSceneIfReady(Model* model, render::ScenePointer scene } } +void MyAvatar::initHeadBones() { + int neckJointIndex = -1; + if (_skeletonModel.getGeometry()) { + neckJointIndex = _skeletonModel.getGeometry()->getFBXGeometry().neckJointIndex; + } + if (neckJointIndex == -1) { + return; + } + _headBoneSet.clear(); + std::queue q; + q.push(neckJointIndex); + _headBoneSet.insert(neckJointIndex); + + // fbxJoints only hold links to parents not children, so we have to do a bit of extra work here. + while (q.size() > 0) { + int jointIndex = q.front(); + for (int i = 0; i < _skeletonModel.getJointStateCount(); i++) { + if (jointIndex == _skeletonModel.getParentJointIndex(i)) { + _headBoneSet.insert(i); + q.push(i); + } + } + q.pop(); + } +} + void MyAvatar::preRender(RenderArgs* renderArgs) { render::ScenePointer scene = Application::getInstance()->getMain3DScene(); const bool shouldDrawHead = shouldRenderHead(renderArgs); - _skeletonModel.initWhenReady(scene); + if (_skeletonModel.initWhenReady(scene)) { + initHeadBones(); + _skeletonModel.setCauterizeBoneSet(_headBoneSet); + } if (shouldDrawHead != _prevShouldDrawHead) { if (_useFullAvatar) { - _skeletonModel.setVisibleInScene(true, scene); - _rig->setFirstPerson(!shouldDrawHead); + _skeletonModel.setCauterizeBones(!shouldDrawHead); } else { getHead()->getFaceModel().setVisibleInScene(shouldDrawHead, scene); } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 802c92ec2c..67097a8f3b 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -273,7 +273,8 @@ private: void updatePosition(float deltaTime); void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency); void maybeUpdateBillboard(); - + void initHeadBones(); + // Avatar Preferences bool _useFullAvatar = false; QUrl _fullAvatarURLFromPreferences; @@ -286,6 +287,7 @@ private: RigPointer _rig; bool _prevShouldDrawHead; + std::unordered_set _headBoneSet; }; #endif // hifi_MyAvatar_h diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 636e58c5a8..122559bedb 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -51,7 +51,7 @@ SkeletonModel::~SkeletonModel() { void SkeletonModel::initJointStates(QVector states) { const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - _boundingRadius = _rig->initJointStates(states, parentTransform, geometry.neckJointIndex); + _boundingRadius = _rig->initJointStates(states, parentTransform); // Determine the default eye position for avatar scale = 1.0 int headJointIndex = _geometry->getFBXGeometry().headJointIndex; @@ -175,14 +175,6 @@ void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { restoreRightHandPosition(HAND_RESTORATION_RATE, PALM_PRIORITY); } } - - // if (_isFirstPerson) { - // cauterizeHead(); - // updateClusterMatrices(); - // } - if (_rig->getJointsAreDirty()) { - updateClusterMatrices(); - } } void SkeletonModel::renderIKConstraints(gpu::Batch& batch) { diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 0bd3f14096..302211b556 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -124,9 +124,8 @@ void Rig::deleteAnimations() { _animationHandles.clear(); } -float Rig::initJointStates(QVector states, glm::mat4 parentTransform, int neckJointIndex) { +float Rig::initJointStates(QVector states, glm::mat4 parentTransform) { _jointStates = states; - _neckJointIndex = neckJointIndex; initJointTransforms(parentTransform); int numStates = _jointStates.size(); @@ -142,8 +141,6 @@ float Rig::initJointStates(QVector states, glm::mat4 parentTransform _jointStates[i].slaveVisibleTransform(); } - initHeadBones(); - return radius; } @@ -195,8 +192,7 @@ JointState Rig::getJointState(int jointIndex) const { if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return JointState(); } - // return _jointStates[jointIndex]; - return maybeCauterizeHead(jointIndex); + return _jointStates[jointIndex]; } bool Rig::getJointStateRotation(int index, glm::quat& rotation) const { @@ -270,8 +266,7 @@ bool Rig::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, return false; } // position is in world-frame - // position = translation + rotation * _jointStates[jointIndex].getPosition(); - position = translation + rotation * maybeCauterizeHead(jointIndex).getPosition(); + position = translation + rotation * _jointStates[jointIndex].getPosition(); return true; } @@ -280,7 +275,7 @@ bool Rig::getJointPosition(int jointIndex, glm::vec3& position) const { return false; } // position is in model-frame - position = extractTranslation(maybeCauterizeHead(jointIndex).getTransform()); + position = extractTranslation(_jointStates[jointIndex].getTransform()); return true; } @@ -288,7 +283,7 @@ bool Rig::getJointRotationInWorldFrame(int jointIndex, glm::quat& result, const if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return false; } - result = rotation * maybeCauterizeHead(jointIndex).getRotation(); + result = rotation * _jointStates[jointIndex].getRotation(); return true; } @@ -296,7 +291,7 @@ bool Rig::getJointRotation(int jointIndex, glm::quat& rotation) const { if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return false; } - rotation = maybeCauterizeHead(jointIndex).getRotation(); + rotation = _jointStates[jointIndex].getRotation(); return true; } @@ -304,7 +299,7 @@ bool Rig::getJointCombinedRotation(int jointIndex, glm::quat& result, const glm: if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return false; } - result = rotation * maybeCauterizeHead(jointIndex).getRotation(); + result = rotation * _jointStates[jointIndex].getRotation(); return true; } @@ -315,7 +310,7 @@ bool Rig::getVisibleJointPositionInWorldFrame(int jointIndex, glm::vec3& positio return false; } // position is in world-frame - position = translation + rotation * maybeCauterizeHead(jointIndex).getVisiblePosition(); + position = translation + rotation * _jointStates[jointIndex].getVisiblePosition(); return true; } @@ -323,7 +318,7 @@ bool Rig::getVisibleJointRotationInWorldFrame(int jointIndex, glm::quat& result, if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return false; } - result = rotation * maybeCauterizeHead(jointIndex).getVisibleRotation(); + result = rotation * _jointStates[jointIndex].getVisibleRotation(); return true; } @@ -331,14 +326,14 @@ glm::mat4 Rig::getJointTransform(int jointIndex) const { if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return glm::mat4(); } - return maybeCauterizeHead(jointIndex).getTransform(); + return _jointStates[jointIndex].getTransform(); } glm::mat4 Rig::getJointVisibleTransform(int jointIndex) const { if (jointIndex == -1 || jointIndex >= _jointStates.size()) { return glm::mat4(); } - return maybeCauterizeHead(jointIndex).getVisibleTransform(); + return _jointStates[jointIndex].getVisibleTransform(); } void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation) { @@ -624,7 +619,7 @@ glm::vec3 Rig::getJointDefaultTranslationInConstrainedFrame(int jointIndex) { if (jointIndex == -1 || _jointStates.isEmpty()) { return glm::vec3(); } - return maybeCauterizeHead(jointIndex).getDefaultTranslationInConstrainedFrame(); + return _jointStates[jointIndex].getDefaultTranslationInConstrainedFrame(); } glm::quat Rig::setJointRotationInConstrainedFrame(int jointIndex, glm::quat targetRotation, float priority, bool constrain) { @@ -669,53 +664,5 @@ glm::quat Rig::getJointDefaultRotationInParentFrame(int jointIndex) { if (jointIndex == -1 || _jointStates.isEmpty()) { return glm::quat(); } - return maybeCauterizeHead(jointIndex).getDefaultRotationInParentFrame(); -} - -void Rig::initHeadBones() { - if (_neckJointIndex == -1) { - return; - } - _headBones.clear(); - std::queue q; - q.push(_neckJointIndex); - _headBones.push_back(_neckJointIndex); - - // fbxJoints only hold links to parents not children, so we have to do a bit of extra work here. - while (q.size() > 0) { - int jointIndex = q.front(); - for (int i = 0; i < _jointStates.size(); i++) { - const FBXJoint& fbxJoint = _jointStates[i].getFBXJoint(); - if (jointIndex == fbxJoint.parentIndex) { - _headBones.push_back(i); - q.push(i); - } - } - q.pop(); - } -} - -JointState Rig::maybeCauterizeHead(int jointIndex) const { - // if (_headBones.contains(jointIndex)) { - // XXX fix this... make _headBones a hash? add a flag to JointState? - if (_neckJointIndex != -1 && - _isFirstPerson && - std::find(_headBones.begin(), _headBones.end(), jointIndex) != _headBones.end()) { - glm::vec4 trans = _jointStates[jointIndex].getTransform()[3]; - glm::vec4 zero(0, 0, 0, 0); - glm::mat4 newXform(zero, zero, zero, trans); - JointState jointStateCopy = _jointStates[jointIndex]; - jointStateCopy.setTransform(newXform); - jointStateCopy.setVisibleTransform(newXform); - return jointStateCopy; - } else { - return _jointStates[jointIndex]; - } -} - -void Rig::setFirstPerson(bool isFirstPerson) { - if (_isFirstPerson != isFirstPerson) { - _isFirstPerson = isFirstPerson; - _jointsAreDirty = true; - } + return _jointStates[jointIndex].getDefaultRotationInParentFrame(); } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 52d5866369..fe6bc82f35 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -75,7 +75,7 @@ public: float priority = 1.0f, bool loop = false, bool hold = false, float firstFrame = 0.0f, float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList(), bool startAutomatically = false); - float initJointStates(QVector states, glm::mat4 parentTransform, int neckJointIndex); + float initJointStates(QVector states, glm::mat4 parentTransform); bool jointStatesEmpty() { return _jointStates.isEmpty(); }; int getJointStateCount() const { return _jointStates.size(); } int indexOfJoint(const QString& jointName) ; @@ -131,10 +131,6 @@ public: virtual void updateJointState(int index, glm::mat4 parentTransform) = 0; virtual void updateFaceJointState(int index, glm::mat4 parentTransform) = 0; - virtual void setFirstPerson(bool isFirstPerson); - virtual bool getIsFirstPerson() const { return _isFirstPerson; } - - bool getJointsAreDirty() { return _jointsAreDirty; } void setEnableRig(bool isEnabled) { _enableRig = isEnabled; } protected: @@ -143,13 +139,6 @@ public: QList _animationHandles; QList _runningAnimations; - JointState maybeCauterizeHead(int jointIndex) const; - void initHeadBones(); - bool _isFirstPerson = false; - std::vector _headBones; - bool _jointsAreDirty = false; - int _neckJointIndex = -1; - bool _enableRig; bool _isWalking; bool _isTurning; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 92e49fdc55..803658a667 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -67,6 +67,7 @@ Model::Model(RigPointer rig, QObject* parent) : _snapModelToRegistrationPoint(false), _snappedToRegistrationPoint(false), _showTrueJointTransforms(true), + _cauterizeBones(false), _lodDistance(0.0f), _pupilDilation(0.0f), _url("http://invalid.com"), @@ -452,6 +453,7 @@ bool Model::updateGeometry() { foreach (const FBXMesh& mesh, fbxGeometry.meshes) { MeshState state; state.clusterMatrices.resize(mesh.clusters.size()); + state.cauterizedClusterMatrices.resize(mesh.clusters.size()); _meshStates.append(state); auto buffer = std::make_shared(); @@ -472,7 +474,7 @@ bool Model::updateGeometry() { void Model::initJointStates(QVector states) { const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - _boundingRadius = _rig->initJointStates(states, parentTransform, geometry.neckJointIndex); + _boundingRadius = _rig->initJointStates(states, parentTransform); } bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, @@ -1337,6 +1339,12 @@ void Model::simulateInternal(float deltaTime) { glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; updateRig(deltaTime, parentTransform); + glm::mat4 zeroScale(glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), + glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), + glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); + auto cauterizeMatrix = _rig->getJointTransform(geometry.neckJointIndex) * zeroScale; + glm::mat4 modelToWorld = glm::mat4_cast(_rotation); for (int i = 0; i < _meshStates.size(); i++) { MeshState& state = _meshStates[i]; @@ -1344,14 +1352,30 @@ void Model::simulateInternal(float deltaTime) { if (_showTrueJointTransforms) { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - state.clusterMatrices[j] = - modelToWorld * _rig->getJointTransform(cluster.jointIndex) * cluster.inverseBindMatrix; + auto jointMatrix =_rig->getJointTransform(cluster.jointIndex); + state.clusterMatrices[j] = modelToWorld * jointMatrix * cluster.inverseBindMatrix; + + // as an optimization, don't build cautrizedClusterMatrices if the boneSet is empty. + if (!_cauterizeBoneSet.empty()) { + if (_cauterizeBoneSet.find(cluster.jointIndex) != _cauterizeBoneSet.end()) { + jointMatrix = cauterizeMatrix; + } + state.cauterizedClusterMatrices[j] = modelToWorld * jointMatrix * cluster.inverseBindMatrix; + } } } else { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); - state.clusterMatrices[j] = - modelToWorld * _rig->getJointVisibleTransform(cluster.jointIndex) * cluster.inverseBindMatrix; + auto jointMatrix = _rig->getJointVisibleTransform(cluster.jointIndex); + state.clusterMatrices[j] = modelToWorld * jointMatrix * cluster.inverseBindMatrix; + + // as an optimization, don't build cautrizedClusterMatrices if the boneSet is empty. + if (!_cauterizeBoneSet.empty()) { + if (_cauterizeBoneSet.find(cluster.jointIndex) != _cauterizeBoneSet.end()) { + jointMatrix = cauterizeMatrix; + } + state.cauterizedClusterMatrices[j] = modelToWorld * jointMatrix * cluster.inverseBindMatrix; + } } } } @@ -1611,8 +1635,11 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran } if (isSkinned) { - batch._glUniformMatrix4fv(locations->clusterMatrices, state.clusterMatrices.size(), false, - (const float*)state.clusterMatrices.constData()); + const float* bones = (const float*)state.clusterMatrices.constData(); + if (_cauterizeBones) { + bones = (const float*)state.cauterizedClusterMatrices.constData(); + } + batch._glUniformMatrix4fv(locations->clusterMatrices, state.clusterMatrices.size(), false, bones); _transforms[0] = Transform(); _transforms[0].preTranslate(_translation); } else { diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 83527969b2..1dab3f37a1 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -172,6 +173,9 @@ public: /// \return true if joint exists bool getJointRotation(int jointIndex, glm::quat& rotation) const; + /// Returns the index of the parent of the indexed joint, or -1 if not found. + int getParentJointIndex(int jointIndex) const; + void inverseKinematics(int jointIndex, glm::vec3 position, const glm::quat& rotation, float priority); /// Returns the extents of the model in its bind pose. @@ -187,6 +191,12 @@ public: bool getIsScaledToFit() const { return _scaledToFit; } /// is model scaled to fit const glm::vec3& getScaleToFitDimensions() const { return _scaleToFitDimensions; } /// the dimensions model is scaled to + void setCauterizeBones(bool flag) { _cauterizeBones = flag; } + bool getCauterizeBones() const { return _cauterizeBones; } + + const std::unordered_set& getCauterizeBoneSet() const { return _cauterizeBoneSet; } + void setCauterizeBoneSet(const std::unordered_set& boneSet) { _cauterizeBoneSet = boneSet; } + protected: void setPupilDilation(float dilation) { _pupilDilation = dilation; } @@ -218,9 +228,6 @@ protected: /// Clear the joint states void clearJointState(int index); - /// Returns the index of the parent of the indexed joint, or -1 if not found. - int getParentJointIndex(int jointIndex) const; - /// Returns the index of the last free ancestor of the indexed joint, or -1 if not found. int getLastFreeJointIndex(int jointIndex) const; @@ -255,9 +262,12 @@ protected: class MeshState { public: QVector clusterMatrices; + QVector cauterizedClusterMatrices; }; QVector _meshStates; + std::unordered_set _cauterizeBoneSet; + bool _cauterizeBones; // returns 'true' if needs fullUpdate after geometry change bool updateGeometry(); From 77a12eb50ea7af97a46060e178c2d6479af0bdc0 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 28 Jul 2015 17:53:01 -0700 Subject: [PATCH 145/242] compile on linux with GLEW --- cmake/modules/FindGLEW.cmake | 14 +++++++ interface/CMakeLists.txt | 6 +++ interface/src/Stars.cpp | 10 ++--- interface/src/devices/OculusManager.cpp | 6 +-- interface/src/devices/TV3DManager.cpp | 9 +++-- interface/src/devices/TV3DManager.h | 1 + .../src/RenderablePolyVoxEntityItem.cpp | 2 +- .../src/RenderableWebEntityItem.cpp | 3 +- libraries/gpu/CMakeLists.txt | 7 +++- libraries/gpu/src/gpu/Batch.cpp | 6 +-- libraries/gpu/src/gpu/GLBackend.cpp | 3 +- libraries/gpu/src/gpu/GLBackendQuery.cpp | 1 - libraries/gpu/src/gpu/GLBackendShader.cpp | 1 - libraries/gpu/src/gpu/GLBackendShared.h | 5 ++- libraries/gpu/src/gpu/GPUConfig.h | 2 +- libraries/model/src/model/Material.h | 2 +- .../src/AmbientOcclusionEffect.cpp | 6 +-- .../src/DeferredLightingEffect.cpp | 38 +++++++++--------- libraries/render-utils/src/GeometryCache.cpp | 10 ++--- libraries/render-utils/src/Model.cpp | 8 ++-- .../render-utils/src/RenderDeferredTask.cpp | 4 +- libraries/render-utils/src/TextureCache.cpp | 7 ++-- libraries/render/src/render/DrawStatus.cpp | 7 ++-- libraries/render/src/render/DrawTask.cpp | 6 +-- tests/render-utils/src/main.cpp | 39 ++++++++++--------- 25 files changed, 109 insertions(+), 94 deletions(-) diff --git a/cmake/modules/FindGLEW.cmake b/cmake/modules/FindGLEW.cmake index e86db3fdac..b1789fb614 100644 --- a/cmake/modules/FindGLEW.cmake +++ b/cmake/modules/FindGLEW.cmake @@ -38,5 +38,19 @@ if (WIN32) find_package_handle_standard_args(GLEW DEFAULT_MSG GLEW_INCLUDE_DIRS GLEW_LIBRARIES GLEW_DLL_PATH) add_paths_to_fixup_libs(${GLEW_DLL_PATH}) +elseif (APPLE) +else () + find_path(GLEW_INCLUDE_DIR GL/glew.h) + find_library(GLEW_LIBRARY NAMES GLEW glew32 glew glew32s PATH_SUFFIXES lib64) + + set(GLEW_INCLUDE_DIRS ${GLEW_INCLUDE_DIR}) + set(GLEW_LIBRARIES ${GLEW_LIBRARY}) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(GLEW + REQUIRED_VARS GLEW_INCLUDE_DIR GLEW_LIBRARY) + + mark_as_advanced(GLEW_INCLUDE_DIR GLEW_LIBRARY) + endif () diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 0c44ac801f..6af3cfa08b 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -245,6 +245,12 @@ else (APPLE) endif () endif() + else (WIN32) + find_package(GLEW REQUIRED) + target_include_directories(${TARGET_NAME} PRIVATE ${GLEW_INCLUDE_DIRS}) + + target_link_libraries(${TARGET_NAME} ${GLEW_LIBRARIES}) + message(STATUS ${GLEW_LIBRARY}) endif() endif (APPLE) diff --git a/interface/src/Stars.cpp b/interface/src/Stars.cpp index ebfddee38c..7b612acb68 100644 --- a/interface/src/Stars.cpp +++ b/interface/src/Stars.cpp @@ -14,8 +14,6 @@ #include #include -#include -#include #include #include #include @@ -207,7 +205,7 @@ void Stars::render(RenderArgs* renderArgs, float alpha) { batch._glUniform1f(_timeSlot, secs); geometryCache->renderUnitCube(batch); - glHint(GL_POINT_SMOOTH_HINT, GL_NICEST); + //glHint(GL_POINT_SMOOTH_HINT, GL_NICEST); static const size_t VERTEX_STRIDE = sizeof(StarVertex); size_t offset = offsetof(StarVertex, position); @@ -217,9 +215,9 @@ void Stars::render(RenderArgs* renderArgs, float alpha) { // Render the stars batch.setPipeline(_starsPipeline); - batch._glEnable(GL_PROGRAM_POINT_SIZE_EXT); - batch._glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); - batch._glEnable(GL_POINT_SMOOTH); + //batch._glEnable(GL_PROGRAM_POINT_SIZE_EXT); + //batch._glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); + //batch._glEnable(GL_POINT_SMOOTH); batch.setInputFormat(streamFormat); batch.setInputBuffer(VERTICES_SLOT, posView); diff --git a/interface/src/devices/OculusManager.cpp b/interface/src/devices/OculusManager.cpp index c9c70b4417..16685df96f 100644 --- a/interface/src/devices/OculusManager.cpp +++ b/interface/src/devices/OculusManager.cpp @@ -11,16 +11,16 @@ // #include "OculusManager.h" -#include +#include #include #include +#include #include +#include #include #include -#include -#include #include #include diff --git a/interface/src/devices/TV3DManager.cpp b/interface/src/devices/TV3DManager.cpp index fefaf060bd..5dee5988c1 100644 --- a/interface/src/devices/TV3DManager.cpp +++ b/interface/src/devices/TV3DManager.cpp @@ -9,13 +9,14 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "TV3DManager.h" + #include #include -#include "gpu/GLBackend.h" -#include "Application.h" +#include -#include "TV3DManager.h" +#include "Application.h" #include "Menu.h" int TV3DManager::_screenWidth = 1; @@ -63,6 +64,7 @@ void TV3DManager::setFrustum(Camera& whichCamera) { } void TV3DManager::configureCamera(Camera& whichCamera, int screenWidth, int screenHeight) { +#ifdef THIS_CURRENTLY_BROKEN_WAITING_FOR_DISPLAY_PLUGINS if (screenHeight == 0) { screenHeight = 1; // prevent divide by 0 } @@ -72,6 +74,7 @@ void TV3DManager::configureCamera(Camera& whichCamera, int screenWidth, int scre setFrustum(whichCamera); glViewport (0, 0, _screenWidth, _screenHeight); // sets drawing viewport +#endif } void TV3DManager::display(RenderArgs* renderArgs, Camera& whichCamera) { diff --git a/interface/src/devices/TV3DManager.h b/interface/src/devices/TV3DManager.h index 330a4ee0ee..96ee79f7d1 100644 --- a/interface/src/devices/TV3DManager.h +++ b/interface/src/devices/TV3DManager.h @@ -17,6 +17,7 @@ #include class Camera; +class RenderArgs; struct eyeFrustum { double left; diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index d3ee312311..681702bb07 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -35,7 +35,7 @@ #include #include "model/Geometry.h" -#include "gpu/GLBackend.h" +#include "gpu/Context.h" #include "EntityTreeRenderer.h" #include "RenderablePolyVoxEntityItem.h" diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 7fa615073b..9e48164647 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -8,7 +8,6 @@ #include "RenderableWebEntityItem.h" -#include #include #include #include @@ -24,7 +23,7 @@ #include #include #include -#include +#include #include "EntityTreeRenderer.h" diff --git a/libraries/gpu/CMakeLists.txt b/libraries/gpu/CMakeLists.txt index 30949b83e1..eda168091e 100644 --- a/libraries/gpu/CMakeLists.txt +++ b/libraries/gpu/CMakeLists.txt @@ -31,13 +31,16 @@ elseif (WIN32) elseif (ANDROID) target_link_libraries(${TARGET_NAME} "-lGLESv3" "-lEGL") else () + find_package(GLEW REQUIRED) + target_include_directories(${TARGET_NAME} PUBLIC ${GLEW_INCLUDE_DIRS}) + find_package(OpenGL REQUIRED) if (${OPENGL_INCLUDE_DIR}) include_directories(SYSTEM "${OPENGL_INCLUDE_DIR}") endif () - target_link_libraries(${TARGET_NAME} "${OPENGL_LIBRARY}") + target_link_libraries(${TARGET_NAME} "${GLEW_LIBRARIES}" "${OPENGL_LIBRARY}") - target_include_directories(${TARGET_NAME} PUBLIC ${OPENGL_INCLUDE_DIR}) + # target_include_directories(${TARGET_NAME} PUBLIC ${OPENGL_INCLUDE_DIR}) endif (APPLE) diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index 4ac33d8f14..b7f3ef444c 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -9,11 +9,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "Batch.h" -#include "GPUConfig.h" - -#include #include +#include + +#include "GPUConfig.h" #if defined(NSIGHT_FOUND) diff --git a/libraries/gpu/src/gpu/GLBackend.cpp b/libraries/gpu/src/gpu/GLBackend.cpp index 3942af4f84..4bc8abadd0 100644 --- a/libraries/gpu/src/gpu/GLBackend.cpp +++ b/libraries/gpu/src/gpu/GLBackend.cpp @@ -8,9 +8,10 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "GLBackendShared.h" + #include #include "GPULogging.h" -#include "GLBackendShared.h" #include using namespace gpu; diff --git a/libraries/gpu/src/gpu/GLBackendQuery.cpp b/libraries/gpu/src/gpu/GLBackendQuery.cpp index 39db19dafd..297bdf9c40 100644 --- a/libraries/gpu/src/gpu/GLBackendQuery.cpp +++ b/libraries/gpu/src/gpu/GLBackendQuery.cpp @@ -8,7 +8,6 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "GPULogging.h" #include "GLBackendShared.h" diff --git a/libraries/gpu/src/gpu/GLBackendShader.cpp b/libraries/gpu/src/gpu/GLBackendShader.cpp index fd5bed2e5e..dccd035cb4 100755 --- a/libraries/gpu/src/gpu/GLBackendShader.cpp +++ b/libraries/gpu/src/gpu/GLBackendShader.cpp @@ -8,7 +8,6 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "GPULogging.h" #include "GLBackendShared.h" #include "Format.h" diff --git a/libraries/gpu/src/gpu/GLBackendShared.h b/libraries/gpu/src/gpu/GLBackendShared.h index 27f58fcbe3..75bef461f9 100644 --- a/libraries/gpu/src/gpu/GLBackendShared.h +++ b/libraries/gpu/src/gpu/GLBackendShared.h @@ -11,10 +11,11 @@ #ifndef hifi_gpu_GLBackend_Shared_h #define hifi_gpu_GLBackend_Shared_h -#include "GLBackend.h" - #include +#include "GPULogging.h" +#include "GLBackend.h" + #include "Batch.h" static const GLenum _primitiveToGLmode[gpu::NUM_PRIMITIVES] = { diff --git a/libraries/gpu/src/gpu/GPUConfig.h b/libraries/gpu/src/gpu/GPUConfig.h index 2e3149919c..5046221ad3 100644 --- a/libraries/gpu/src/gpu/GPUConfig.h +++ b/libraries/gpu/src/gpu/GPUConfig.h @@ -36,7 +36,7 @@ #else #include -#include +//#include //#include //#include diff --git a/libraries/model/src/model/Material.h b/libraries/model/src/model/Material.h index 392fd918a1..a1a17d29e9 100755 --- a/libraries/model/src/model/Material.h +++ b/libraries/model/src/model/Material.h @@ -13,13 +13,13 @@ #include #include +#include #include #include "gpu/Resource.h" #include "gpu/Texture.h" -#include namespace model { diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index f19fa6e18a..22e5a705e3 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -9,15 +9,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -// include this before QOpenGLFramebufferObject, which includes an earlier version of OpenGL -#include - -#include #include #include #include +#include #include "gpu/StandardShaderLib.h" #include "AmbientOcclusionEffect.h" @@ -176,7 +173,6 @@ void AmbientOcclusion::run(const render::SceneContextPointer& sceneContext, cons assert(renderContext->args); assert(renderContext->args->_viewFrustum); RenderArgs* args = renderContext->args; - auto& scene = sceneContext->_scene; gpu::Batch batch; diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index c14bbfcb1d..7563609026 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -12,12 +12,12 @@ #include "DeferredLightingEffect.h" #include -#include +#include +#include + #include #include #include -#include -#include #include "AbstractViewStateInterface.h" #include "GeometryCache.h" @@ -291,7 +291,7 @@ void DeferredLightingEffect::render(RenderArgs* args) { locations = &_directionalAmbientSphereLightCascadedShadowMapLocations; } batch.setPipeline(program); - batch._glUniform3fv(locations->shadowDistances, 1, (const GLfloat*) &_viewState->getShadowDistances()); + batch._glUniform3fv(locations->shadowDistances, 1, (const float*) &_viewState->getShadowDistances()); } else { if (useSkyboxCubemap) { @@ -325,7 +325,7 @@ void DeferredLightingEffect::render(RenderArgs* args) { sh = (*_skybox->getCubemap()->getIrradiance()); } for (int i =0; i ambientSphere + i, 1, (const GLfloat*) (&sh) + i * 4); + batch._glUniform4fv(locations->ambientSphere + i, 1, (const float*) (&sh) + i * 4); } } @@ -340,7 +340,7 @@ void DeferredLightingEffect::render(RenderArgs* args) { if (_atmosphere && (locations->atmosphereBufferUnit >= 0)) { batch.setUniformBuffer(locations->atmosphereBufferUnit, _atmosphere->getDataBuffer()); } - batch._glUniformMatrix4fv(locations->invViewMat, 1, false, reinterpret_cast< const GLfloat* >(&invViewMat)); + batch._glUniformMatrix4fv(locations->invViewMat, 1, false, reinterpret_cast< const float* >(&invViewMat)); } float left, right, bottom, top, nearVal, farVal; @@ -419,9 +419,9 @@ void DeferredLightingEffect::render(RenderArgs* args) { batch._glUniform2f(_pointLightLocations.depthTexCoordOffset, depthTexCoordOffsetS, depthTexCoordOffsetT); batch._glUniform2f(_pointLightLocations.depthTexCoordScale, depthTexCoordScaleS, depthTexCoordScaleT); - batch._glUniformMatrix4fv(_pointLightLocations.invViewMat, 1, false, reinterpret_cast< const GLfloat* >(&invViewMat)); + batch._glUniformMatrix4fv(_pointLightLocations.invViewMat, 1, false, reinterpret_cast< const float* >(&invViewMat)); - batch._glUniformMatrix4fv(_pointLightLocations.texcoordMat, 1, false, reinterpret_cast< const GLfloat* >(&texcoordMat)); + batch._glUniformMatrix4fv(_pointLightLocations.texcoordMat, 1, false, reinterpret_cast< const float* >(&texcoordMat)); for (auto lightID : _pointLights) { auto& light = _allocatedLights[lightID]; @@ -467,9 +467,9 @@ void DeferredLightingEffect::render(RenderArgs* args) { batch._glUniform2f(_spotLightLocations.depthTexCoordOffset, depthTexCoordOffsetS, depthTexCoordOffsetT); batch._glUniform2f(_spotLightLocations.depthTexCoordScale, depthTexCoordScaleS, depthTexCoordScaleT); - batch._glUniformMatrix4fv(_spotLightLocations.invViewMat, 1, false, reinterpret_cast< const GLfloat* >(&invViewMat)); + batch._glUniformMatrix4fv(_spotLightLocations.invViewMat, 1, false, reinterpret_cast< const float* >(&invViewMat)); - batch._glUniformMatrix4fv(_spotLightLocations.texcoordMat, 1, false, reinterpret_cast< const GLfloat* >(&texcoordMat)); + batch._glUniformMatrix4fv(_spotLightLocations.texcoordMat, 1, false, reinterpret_cast< const float* >(&texcoordMat)); for (auto lightID : _spotLights) { auto light = _allocatedLights[lightID]; @@ -489,7 +489,7 @@ void DeferredLightingEffect::render(RenderArgs* args) { if ((eyeHalfPlaneDistance > -nearRadius) && (glm::distance(eyePoint, glm::vec3(light->getPosition())) < expandedRadius + nearRadius)) { coneParam.w = 0.0f; - batch._glUniform4fv(_spotLightLocations.coneParam, 1, reinterpret_cast< const GLfloat* >(&coneParam)); + batch._glUniform4fv(_spotLightLocations.coneParam, 1, reinterpret_cast< const float* >(&coneParam)); Transform model; model.setTranslation(glm::vec3(0.0f, 0.0f, -1.0f)); @@ -509,7 +509,7 @@ void DeferredLightingEffect::render(RenderArgs* args) { batch.setViewTransform(viewMat); } else { coneParam.w = 1.0f; - batch._glUniform4fv(_spotLightLocations.coneParam, 1, reinterpret_cast< const GLfloat* >(&coneParam)); + batch._glUniform4fv(_spotLightLocations.coneParam, 1, reinterpret_cast< const float* >(&coneParam)); Transform model; model.setTranslation(light->getPosition()); @@ -595,9 +595,9 @@ void DeferredLightingEffect::loadLightProgram(const char* vertSource, const char slotBindings.insert(gpu::Shader::Binding(std::string("depthMap"), 3)); slotBindings.insert(gpu::Shader::Binding(std::string("shadowMap"), 4)); slotBindings.insert(gpu::Shader::Binding(std::string("skyboxMap"), 5)); - const GLint LIGHT_GPU_SLOT = 3; + const int LIGHT_GPU_SLOT = 3; slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), LIGHT_GPU_SLOT)); - const GLint ATMOSPHERE_GPU_SLOT = 4; + const int ATMOSPHERE_GPU_SLOT = 4; slotBindings.insert(gpu::Shader::Binding(std::string("atmosphereBufferUnit"), ATMOSPHERE_GPU_SLOT)); gpu::Shader::makeProgram(*program, slotBindings); @@ -677,10 +677,10 @@ model::MeshPointer DeferredLightingEffect::getSpotLightMesh() { int ringFloatOffset = slices * 3; - GLfloat* vertexData = new GLfloat[verticesSize]; - GLfloat* vertexRing0 = vertexData; - GLfloat* vertexRing1 = vertexRing0 + ringFloatOffset; - GLfloat* vertexRing2 = vertexRing1 + ringFloatOffset; + float* vertexData = new float[verticesSize]; + float* vertexRing0 = vertexData; + float* vertexRing1 = vertexRing0 + ringFloatOffset; + float* vertexRing2 = vertexRing1 + ringFloatOffset; for (int i = 0; i < slices; i++) { float theta = TWO_PI * i / slices; @@ -746,7 +746,7 @@ model::MeshPointer DeferredLightingEffect::getSpotLightMesh() { *(index++) = capVertex; } - _spotLightMesh->setIndexBuffer(gpu::BufferView(new gpu::Buffer(sizeof(GLushort) * indices, (gpu::Byte*) indexData), gpu::Element::INDEX_UINT16)); + _spotLightMesh->setIndexBuffer(gpu::BufferView(new gpu::Buffer(sizeof(unsigned short) * indices, (gpu::Byte*) indexData), gpu::Element::INDEX_UINT16)); delete[] indexData; model::Mesh::Part part(0, indices, 0, model::Mesh::TRIANGLES); diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 8550f8d8b6..b873b35b9c 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -9,22 +9,22 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -// include this before QOpenGLBuffer, which includes an earlier version of OpenGL +#include "GeometryCache.h" + #include #include #include #include -#include -#include - #include #include +#include +#include + #include "TextureCache.h" #include "RenderUtilsLogging.h" -#include "GeometryCache.h" #include "standardTransformPNTC_vert.h" #include "standardDrawTexture_frag.h" diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 67eb85edfc..1e0e04c776 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -18,20 +18,18 @@ #include #include -#include -#include #include #include -#include "PhysicsEntity.h" #include #include #include +#include +#include #include "AbstractViewStateInterface.h" #include "AnimationHandle.h" #include "DeferredLightingEffect.h" #include "Model.h" -#include "RenderUtilsLogging.h" #include "model_vert.h" #include "model_shadow_vert.h" @@ -96,7 +94,7 @@ Model::~Model() { } Model::RenderPipelineLib Model::_renderPipelineLib; -const GLint MATERIAL_GPU_SLOT = 3; +const int MATERIAL_GPU_SLOT = 3; void Model::RenderPipelineLib::addRenderPipeline(Model::RenderKey key, gpu::ShaderPointer& vertexShader, diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index cf9b455059..b4863a4764 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -10,12 +10,10 @@ // #include "RenderDeferredTask.h" -#include -#include -#include #include #include #include +#include #include "FramebufferCache.h" #include "DeferredLightingEffect.h" diff --git a/libraries/render-utils/src/TextureCache.cpp b/libraries/render-utils/src/TextureCache.cpp index 1a6ea97b64..d6a9bf5b36 100644 --- a/libraries/render-utils/src/TextureCache.cpp +++ b/libraries/render-utils/src/TextureCache.cpp @@ -16,16 +16,15 @@ #include #include -#include -#include -#include - #include #include #include #include #include +#include + + #include "RenderUtilsLogging.h" diff --git a/libraries/render/src/render/DrawStatus.cpp b/libraries/render/src/render/DrawStatus.cpp index f84d212112..f50f517d7d 100644 --- a/libraries/render/src/render/DrawStatus.cpp +++ b/libraries/render/src/render/DrawStatus.cpp @@ -15,15 +15,14 @@ #include #include +#include +#include #include #include #include #include -#include -#include - #include "drawItemBounds_vert.h" #include "drawItemBounds_frag.h" #include "drawItemStatus_vert.h" @@ -171,4 +170,4 @@ void DrawStatus::run(const SceneContextPointer& sceneContext, const RenderContex args->_context->syncCache(); renderContext->args->_context->syncCache(); args->_context->render((batch)); -} \ No newline at end of file +} diff --git a/libraries/render/src/render/DrawTask.cpp b/libraries/render/src/render/DrawTask.cpp index 0e3eba0b53..d29420fdfd 100755 --- a/libraries/render/src/render/DrawTask.cpp +++ b/libraries/render/src/render/DrawTask.cpp @@ -14,12 +14,12 @@ #include #include -#include -#include -#include #include #include #include +#include +#include +#include using namespace render; diff --git a/tests/render-utils/src/main.cpp b/tests/render-utils/src/main.cpp index 3b7eb18368..ab01d333ac 100644 --- a/tests/render-utils/src/main.cpp +++ b/tests/render-utils/src/main.cpp @@ -8,30 +8,31 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - #include #include + #include -#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include class RateCounter { std::vector times; From 475d069185ffe68e2979a43856d082adb747b04c Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 28 Jul 2015 18:12:10 -0700 Subject: [PATCH 146/242] fix rendering on linux --- libraries/gpu/src/gpu/Config.slh | 7 +++---- libraries/gpu/src/gpu/GPUConfig.h | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/libraries/gpu/src/gpu/Config.slh b/libraries/gpu/src/gpu/Config.slh index 76be161822..f24b54e5d5 100644 --- a/libraries/gpu/src/gpu/Config.slh +++ b/libraries/gpu/src/gpu/Config.slh @@ -21,10 +21,9 @@ <@def VERSION_HEADER #version 120 #extension GL_EXT_gpu_shader4 : enable@> <@else@> - <@def GPU_FEATURE_PROFILE GPU_LEGACY@> - <@def GPU_TRANSFORM_PROFILE GPU_LEGACY@> - <@def VERSION_HEADER #version 120 -#extension GL_EXT_gpu_shader4 : enable@> + <@def GPU_FEATURE_PROFILE GPU_CORE@> + <@def GPU_TRANSFORM_PROFILE GPU_CORE@> + <@def VERSION_HEADER #version 430 compatibility@> <@endif@> <@endif@> diff --git a/libraries/gpu/src/gpu/GPUConfig.h b/libraries/gpu/src/gpu/GPUConfig.h index 5046221ad3..d9b6d18894 100644 --- a/libraries/gpu/src/gpu/GPUConfig.h +++ b/libraries/gpu/src/gpu/GPUConfig.h @@ -40,8 +40,8 @@ //#include //#include -#define GPU_FEATURE_PROFILE GPU_LEGACY -#define GPU_TRANSFORM_PROFILE GPU_LEGACY +#define GPU_FEATURE_PROFILE GPU_CORE +#define GPU_TRANSFORM_PROFILE GPU_CORE #endif From 044ea2ace56303e1a5101dc282db4c1459f561ce Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 28 Jul 2015 19:31:43 -0700 Subject: [PATCH 147/242] Changed mirror position to not be achored to the head. But instead it's anchored to the "default" eye position, which is where the eyes are in the model, before IK or animations occur. --- interface/src/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3f185af5a1..967880ca8d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3381,7 +3381,7 @@ void Application::renderRearViewMirror(RenderArgs* renderArgs, const QRect& regi // This was removed in commit 71e59cfa88c6563749594e25494102fe01db38e9 but could be further // investigated in order to adapt the technique while fixing the head rendering issue, // but the complexity of the hack suggests that a better approach - _mirrorCamera.setPosition(_myAvatar->getHead()->getEyePosition() + + _mirrorCamera.setPosition(_myAvatar->getDefaultEyePosition() + _myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_DISTANCE * _myAvatar->getScale()); } _mirrorCamera.setProjection(glm::perspective(glm::radians(fov), aspect, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); From 1d29fb9d7f9b2c482a352881cb6595904671e657 Mon Sep 17 00:00:00 2001 From: Marcel Verhagen Date: Wed, 29 Jul 2015 14:56:24 +0200 Subject: [PATCH 148/242] replace tabs with whitespaces. --- interface/src/devices/3Dconnexion.cpp | 38 +++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/interface/src/devices/3Dconnexion.cpp b/interface/src/devices/3Dconnexion.cpp index 111e2d5991..ef66e6660b 100755 --- a/interface/src/devices/3Dconnexion.cpp +++ b/interface/src/devices/3Dconnexion.cpp @@ -165,11 +165,11 @@ static ConnexionClient* gMouseInput = 0; void ConnexionClient::toggleConnexion(bool shouldEnable) { - ConnexionData& connexiondata = ConnexionData::getInstance(); - if (shouldEnable && connexiondata.getDeviceID() == 0) { + ConnexionData& connexiondata = ConnexionData::getInstance(); + if (shouldEnable && connexiondata.getDeviceID() == 0) { ConnexionClient::init(); } - if (!shouldEnable && connexiondata.getDeviceID() != 0) { + if (!shouldEnable && connexiondata.getDeviceID() != 0) { ConnexionClient::destroy(); } @@ -177,24 +177,24 @@ void ConnexionClient::toggleConnexion(bool shouldEnable) void ConnexionClient::init() { if (Menu::getInstance()->isOptionChecked(MenuOption::Connexion)) { - ConnexionClient& cclient = ConnexionClient::getInstance(); - cclient.fLast3dmouseInputTime = 0; + ConnexionClient& cclient = ConnexionClient::getInstance(); + cclient.fLast3dmouseInputTime = 0; - cclient.InitializeRawInput(GetActiveWindow()); + cclient.InitializeRawInput(GetActiveWindow()); - gMouseInput = &cclient; + gMouseInput = &cclient; - QAbstractEventDispatcher::instance()->installNativeEventFilter(&cclient); + QAbstractEventDispatcher::instance()->installNativeEventFilter(&cclient); } } void ConnexionClient::destroy() { - ConnexionClient& cclient = ConnexionClient::getInstance(); - QAbstractEventDispatcher::instance()->removeNativeEventFilter(&cclient); - ConnexionData& connexiondata = ConnexionData::getInstance(); - int deviceid = connexiondata.getDeviceID(); - connexiondata.setDeviceID(0); - Application::getUserInputMapper()->removeDevice(deviceid); + ConnexionClient& cclient = ConnexionClient::getInstance(); + QAbstractEventDispatcher::instance()->removeNativeEventFilter(&cclient); + ConnexionData& connexiondata = ConnexionData::getInstance(); + int deviceid = connexiondata.getDeviceID(); + connexiondata.setDeviceID(0); + Application::getUserInputMapper()->removeDevice(deviceid); } #define LOGITECH_VENDOR_ID 0x46d @@ -305,9 +305,9 @@ bool ConnexionClient::RawInputEventFilter(void* msg, long* result) { connexiondata.assignDefaultInputMapping(*Application::getUserInputMapper()); UserActivityLogger::getInstance().connectedDevice("controller", "3Dconnexion"); } else if (!ConnexionClient::Is3dmouseAttached() && connexiondata.getDeviceID() != 0) { - int deviceid = connexiondata.getDeviceID(); - connexiondata.setDeviceID(0); - Application::getUserInputMapper()->removeDevice(deviceid); + int deviceid = connexiondata.getDeviceID(); + connexiondata.setDeviceID(0); + Application::getUserInputMapper()->removeDevice(deviceid); } if (!ConnexionClient::Is3dmouseAttached()) { @@ -332,8 +332,8 @@ ConnexionClient::ConnexionClient() { } ConnexionClient::~ConnexionClient() { - ConnexionClient& cclient = ConnexionClient::getInstance(); - QAbstractEventDispatcher::instance()->removeNativeEventFilter(&cclient); + ConnexionClient& cclient = ConnexionClient::getInstance(); + QAbstractEventDispatcher::instance()->removeNativeEventFilter(&cclient); } // Access the mouse parameters structure From 7325e6a7b7fcfa280ab3ed4534f84db804402880 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 29 Jul 2015 14:57:06 +0200 Subject: [PATCH 149/242] Send ping requests to the nodes (AvatarMixer, AudioMixer, EntityServer) that the Assignment agent connected with to keep the connections alive. --- assignment-client/src/Agent.cpp | 27 ++++++++++++++++++++++++++- assignment-client/src/Agent.h | 2 ++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index da588bc316..77b47f4d7c 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -107,6 +107,7 @@ void Agent::handleAudioPacket(QSharedPointer packet) { } const QString AGENT_LOGGING_NAME = "agent"; +const int PING_INTERVAL = 1000; void Agent::run() { ThreadedAssignment::commonInit(AGENT_LOGGING_NAME, NodeType::Agent); @@ -118,6 +119,10 @@ void Agent::run() { << NodeType::EntityServer ); + _pingTimer = new QTimer(this); + connect(_pingTimer, SIGNAL(timeout()), SLOT(sendPingRequests())); + _pingTimer->start(PING_INTERVAL); + // figure out the URL for the script for this agent assignment QUrl scriptURL; if (_payload.isEmpty()) { @@ -193,7 +198,27 @@ void Agent::run() { void Agent::aboutToFinish() { _scriptEngine.stop(); - + + _pingTimer->stop(); + delete _pingTimer; + // our entity tree is going to go away so tell that to the EntityScriptingInterface DependencyManager::get()->setEntityTree(NULL); } + +void Agent::sendPingRequests() { + auto nodeList = DependencyManager::get(); + + nodeList->eachMatchingNode([](const SharedNodePointer& node)->bool { + switch (node->getType()) { + case NodeType::AvatarMixer: + case NodeType::AudioMixer: + case NodeType::EntityServer: + return true; + default: + return false; + } + }, [nodeList](const SharedNodePointer& node) { + nodeList->sendPacket(nodeList->constructPingPacket(), *node); + }); +} diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 241e14439c..4c207e59aa 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -58,11 +58,13 @@ private slots: void handleAudioPacket(QSharedPointer packet); void handleOctreePacket(QSharedPointer packet, SharedNodePointer senderNode); void handleJurisdictionPacket(QSharedPointer packet, SharedNodePointer senderNode); + void sendPingRequests(); private: ScriptEngine _scriptEngine; EntityEditPacketSender _entityEditSender; EntityTreeHeadlessViewer _entityViewer; + QTimer* _pingTimer; MixedAudioStream _receivedAudioStream; float _lastReceivedAudioLoudness; From 0a5ada3c09dfe906031d7f2b4b3dc291f85dd409 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 29 Jul 2015 09:02:24 -0700 Subject: [PATCH 150/242] remove legacy shapes --- interface/src/avatar/Avatar.h | 3 - libraries/avatars/src/AvatarData.h | 5 - libraries/entities/src/EntityItem.h | 1 - .../entities/src/EntityScriptingInterface.h | 1 - libraries/entities/src/TextEntityItem.cpp | 47 +- libraries/entities/src/WebEntityItem.cpp | 49 +- libraries/fbx/src/OBJReader.cpp | 1 - libraries/octree/src/Octree.cpp | 8 - libraries/octree/src/OctreeElement.cpp | 1 - libraries/octree/src/OctreeElement.h | 1 - libraries/render-utils/src/Model.cpp | 1 - libraries/render-utils/src/PhysicsEntity.h | 3 - libraries/script-engine/src/ScriptEngine.cpp | 3 - libraries/shared/src/AACubeShape.cpp | 76 - libraries/shared/src/AACubeShape.h | 50 - libraries/shared/src/CapsuleShape.cpp | 218 -- libraries/shared/src/CapsuleShape.h | 62 - libraries/shared/src/CollisionInfo.cpp | 110 - libraries/shared/src/CollisionInfo.h | 101 - libraries/shared/src/GeometryUtil.cpp | 1 - libraries/shared/src/ListShape.cpp | 90 - libraries/shared/src/ListShape.h | 72 - libraries/shared/src/PlaneShape.cpp | 78 - libraries/shared/src/PlaneShape.h | 30 - libraries/shared/src/RayIntersectionInfo.h | 37 - libraries/shared/src/Shape.h | 144 -- libraries/shared/src/ShapeCollider.cpp | 1211 ----------- libraries/shared/src/ShapeCollider.h | 156 -- libraries/shared/src/SphereShape.cpp | 51 - libraries/shared/src/SphereShape.h | 63 - libraries/shared/src/StreamUtils.cpp | 32 - libraries/shared/src/StreamUtils.h | 10 - tests/physics/src/CollisionInfoTests.cpp | 70 - tests/physics/src/CollisionInfoTests.h | 29 - tests/physics/src/ShapeColliderTests.cpp | 1936 ----------------- tests/physics/src/ShapeColliderTests.h | 60 - 36 files changed, 15 insertions(+), 4796 deletions(-) delete mode 100644 libraries/shared/src/AACubeShape.cpp delete mode 100644 libraries/shared/src/AACubeShape.h delete mode 100644 libraries/shared/src/CapsuleShape.cpp delete mode 100644 libraries/shared/src/CapsuleShape.h delete mode 100644 libraries/shared/src/CollisionInfo.cpp delete mode 100644 libraries/shared/src/CollisionInfo.h delete mode 100644 libraries/shared/src/ListShape.cpp delete mode 100644 libraries/shared/src/ListShape.h delete mode 100644 libraries/shared/src/PlaneShape.cpp delete mode 100644 libraries/shared/src/PlaneShape.h delete mode 100644 libraries/shared/src/RayIntersectionInfo.h delete mode 100644 libraries/shared/src/Shape.h delete mode 100644 libraries/shared/src/ShapeCollider.cpp delete mode 100644 libraries/shared/src/ShapeCollider.h delete mode 100644 libraries/shared/src/SphereShape.cpp delete mode 100644 libraries/shared/src/SphereShape.h delete mode 100644 tests/physics/src/CollisionInfoTests.cpp delete mode 100644 tests/physics/src/CollisionInfoTests.h delete mode 100644 tests/physics/src/ShapeColliderTests.cpp delete mode 100644 tests/physics/src/ShapeColliderTests.h diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index dcf37c9a1e..ea4c95aca2 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -168,9 +168,6 @@ public: friend class AvatarManager; -signals: - void collisionWithAvatar(const QUuid& myUUID, const QUuid& theirUUID, const CollisionInfo& collision); - protected: SkeletonModel _skeletonModel; glm::vec3 _skeletonOffset; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 60c643eff9..919004aa84 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -46,7 +46,6 @@ typedef unsigned long long quint64; #include #include -#include #include #include #include @@ -257,10 +256,6 @@ public: const HeadData* getHeadData() const { return _headData; } const HandData* getHandData() const { return _handData; } - virtual bool findSphereCollisions(const glm::vec3& particleCenter, float particleRadius, CollisionList& collisions) { - return false; - } - bool hasIdentityChangedAfterParsing(NLPacket& packet); QByteArray identityByteArray(); diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index b698a5c511..57f8883cea 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -18,7 +18,6 @@ #include #include // for Animation, AnimationCache, and AnimationPointer classes -#include #include // for EncodeBitstreamParams class #include // for OctreeElement::AppendState #include diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index c232437757..d3710c6ccc 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -16,7 +16,6 @@ #include -#include #include #include #include diff --git a/libraries/entities/src/TextEntityItem.cpp b/libraries/entities/src/TextEntityItem.cpp index a851d025fc..3ab15ba6cb 100644 --- a/libraries/entities/src/TextEntityItem.cpp +++ b/libraries/entities/src/TextEntityItem.cpp @@ -15,7 +15,7 @@ #include #include -#include +#include #include "EntityTree.h" #include "EntityTreeElement.h" @@ -128,46 +128,13 @@ void TextEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBits } - bool TextEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElement*& element, float& distance, BoxFace& face, void** intersectedObject, bool precisionPicking) const { - - RayIntersectionInfo rayInfo; - rayInfo._rayStart = origin; - rayInfo._rayDirection = direction; - rayInfo._rayLength = std::numeric_limits::max(); - - PlaneShape plane; - - const glm::vec3 UNROTATED_NORMAL(0.0f, 0.0f, -1.0f); - glm::vec3 normal = getRotation() * UNROTATED_NORMAL; - plane.setNormal(normal); - plane.setPoint(getPosition()); // the position is definitely a point on our plane - - bool intersects = plane.findRayIntersection(rayInfo); - - if (intersects) { - glm::vec3 hitAt = origin + (direction * rayInfo._hitDistance); - // now we know the point the ray hit our plane - - glm::mat4 rotation = glm::mat4_cast(getRotation()); - glm::mat4 translation = glm::translate(getPosition()); - glm::mat4 entityToWorldMatrix = translation * rotation; - glm::mat4 worldToEntityMatrix = glm::inverse(entityToWorldMatrix); - - glm::vec3 dimensions = getDimensions(); - glm::vec3 registrationPoint = getRegistrationPoint(); - glm::vec3 corner = -(dimensions * registrationPoint); - AABox entityFrameBox(corner, dimensions); - - glm::vec3 entityFrameHitAt = glm::vec3(worldToEntityMatrix * glm::vec4(hitAt, 1.0f)); - - intersects = entityFrameBox.contains(entityFrameHitAt); - } - - if (intersects) { - distance = rayInfo._hitDistance; - } - return intersects; + glm::vec3 dimensions = getDimensions(); + glm::vec2 xyDimensions(dimensions.x, dimensions.y); + glm::quat rotation = getRotation(); + glm::vec3 position = getPosition() + rotation * + (dimensions * (getRegistrationPoint() - ENTITY_ITEM_DEFAULT_REGISTRATION_POINT)); + return findRayRectangleIntersection(origin, direction, rotation, position, xyDimensions, distance); } diff --git a/libraries/entities/src/WebEntityItem.cpp b/libraries/entities/src/WebEntityItem.cpp index 2cb233bbee..c3dc757199 100644 --- a/libraries/entities/src/WebEntityItem.cpp +++ b/libraries/entities/src/WebEntityItem.cpp @@ -13,7 +13,7 @@ #include #include -#include +#include #include "EntityTree.h" #include "EntityTreeElement.h" @@ -98,50 +98,17 @@ void WebEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBitst APPEND_ENTITY_PROPERTY(PROP_SOURCE_URL, _sourceUrl); } - bool WebEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElement*& element, float& distance, BoxFace& face, void** intersectedObject, bool precisionPicking) const { - - RayIntersectionInfo rayInfo; - rayInfo._rayStart = origin; - rayInfo._rayDirection = direction; - rayInfo._rayLength = std::numeric_limits::max(); - - PlaneShape plane; - - const glm::vec3 UNROTATED_NORMAL(0.0f, 0.0f, -1.0f); - glm::vec3 normal = getRotation() * UNROTATED_NORMAL; - plane.setNormal(normal); - plane.setPoint(getPosition()); // the position is definitely a point on our plane - - bool intersects = plane.findRayIntersection(rayInfo); - - if (intersects) { - glm::vec3 hitAt = origin + (direction * rayInfo._hitDistance); - // now we know the point the ray hit our plane - - glm::mat4 rotation = glm::mat4_cast(getRotation()); - glm::mat4 translation = glm::translate(getPosition()); - glm::mat4 entityToWorldMatrix = translation * rotation; - glm::mat4 worldToEntityMatrix = glm::inverse(entityToWorldMatrix); - - glm::vec3 dimensions = getDimensions(); - glm::vec3 registrationPoint = getRegistrationPoint(); - glm::vec3 corner = -(dimensions * registrationPoint); - AABox entityFrameBox(corner, dimensions); - - glm::vec3 entityFrameHitAt = glm::vec3(worldToEntityMatrix * glm::vec4(hitAt, 1.0f)); - - intersects = entityFrameBox.contains(entityFrameHitAt); - } - - if (intersects) { - distance = rayInfo._hitDistance; - } - return intersects; + glm::vec3 dimensions = getDimensions(); + glm::vec2 xyDimensions(dimensions.x, dimensions.y); + glm::quat rotation = getRotation(); + glm::vec3 position = getPosition() + rotation * + (dimensions * (getRegistrationPoint() - ENTITY_ITEM_DEFAULT_REGISTRATION_POINT)); + return findRayRectangleIntersection(origin, direction, rotation, position, xyDimensions, distance); } - + void WebEntityItem::setSourceUrl(const QString& value) { if (_sourceUrl != value) { _sourceUrl = value; diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 35ba437745..693c1f64d3 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -23,7 +23,6 @@ #include #include "FBXReader.h" #include "OBJReader.h" -#include "Shape.h" #include "ModelFormatLogging.h" diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index 203ff2b072..48f5f99906 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -38,7 +38,6 @@ #include #include #include -#include #include #include "CoverageMap.h" @@ -791,13 +790,6 @@ public: bool found; }; -class ShapeArgs { -public: - const Shape* shape; - CollisionList& collisions; - bool found; -}; - class ContentArgs { public: AACube cube; diff --git a/libraries/octree/src/OctreeElement.cpp b/libraries/octree/src/OctreeElement.cpp index e11a11fc8e..23034e636a 100644 --- a/libraries/octree/src/OctreeElement.cpp +++ b/libraries/octree/src/OctreeElement.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include "AACube.h" #include "OctalCode.h" diff --git a/libraries/octree/src/OctreeElement.h b/libraries/octree/src/OctreeElement.h index 830655242f..d2f3f7d3aa 100644 --- a/libraries/octree/src/OctreeElement.h +++ b/libraries/octree/src/OctreeElement.h @@ -24,7 +24,6 @@ #include "ViewFrustum.h" #include "OctreeConstants.h" -class CollisionList; class EncodeBitstreamParams; class Octree; class OctreeElement; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index c28dbf4247..d352da55ee 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -16,7 +16,6 @@ #include #include -#include #include #include #include diff --git a/libraries/render-utils/src/PhysicsEntity.h b/libraries/render-utils/src/PhysicsEntity.h index 3b527c7827..f36473af26 100644 --- a/libraries/render-utils/src/PhysicsEntity.h +++ b/libraries/render-utils/src/PhysicsEntity.h @@ -18,9 +18,6 @@ #include #include -#include -#include - class PhysicsEntity { public: diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index d5e727657c..cfd6cda56b 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -366,8 +365,6 @@ void ScriptEngine::init() { // constants globalObject().setProperty("TREE_SCALE", newVariant(QVariant(TREE_SCALE))); - globalObject().setProperty("COLLISION_GROUP_ENVIRONMENT", newVariant(QVariant(COLLISION_GROUP_ENVIRONMENT))); - globalObject().setProperty("COLLISION_GROUP_AVATARS", newVariant(QVariant(COLLISION_GROUP_AVATARS))); } QScriptValue ScriptEngine::registerGlobalObject(const QString& name, QObject* object) { diff --git a/libraries/shared/src/AACubeShape.cpp b/libraries/shared/src/AACubeShape.cpp deleted file mode 100644 index 5c40b3bb64..0000000000 --- a/libraries/shared/src/AACubeShape.cpp +++ /dev/null @@ -1,76 +0,0 @@ -// -// AACubeShape.cpp -// libraries/shared/src -// -// Created by Andrew Meadows on 2014.08.22 -// 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 "AACubeShape.h" -#include "NumericalConstants.h" // for SQUARE_ROOT_OF_3 - -glm::vec3 faceNormals[3] = { glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f) }; - -bool AACubeShape::findRayIntersection(RayIntersectionInfo& intersection) const { - // A = ray point - // B = cube center - glm::vec3 BA = _translation - intersection._rayStart; - - // check for ray intersection with cube's bounding sphere - // a = distance along line to closest approach to B - float a = glm::dot(intersection._rayDirection, BA); - // b2 = squared distance from cube center to point of closest approach - float b2 = glm::length2(a * intersection._rayDirection - BA); - // r = bounding radius of cube - float halfSide = 0.5f * _scale; - const float r = SQUARE_ROOT_OF_3 * halfSide; - if (b2 > r * r) { - // line doesn't hit cube's bounding sphere - return false; - } - - // check for tuncated/short ray - // maxLength = maximum possible distance between rayStart and center of cube - const float maxLength = glm::min(intersection._rayLength, intersection._hitDistance) + r; - if (a * a + b2 > maxLength * maxLength) { - // ray is not long enough to reach cube's bounding sphere - // NOTE: we don't fall in here when ray's length if FLT_MAX because maxLength^2 will be inf or nan - return false; - } - - // the trivial checks have been exhausted, so must trace to each face - bool hit = false; - for (int i = 0; i < 3; ++i) { - for (float sign = -1.0f; sign < 2.0f; sign += 2.0f) { - glm::vec3 faceNormal = sign * faceNormals[i]; - float rayDotPlane = glm::dot(intersection._rayDirection, faceNormal); - if (glm::abs(rayDotPlane) > EPSILON) { - float distanceToFace = (halfSide + glm::dot(BA, faceNormal)) / rayDotPlane; - if (distanceToFace >= 0.0f) { - glm::vec3 point = distanceToFace * intersection._rayDirection - BA; - int j = (i + 1) % 3; - int k = (i + 2) % 3; - glm::vec3 secondNormal = faceNormals[j]; - glm::vec3 thirdNormal = faceNormals[k]; - if (glm::abs(glm::dot(point, secondNormal)) > halfSide || - glm::abs(glm::dot(point, thirdNormal)) > halfSide) { - continue; - } - if (distanceToFace < intersection._hitDistance && distanceToFace < intersection._rayLength) { - intersection._hitDistance = distanceToFace; - intersection._hitNormal = faceNormal; - intersection._hitShape = const_cast(this); - hit = true; - } - } - } - } - } - return hit; -} diff --git a/libraries/shared/src/AACubeShape.h b/libraries/shared/src/AACubeShape.h deleted file mode 100644 index da7ba9d53f..0000000000 --- a/libraries/shared/src/AACubeShape.h +++ /dev/null @@ -1,50 +0,0 @@ -// -// AACubeShape.h -// libraries/shared/src -// -// Created by Andrew Meadows on 2014.08.22 -// 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_AACubeShape_h -#define hifi_AACubeShape_h - -#include -#include "Shape.h" - -class AACubeShape : public Shape { -public: - AACubeShape() : Shape(AACUBE_SHAPE), _scale(1.0f) { } - AACubeShape(float scale) : Shape(AACUBE_SHAPE), _scale(scale) { } - AACubeShape(float scale, const glm::vec3& position) : Shape(AACUBE_SHAPE, position), _scale(scale) { } - - virtual ~AACubeShape() { } - - float getScale() const { return _scale; } - void setScale(float scale) { _scale = scale; } - - bool findRayIntersection(RayIntersectionInfo& intersection) const; - - float getVolume() const { return _scale * _scale * _scale; } - virtual QDebug& dumpToDebug(QDebug& debugConext) const; - -protected: - float _scale; -}; - -inline QDebug& AACubeShape::dumpToDebug(QDebug& debugConext) const { - debugConext << "AACubeShape[ (" - << "type: " << getType() - << "position: " - << getTranslation().x << ", " << getTranslation().y << ", " << getTranslation().z - << "scale: " - << getScale() - << "]"; - - return debugConext; -} - -#endif // hifi_AACubeShape_h diff --git a/libraries/shared/src/CapsuleShape.cpp b/libraries/shared/src/CapsuleShape.cpp deleted file mode 100644 index ec71ebadbb..0000000000 --- a/libraries/shared/src/CapsuleShape.cpp +++ /dev/null @@ -1,218 +0,0 @@ -// -// CapsuleShape.cpp -// libraries/shared/src -// -// Created by Andrew Meadows on 02/20/2014. -// 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 "CapsuleShape.h" -#include "GeometryUtil.h" -#include "NumericalConstants.h" - -CapsuleShape::CapsuleShape() : Shape(CAPSULE_SHAPE), _radius(0.0f), _halfHeight(0.0f) {} - -CapsuleShape::CapsuleShape(float radius, float halfHeight) : Shape(CAPSULE_SHAPE), - _radius(radius), _halfHeight(halfHeight) { - updateBoundingRadius(); -} - -CapsuleShape::CapsuleShape(float radius, float halfHeight, const glm::vec3& position, const glm::quat& rotation) : - Shape(CAPSULE_SHAPE, position, rotation), _radius(radius), _halfHeight(halfHeight) { - updateBoundingRadius(); -} - -CapsuleShape::CapsuleShape(float radius, const glm::vec3& startPoint, const glm::vec3& endPoint) : - Shape(CAPSULE_SHAPE), _radius(radius), _halfHeight(0.0f) { - setEndPoints(startPoint, endPoint); -} - -/// \param[out] startPoint is the center of start cap -void CapsuleShape::getStartPoint(glm::vec3& startPoint) const { - startPoint = _translation - _rotation * glm::vec3(0.0f, _halfHeight, 0.0f); -} - -/// \param[out] endPoint is the center of the end cap -void CapsuleShape::getEndPoint(glm::vec3& endPoint) const { - endPoint = _translation + _rotation * glm::vec3(0.0f, _halfHeight, 0.0f); -} - -void CapsuleShape::computeNormalizedAxis(glm::vec3& axis) const { - // default axis of a capsule is along the yAxis - axis = _rotation * DEFAULT_CAPSULE_AXIS; -} - -void CapsuleShape::setRadius(float radius) { - _radius = radius; - updateBoundingRadius(); -} - -void CapsuleShape::setHalfHeight(float halfHeight) { - _halfHeight = halfHeight; - updateBoundingRadius(); -} - -void CapsuleShape::setRadiusAndHalfHeight(float radius, float halfHeight) { - _radius = radius; - _halfHeight = halfHeight; - updateBoundingRadius(); -} - -void CapsuleShape::setEndPoints(const glm::vec3& startPoint, const glm::vec3& endPoint) { - glm::vec3 axis = endPoint - startPoint; - _translation = 0.5f * (endPoint + startPoint); - float height = glm::length(axis); - if (height > EPSILON) { - _halfHeight = 0.5f * height; - axis /= height; - _rotation = computeNewRotation(axis); - } - updateBoundingRadius(); -} - -// helper -bool findRayIntersectionWithCap(const glm::vec3& sphereCenter, float sphereRadius, - const glm::vec3& capsuleCenter, RayIntersectionInfo& intersection) { - float r2 = sphereRadius * sphereRadius; - - // compute closest approach (CA) - float a = glm::dot(sphereCenter - intersection._rayStart, intersection._rayDirection); // a = distance from ray-start to CA - float b2 = glm::distance2(sphereCenter, intersection._rayStart + a * intersection._rayDirection); // b2 = squared distance from sphere-center to CA - if (b2 > r2) { - // ray does not hit sphere - return false; - } - float c = sqrtf(r2 - b2); // c = distance from CA to sphere surface along intersection._rayDirection - float d2 = glm::distance2(intersection._rayStart, sphereCenter); // d2 = squared distance from sphere-center to ray-start - float distance = FLT_MAX; - if (a < 0.0f) { - // ray points away from sphere-center - if (d2 > r2) { - // ray starts outside sphere - return false; - } - // ray starts inside sphere - distance = c + a; - } else if (d2 > r2) { - // ray starts outside sphere - distance = a - c; - } else { - // ray starts inside sphere - distance = a + c; - } - if (distance > 0.0f && distance < intersection._rayLength && distance < intersection._hitDistance) { - glm::vec3 sphereCenterToHitPoint = intersection._rayStart + distance * intersection._rayDirection - sphereCenter; - if (glm::dot(sphereCenterToHitPoint, sphereCenter - capsuleCenter) >= 0.0f) { - intersection._hitDistance = distance; - intersection._hitNormal = glm::normalize(sphereCenterToHitPoint); - return true; - } - } - return false; -} - -bool CapsuleShape::findRayIntersectionWithCaps(const glm::vec3& capsuleCenter, RayIntersectionInfo& intersection) const { - glm::vec3 capCenter; - getStartPoint(capCenter); - bool hit = findRayIntersectionWithCap(capCenter, _radius, capsuleCenter, intersection); - getEndPoint(capCenter); - hit = findRayIntersectionWithCap(capCenter, _radius, capsuleCenter, intersection) || hit; - if (hit) { - intersection._hitShape = const_cast(this); - } - return hit; -} - -bool CapsuleShape::findRayIntersection(RayIntersectionInfo& intersection) const { - // ray is U, capsule is V - glm::vec3 axisV; - computeNormalizedAxis(axisV); - glm::vec3 centerV = getTranslation(); - - // first handle parallel case - float uDotV = glm::dot(axisV, intersection._rayDirection); - glm::vec3 UV = intersection._rayStart - centerV; - if (glm::abs(1.0f - glm::abs(uDotV)) < EPSILON) { - // line and cylinder are parallel - float distanceV = glm::dot(UV, intersection._rayDirection); - if (glm::length2(UV - distanceV * intersection._rayDirection) <= _radius * _radius) { - // ray is inside cylinder's radius and might intersect caps - return findRayIntersectionWithCaps(centerV, intersection); - } - return false; - } - - // Given a line with point 'U' and normalized direction 'u' and - // a cylinder with axial point 'V', radius 'r', and normalized direction 'v' - // the intersection of the two is on the line at distance 't' from 'U'. - // - // Determining the values of t reduces to solving a quadratic equation: At^2 + Bt + C = 0 - // - // where: - // - // UV = U-V - // w = u-(u.v)v - // Q = UV-(UV.v)v - // - // A = w^2 - // B = 2(w.Q) - // C = Q^2 - r^2 - - glm::vec3 w = intersection._rayDirection - uDotV * axisV; - glm::vec3 Q = UV - glm::dot(UV, axisV) * axisV; - - // we save a few multiplies by storing 2*A rather than just A - float A2 = 2.0f * glm::dot(w, w); - float B = 2.0f * glm::dot(w, Q); - - // since C is only ever used once (in the determinant) we compute it inline - float determinant = B * B - 2.0f * A2 * (glm::dot(Q, Q) - _radius * _radius); - if (determinant < 0.0f) { - return false; - } - float hitLow = (-B - sqrtf(determinant)) / A2; - float hitHigh = -(hitLow + 2.0f * B / A2); - - if (hitLow > hitHigh) { - // re-arrange so hitLow is always the smaller value - float temp = hitHigh; - hitHigh = hitLow; - hitLow = temp; - } - if (hitLow < 0.0f) { - if (hitHigh < 0.0f) { - // capsule is completely behind rayStart - return false; - } - hitLow = hitHigh; - } - - glm::vec3 p = intersection._rayStart + hitLow * intersection._rayDirection; - float d = glm::dot(p - centerV, axisV); - if (glm::abs(d) <= getHalfHeight()) { - // we definitely hit the cylinder wall - intersection._hitDistance = hitLow; - intersection._hitNormal = glm::normalize(p - centerV - d * axisV); - intersection._hitShape = const_cast(this); - return true; - } - - // ray still might hit the caps - return findRayIntersectionWithCaps(centerV, intersection); -} - -// static -glm::quat CapsuleShape::computeNewRotation(const glm::vec3& newAxis) { - float angle = glm::angle(newAxis, DEFAULT_CAPSULE_AXIS); - if (angle > EPSILON) { - glm::vec3 rotationAxis = glm::normalize(glm::cross(DEFAULT_CAPSULE_AXIS, newAxis)); - return glm::angleAxis(angle, rotationAxis); - } - return glm::quat(); -} diff --git a/libraries/shared/src/CapsuleShape.h b/libraries/shared/src/CapsuleShape.h deleted file mode 100644 index 32c09696d7..0000000000 --- a/libraries/shared/src/CapsuleShape.h +++ /dev/null @@ -1,62 +0,0 @@ -// -// CapsuleShape.h -// libraries/shared/src -// -// Created by Andrew Meadows on 02/20/2014. -// 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_CapsuleShape_h -#define hifi_CapsuleShape_h - -#include "NumericalConstants.h" -#include "Shape.h" - -// default axis of CapsuleShape is Y-axis -const glm::vec3 DEFAULT_CAPSULE_AXIS(0.0f, 1.0f, 0.0f); - - -class CapsuleShape : public Shape { -public: - CapsuleShape(); - CapsuleShape(float radius, float halfHeight); - CapsuleShape(float radius, float halfHeight, const glm::vec3& position, const glm::quat& rotation); - CapsuleShape(float radius, const glm::vec3& startPoint, const glm::vec3& endPoint); - - virtual ~CapsuleShape() {} - - float getRadius() const { return _radius; } - virtual float getHalfHeight() const { return _halfHeight; } - - /// \param[out] startPoint is the center of start cap - virtual void getStartPoint(glm::vec3& startPoint) const; - - /// \param[out] endPoint is the center of the end cap - virtual void getEndPoint(glm::vec3& endPoint) const; - - virtual void computeNormalizedAxis(glm::vec3& axis) const; - - void setRadius(float radius); - virtual void setHalfHeight(float height); - virtual void setRadiusAndHalfHeight(float radius, float height); - - /// Sets the endpoints and updates center, rotation, and halfHeight to agree. - virtual void setEndPoints(const glm::vec3& startPoint, const glm::vec3& endPoint); - - bool findRayIntersection(RayIntersectionInfo& intersection) const; - - virtual float getVolume() const { return (PI * _radius * _radius) * (1.3333333333f * _radius + getHalfHeight()); } - -protected: - bool findRayIntersectionWithCaps(const glm::vec3& capsuleCenter, RayIntersectionInfo& intersection) const; - virtual void updateBoundingRadius() { _boundingRadius = _radius + getHalfHeight(); } - static glm::quat computeNewRotation(const glm::vec3& newAxis); - - float _radius; - float _halfHeight; -}; - -#endif // hifi_CapsuleShape_h diff --git a/libraries/shared/src/CollisionInfo.cpp b/libraries/shared/src/CollisionInfo.cpp deleted file mode 100644 index 7f145efb20..0000000000 --- a/libraries/shared/src/CollisionInfo.cpp +++ /dev/null @@ -1,110 +0,0 @@ -// -// CollisionInfo.cpp -// libraries/shared/src -// -// Created by Andrew Meadows on 02/14/2014. -// 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 "CollisionInfo.h" -#include "NumericalConstants.h" -#include "Shape.h" - -CollisionInfo::CollisionInfo() : - _data(NULL), - _intData(0), - _shapeA(NULL), - _shapeB(NULL), - _damping(0.0f), - _elasticity(1.0f), - _contactPoint(0.0f), - _penetration(0.0f), - _addedVelocity(0.0f) { -} - -quint64 CollisionInfo::getShapePairKey() const { - if (_shapeB == NULL || _shapeA == NULL) { - // zero is an invalid key - return 0; - } - quint32 idA = _shapeA->getID(); - quint32 idB = _shapeB->getID(); - return idA < idB ? ((quint64)idA << 32) + (quint64)idB : ((quint64)idB << 32) + (quint64)idA; -} - -CollisionList::CollisionList(int maxSize) : - _maxSize(maxSize), - _size(0) { - _collisions.resize(_maxSize); -} - -void CollisionInfo::apply() { - assert(_shapeA); - // NOTE: Shape::computeEffectiveMass() has side effects: computes and caches partial Lagrangian coefficients - Shape* shapeA = const_cast(_shapeA); - float massA = shapeA->computeEffectiveMass(_penetration, _contactPoint); - float massB = MAX_SHAPE_MASS; - float totalMass = massA + massB; - if (_shapeB) { - Shape* shapeB = const_cast(_shapeB); - massB = shapeB->computeEffectiveMass(-_penetration, _contactPoint - _penetration); - totalMass = massA + massB; - if (totalMass < EPSILON) { - massA = massB = 1.0f; - totalMass = 2.0f; - } - // remember that _penetration points from A into B - shapeB->accumulateDelta(massA / totalMass, _penetration); - } - // NOTE: Shape::accumulateDelta() uses the coefficients from previous call to Shape::computeEffectiveMass() - // remember that _penetration points from A into B - shapeA->accumulateDelta(massB / totalMass, -_penetration); -} - -CollisionInfo* CollisionList::getNewCollision() { - // return pointer to existing CollisionInfo, or NULL of list is full - return (_size < _maxSize) ? &(_collisions[_size++]) : NULL; -} - -void CollisionList::deleteLastCollision() { - if (_size > 0) { - --_size; - } -} - -CollisionInfo* CollisionList::getCollision(int index) { - return (index > -1 && index < _size) ? &(_collisions[index]) : NULL; -} - -CollisionInfo* CollisionList::getLastCollision() { - return (_size > 0) ? &(_collisions[_size - 1]) : NULL; -} - -void CollisionList::clear() { - // we rely on the external context to properly set or clear the data members of CollisionInfos - /* - for (int i = 0; i < _size; ++i) { - // we only clear the important stuff - CollisionInfo& collision = _collisions[i]; - //collision._data = NULL; - //collision._intData = 0; - //collision._floatDAta = 0.0f; - //collision._vecData = glm::vec3(0.0f); - //collision._shapeA = NULL; - //collision._shapeB = NULL; - //collision._damping; - //collision._elasticity; - //collision._contactPoint; - //collision._penetration; - //collision._addedVelocity; - } - */ - _size = 0; -} - -CollisionInfo* CollisionList::operator[](int index) { - return (index > -1 && index < _size) ? &(_collisions[index]) : NULL; -} diff --git a/libraries/shared/src/CollisionInfo.h b/libraries/shared/src/CollisionInfo.h deleted file mode 100644 index e6daf949b3..0000000000 --- a/libraries/shared/src/CollisionInfo.h +++ /dev/null @@ -1,101 +0,0 @@ -// -// CollisionInfo.h -// libraries/shared/src -// -// Created by Andrew Meadows on 02/14/2014. -// 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_CollisionInfo_h -#define hifi_CollisionInfo_h - -#include -#include - -#include -#include - -class Shape; - -const quint32 COLLISION_GROUP_ENVIRONMENT = 1U << 0; -const quint32 COLLISION_GROUP_AVATARS = 1U << 1; -const quint32 COLLISION_GROUP_VOXELS = 1U << 2; -const quint32 VALID_COLLISION_GROUPS = 0x0f; - -// CollisionInfo contains details about the collision between two things: BodyA and BodyB. -// The assumption is that the context that analyzes the collision knows about BodyA but -// does not necessarily know about BodyB. Hence the data storred in the CollisionInfo -// is expected to be relative to BodyA (for example the penetration points from A into B). - -class CollisionInfo { -public: - CollisionInfo(); - ~CollisionInfo() {} - - // TODO: Andrew to get rid of these data members - void* _data; - int _intData; - float _floatData; - glm::vec3 _vecData; - - /// accumulates position changes for the shapes in this collision to resolve penetration - void apply(); - - Shape* getShapeA() const { return const_cast(_shapeA); } - Shape* getShapeB() const { return const_cast(_shapeB); } - - /// \return unique key for shape pair - quint64 getShapePairKey() const; - - const Shape* _shapeA; // pointer to shapeA in this collision - const Shape* _shapeB; // pointer to shapeB in this collision - - void* _extraData; // pointer to extraData for this collision, opaque to the collision info, useful for external data - - float _damping; // range [0,1] of friction coeficient - float _elasticity; // range [0,1] of energy conservation - glm::vec3 _contactPoint; // world-frame point on BodyA that is deepest into BodyB - glm::vec3 _penetration; // depth that BodyA penetrates into BodyB - glm::vec3 _addedVelocity; // velocity of BodyB -}; - -// CollisionList is intended to be a recycled container. Fill the CollisionInfo's, -// use them, and then clear them for the next frame or context. - -class CollisionList { -public: - CollisionList(int maxSize); - - /// \return pointer to next collision. NULL if list is full. - CollisionInfo* getNewCollision(); - - /// \forget about collision at the end - void deleteLastCollision(); - - /// \return pointer to collision by index. NULL if index out of bounds. - CollisionInfo* getCollision(int index); - - /// \return pointer to last collision on the list. NULL if list is empty - CollisionInfo* getLastCollision(); - - /// \return true if list is full - bool isFull() const { return _size == _maxSize; } - - /// \return number of valid collisions - int size() const { return _size; } - - /// Clear valid collisions. - void clear(); - - CollisionInfo* operator[](int index); - -private: - int _maxSize; // the container cannot get larger than this - int _size; // the current number of valid collisions in the list - QVector _collisions; -}; - -#endif // hifi_CollisionInfo_h diff --git a/libraries/shared/src/GeometryUtil.cpp b/libraries/shared/src/GeometryUtil.cpp index b88166995a..b612fe0696 100644 --- a/libraries/shared/src/GeometryUtil.cpp +++ b/libraries/shared/src/GeometryUtil.cpp @@ -15,7 +15,6 @@ #include "GeometryUtil.h" #include "NumericalConstants.h" -#include "RayIntersectionInfo.h" glm::vec3 computeVectorFromPointToSegment(const glm::vec3& point, const glm::vec3& start, const glm::vec3& end) { // compute the projection of the point vector onto the segment vector diff --git a/libraries/shared/src/ListShape.cpp b/libraries/shared/src/ListShape.cpp deleted file mode 100644 index 255330c713..0000000000 --- a/libraries/shared/src/ListShape.cpp +++ /dev/null @@ -1,90 +0,0 @@ -// -// ListShape.cpp -// libraries/shared/src -// -// Created by Andrew Meadows on 02/20/2014. -// 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 "ListShape.h" - -// ListShapeEntry - -void ListShapeEntry::updateTransform(const glm::vec3& rootPosition, const glm::quat& rootRotation) { - _shape->setTranslation(rootPosition + rootRotation * _localPosition); - _shape->setRotation(_localRotation * rootRotation); -} - -// ListShape - -ListShape::~ListShape() { - clear(); -} - -void ListShape::setTranslation(const glm::vec3& position) { - _subShapeTransformsAreDirty = true; - Shape::setTranslation(position); -} - -void ListShape::setRotation(const glm::quat& rotation) { - _subShapeTransformsAreDirty = true; - Shape::setRotation(rotation); -} - -const Shape* ListShape::getSubShape(int index) const { - if (index < 0 || index > _subShapeEntries.size()) { - return NULL; - } - return _subShapeEntries[index]._shape; -} - -void ListShape::updateSubTransforms() { - if (_subShapeTransformsAreDirty) { - for (int i = 0; i < _subShapeEntries.size(); ++i) { - _subShapeEntries[i].updateTransform(_translation, _rotation); - } - _subShapeTransformsAreDirty = false; - } -} - -void ListShape::addShape(Shape* shape, const glm::vec3& localPosition, const glm::quat& localRotation) { - if (shape) { - ListShapeEntry entry; - entry._shape = shape; - entry._localPosition = localPosition; - entry._localRotation = localRotation; - _subShapeEntries.push_back(entry); - } -} - -void ListShape::setShapes(QVector& shapes) { - clear(); - _subShapeEntries.swap(shapes); - // TODO: audit our new list of entries and delete any that have null pointers - computeBoundingRadius(); -} - -void ListShape::clear() { - // the ListShape owns its subShapes, so they need to be deleted - for (int i = 0; i < _subShapeEntries.size(); ++i) { - delete _subShapeEntries[i]._shape; - } - _subShapeEntries.clear(); - setBoundingRadius(0.0f); -} - -void ListShape::computeBoundingRadius() { - float maxRadius = 0.0f; - for (int i = 0; i < _subShapeEntries.size(); ++i) { - ListShapeEntry& entry = _subShapeEntries[i]; - float radius = glm::length(entry._localPosition) + entry._shape->getBoundingRadius(); - if (radius > maxRadius) { - maxRadius = radius; - } - } - setBoundingRadius(maxRadius); -} - diff --git a/libraries/shared/src/ListShape.h b/libraries/shared/src/ListShape.h deleted file mode 100644 index 6352ef3f07..0000000000 --- a/libraries/shared/src/ListShape.h +++ /dev/null @@ -1,72 +0,0 @@ -// -// ListShape.h -// libraries/shared/src -// -// Created by Andrew Meadows on 02/20/2014. -// Copyright 2014 High Fidelity, Inc. -// -// ListShape: A collection of shapes, each with a local transform. -// -// 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_ListShape_h -#define hifi_ListShape_h - -#include - -#include -#include -#include - -#include "Shape.h" - - -class ListShapeEntry { -public: - void updateTransform(const glm::vec3& position, const glm::quat& rotation); - - Shape* _shape; - glm::vec3 _localPosition; - glm::quat _localRotation; -}; - -class ListShape : public Shape { -public: - - ListShape() : Shape(LIST_SHAPE), _subShapeEntries(), _subShapeTransformsAreDirty(false) {} - - ListShape(const glm::vec3& position, const glm::quat& rotation) : - Shape(LIST_SHAPE, position, rotation), _subShapeEntries(), _subShapeTransformsAreDirty(false) {} - - ~ListShape(); - - void setTranslation(const glm::vec3& position); - void setRotation(const glm::quat& rotation); - - const Shape* getSubShape(int index) const; - - void updateSubTransforms(); - - int size() const { return _subShapeEntries.size(); } - - void addShape(Shape* shape, const glm::vec3& localPosition, const glm::quat& localRotation); - - void setShapes(QVector& shapes); - - // TODO: either implement this or remove ListShape altogether - bool findRayIntersection(RayIntersectionInfo& intersection) const { return false; } - -protected: - void clear(); - void computeBoundingRadius(); - - QVector _subShapeEntries; - bool _subShapeTransformsAreDirty; - -private: - ListShape(const ListShape& otherList); // don't implement this -}; - -#endif // hifi_ListShape_h diff --git a/libraries/shared/src/PlaneShape.cpp b/libraries/shared/src/PlaneShape.cpp deleted file mode 100644 index cad04afa42..0000000000 --- a/libraries/shared/src/PlaneShape.cpp +++ /dev/null @@ -1,78 +0,0 @@ -// -// PlaneShape.cpp -// libraries/shared/src -// -// Created by Andrzej Kapolka on 4/10/2014. -// 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 "GLMHelpers.h" -#include "NumericalConstants.h" -#include "PlaneShape.h" - -const glm::vec3 UNROTATED_NORMAL(0.0f, 1.0f, 0.0f); - -PlaneShape::PlaneShape(const glm::vec4& coefficients) : - Shape(PLANE_SHAPE) { - - glm::vec3 normal = glm::vec3(coefficients); - _translation = -normal * coefficients.w; - - float angle = acosf(glm::dot(normal, UNROTATED_NORMAL)); - if (angle > EPSILON) { - if (angle > PI - EPSILON) { - _rotation = glm::angleAxis(PI, glm::vec3(1.0f, 0.0f, 0.0f)); - } else { - _rotation = glm::angleAxis(angle, glm::normalize(glm::cross(UNROTATED_NORMAL, normal))); - } - } -} - -glm::vec3 PlaneShape::getNormal() const { - return _rotation * UNROTATED_NORMAL; -} - -void PlaneShape::setNormal(const glm::vec3& direction) { - glm::vec3 oldTranslation = _translation; - _rotation = rotationBetween(UNROTATED_NORMAL, direction); - glm::vec3 normal = getNormal(); - _translation = glm::dot(oldTranslation, normal) * normal; -} - -void PlaneShape::setPoint(const glm::vec3& point) { - glm::vec3 normal = getNormal(); - _translation = glm::dot(point, normal) * normal; -} - -glm::vec4 PlaneShape::getCoefficients() const { - glm::vec3 normal = _rotation * UNROTATED_NORMAL; - return glm::vec4(normal.x, normal.y, normal.z, -glm::dot(normal, _translation)); -} - -bool PlaneShape::findRayIntersection(RayIntersectionInfo& intersection) const { - glm::vec3 n = getNormal(); - float denominator = glm::dot(n, intersection._rayDirection); - if (fabsf(denominator) < EPSILON) { - // line is parallel to plane - if (glm::dot(_translation - intersection._rayStart, n) < EPSILON) { - // ray starts on the plane - intersection._hitDistance = 0.0f; - intersection._hitNormal = n; - intersection._hitShape = const_cast(this); - return true; - } - } else { - float d = glm::dot(_translation - intersection._rayStart, n) / denominator; - if (d > 0.0f && d < intersection._rayLength && d < intersection._hitDistance) { - // ray points toward plane - intersection._hitDistance = d; - intersection._hitNormal = n; - intersection._hitShape = const_cast(this); - return true; - } - } - return false; -} diff --git a/libraries/shared/src/PlaneShape.h b/libraries/shared/src/PlaneShape.h deleted file mode 100644 index 8d6de326af..0000000000 --- a/libraries/shared/src/PlaneShape.h +++ /dev/null @@ -1,30 +0,0 @@ -// -// PlaneShape.h -// libraries/shared/src -// -// Created by Andrzej Kapolka on 4/9/2014. -// 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_PlaneShape_h -#define hifi_PlaneShape_h - -#include "Shape.h" - -class PlaneShape : public Shape { -public: - PlaneShape(const glm::vec4& coefficients = glm::vec4(0.0f, 1.0f, 0.0f, 0.0f)); - - glm::vec3 getNormal() const; - glm::vec4 getCoefficients() const; - - void setNormal(const glm::vec3& normal); - void setPoint(const glm::vec3& point); - - bool findRayIntersection(RayIntersectionInfo& intersection) const; -}; - -#endif // hifi_PlaneShape_h diff --git a/libraries/shared/src/RayIntersectionInfo.h b/libraries/shared/src/RayIntersectionInfo.h deleted file mode 100644 index 06ec0aceaa..0000000000 --- a/libraries/shared/src/RayIntersectionInfo.h +++ /dev/null @@ -1,37 +0,0 @@ -// -// RayIntersectionInfo.h -// libraries/physcis/src -// -// Created by Andrew Meadows 2014.09.09 -// 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_RayIntersectionInfo_h -#define hifi_RayIntersectionInfo_h - -#include - -class Shape; - -class RayIntersectionInfo { -public: - RayIntersectionInfo() : _rayStart(0.0f), _rayDirection(1.0f, 0.0f, 0.0f), _rayLength(FLT_MAX), - _hitDistance(FLT_MAX), _hitNormal(1.0f, 0.0f, 0.0f), _hitShape(NULL) { } - - glm::vec3 getIntersectionPoint() const { return _rayStart + _hitDistance * _rayDirection; } - - // input - glm::vec3 _rayStart; - glm::vec3 _rayDirection; - float _rayLength; - - // output - float _hitDistance; - glm::vec3 _hitNormal; - Shape* _hitShape; -}; - -#endif // hifi_RayIntersectionInfo_h diff --git a/libraries/shared/src/Shape.h b/libraries/shared/src/Shape.h deleted file mode 100644 index 048b8d7257..0000000000 --- a/libraries/shared/src/Shape.h +++ /dev/null @@ -1,144 +0,0 @@ -// -// Shape.h -// libraries/shared/src -// -// Created by Andrew Meadows on 2014. -// 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_Shape_h -#define hifi_Shape_h - -#include -#include -#include -#include -#include - -#include "RayIntersectionInfo.h" - -class PhysicsEntity; - -const float MAX_SHAPE_MASS = 1.0e18f; // something less than sqrt(FLT_MAX) - -// DANGER: until ShapeCollider goes away the order of these values matter. Specifically, -// UNKNOWN_SHAPE must be equal to the number of shapes that ShapeCollider actually supports. -const quint8 SPHERE_SHAPE = 0; -const quint8 CAPSULE_SHAPE = 1; -const quint8 PLANE_SHAPE = 2; -const quint8 AACUBE_SHAPE = 3; -const quint8 LIST_SHAPE = 4; -const quint8 UNKNOWN_SHAPE = 5; -const quint8 INVALID_SHAPE = 5; - -// new shapes to be supported by Bullet -const quint8 BOX_SHAPE = 7; -const quint8 CYLINDER_SHAPE = 8; - -class Shape { -public: - - typedef quint8 Type; - - static quint32 getNextID() { static quint32 nextID = 0; return ++nextID; } - - Shape() : _type(UNKNOWN_SHAPE), _owningEntity(NULL), _boundingRadius(0.0f), - _translation(0.0f), _rotation(), _mass(MAX_SHAPE_MASS) { - _id = getNextID(); - } - virtual ~Shape() { } - - Type getType() const { return _type; } - quint32 getID() const { return _id; } - - void setEntity(PhysicsEntity* entity) { _owningEntity = entity; } - PhysicsEntity* getEntity() const { return _owningEntity; } - - float getBoundingRadius() const { return _boundingRadius; } - - virtual const glm::quat& getRotation() const { return _rotation; } - virtual void setRotation(const glm::quat& rotation) { _rotation = rotation; } - - virtual void setTranslation(const glm::vec3& translation) { _translation = translation; } - virtual const glm::vec3& getTranslation() const { return _translation; } - - virtual void setMass(float mass) { _mass = mass; } - virtual float getMass() const { return _mass; } - - virtual bool findRayIntersection(RayIntersectionInfo& intersection) const = 0; - - /// \param penetration of collision - /// \param contactPoint of collision - /// \return the effective mass for the collision - /// For most shapes has side effects: computes and caches the partial Lagrangian coefficients which will be - /// used in the next accumulateDelta() call. - virtual float computeEffectiveMass(const glm::vec3& penetration, const glm::vec3& contactPoint) { return _mass; } - - /// \param relativeMassFactor the final ingredient for partial Lagrangian coefficients from computeEffectiveMass() - /// \param penetration the delta movement - virtual void accumulateDelta(float relativeMassFactor, const glm::vec3& penetration) {} - - virtual void applyAccumulatedDelta() {} - - /// \return volume of shape in cubic meters - virtual float getVolume() const { return 1.0; } - - virtual QDebug& dumpToDebug(QDebug& debugConext) const; - -protected: - // these ctors are protected (used by derived classes only) - Shape(Type type) : _type(type), _owningEntity(NULL), - _boundingRadius(0.0f), _translation(0.0f), - _rotation(), _mass(MAX_SHAPE_MASS) { - _id = getNextID(); - } - - Shape(Type type, const glm::vec3& position) : - _type(type), _owningEntity(NULL), - _boundingRadius(0.0f), _translation(position), - _rotation(), _mass(MAX_SHAPE_MASS) { - _id = getNextID(); - } - - Shape(Type type, const glm::vec3& position, const glm::quat& rotation) : _type(type), _owningEntity(NULL), - _boundingRadius(0.0f), _translation(position), - _rotation(rotation), _mass(MAX_SHAPE_MASS) { - _id = getNextID(); - } - - void setBoundingRadius(float radius) { _boundingRadius = radius; } - - Type _type; - quint32 _id; - PhysicsEntity* _owningEntity; - float _boundingRadius; - glm::vec3 _translation; - glm::quat _rotation; - float _mass; -}; - -inline QDebug& Shape::dumpToDebug(QDebug& debugConext) const { - debugConext << "Shape[ (" - << "type: " << getType() - << "position: " - << getTranslation().x << ", " << getTranslation().y << ", " << getTranslation().z - << "radius: " - << getBoundingRadius() - << "]"; - - return debugConext; -} - -inline QDebug operator<<(QDebug debug, const Shape& shape) { - return shape.dumpToDebug(debug); -} - -inline QDebug operator<<(QDebug debug, const Shape* shape) { - return shape->dumpToDebug(debug); -} - - -#endif // hifi_Shape_h diff --git a/libraries/shared/src/ShapeCollider.cpp b/libraries/shared/src/ShapeCollider.cpp deleted file mode 100644 index fa86d8c0fb..0000000000 --- a/libraries/shared/src/ShapeCollider.cpp +++ /dev/null @@ -1,1211 +0,0 @@ -// -// ShapeCollider.cpp -// libraries/shared/src -// -// Created by Andrew Meadows on 02/20/2014. -// 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 "AACubeShape.h" -#include "CapsuleShape.h" -#include "GeometryUtil.h" -#include "ListShape.h" -#include "NumericalConstants.h" -#include "PlaneShape.h" -#include "ShapeCollider.h" -#include "SphereShape.h" -#include "StreamUtils.h" - - -// NOTE: -// -// * Large ListShape's are inefficient keep the lists short. -// * Collisions between lists of lists work in theory but are not recommended. - -const quint8 NUM_SHAPE_TYPES = UNKNOWN_SHAPE; -const quint8 NUM_DISPATCH_CELLS = NUM_SHAPE_TYPES * NUM_SHAPE_TYPES; - -Shape::Type getDispatchKey(Shape::Type typeA, Shape::Type typeB) { - return typeA + NUM_SHAPE_TYPES * typeB; -} - -// dummy dispatch for any non-implemented pairings -bool notImplemented(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - return false; -} - -// NOTE: hardcode the number of dispatchTable entries (NUM_SHAPE_TYPES ^2) -bool (*dispatchTable[NUM_DISPATCH_CELLS])(const Shape*, const Shape*, CollisionList&); - -namespace ShapeCollider { - -// NOTE: the dispatch table must be initialized before the ShapeCollider is used. -void initDispatchTable() { - for (Shape::Type i = 0; i < NUM_DISPATCH_CELLS; ++i) { - dispatchTable[i] = ¬Implemented; - } - - dispatchTable[getDispatchKey(SPHERE_SHAPE, SPHERE_SHAPE)] = &sphereVsSphere; - dispatchTable[getDispatchKey(SPHERE_SHAPE, CAPSULE_SHAPE)] = &sphereVsCapsule; - dispatchTable[getDispatchKey(SPHERE_SHAPE, PLANE_SHAPE)] = &sphereVsPlane; - dispatchTable[getDispatchKey(SPHERE_SHAPE, AACUBE_SHAPE)] = &sphereVsAACube; - dispatchTable[getDispatchKey(SPHERE_SHAPE, LIST_SHAPE)] = &shapeVsList; - - dispatchTable[getDispatchKey(CAPSULE_SHAPE, SPHERE_SHAPE)] = &capsuleVsSphere; - dispatchTable[getDispatchKey(CAPSULE_SHAPE, CAPSULE_SHAPE)] = &capsuleVsCapsule; - dispatchTable[getDispatchKey(CAPSULE_SHAPE, PLANE_SHAPE)] = &capsuleVsPlane; - dispatchTable[getDispatchKey(CAPSULE_SHAPE, AACUBE_SHAPE)] = &capsuleVsAACube; - dispatchTable[getDispatchKey(CAPSULE_SHAPE, LIST_SHAPE)] = &shapeVsList; - - dispatchTable[getDispatchKey(PLANE_SHAPE, SPHERE_SHAPE)] = &planeVsSphere; - dispatchTable[getDispatchKey(PLANE_SHAPE, CAPSULE_SHAPE)] = &planeVsCapsule; - dispatchTable[getDispatchKey(PLANE_SHAPE, PLANE_SHAPE)] = &planeVsPlane; - dispatchTable[getDispatchKey(PLANE_SHAPE, AACUBE_SHAPE)] = ¬Implemented; - dispatchTable[getDispatchKey(PLANE_SHAPE, LIST_SHAPE)] = &shapeVsList; - - dispatchTable[getDispatchKey(AACUBE_SHAPE, SPHERE_SHAPE)] = &aaCubeVsSphere; - dispatchTable[getDispatchKey(AACUBE_SHAPE, CAPSULE_SHAPE)] = &aaCubeVsCapsule; - dispatchTable[getDispatchKey(AACUBE_SHAPE, PLANE_SHAPE)] = ¬Implemented; - dispatchTable[getDispatchKey(AACUBE_SHAPE, AACUBE_SHAPE)] = &aaCubeVsAACube; - dispatchTable[getDispatchKey(AACUBE_SHAPE, LIST_SHAPE)] = &shapeVsList; - - dispatchTable[getDispatchKey(LIST_SHAPE, SPHERE_SHAPE)] = &listVsShape; - dispatchTable[getDispatchKey(LIST_SHAPE, CAPSULE_SHAPE)] = &listVsShape; - dispatchTable[getDispatchKey(LIST_SHAPE, PLANE_SHAPE)] = &listVsShape; - dispatchTable[getDispatchKey(LIST_SHAPE, AACUBE_SHAPE)] = &listVsShape; - dispatchTable[getDispatchKey(LIST_SHAPE, LIST_SHAPE)] = &listVsList; - - // all of the UNKNOWN_SHAPE pairings are notImplemented -} - -bool collideShapes(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - return (*dispatchTable[shapeA->getType() + NUM_SHAPE_TYPES * shapeB->getType()])(shapeA, shapeB, collisions); -} - -static CollisionList tempCollisions(32); - -bool collideShapeWithShapes(const Shape* shapeA, const QVector& shapes, int startIndex, CollisionList& collisions) { - bool collided = false; - if (shapeA) { - int numShapes = shapes.size(); - for (int i = startIndex; i < numShapes; ++i) { - const Shape* shapeB = shapes.at(i); - if (!shapeB) { - continue; - } - if (collideShapes(shapeA, shapeB, collisions)) { - collided = true; - if (collisions.isFull()) { - break; - } - } - } - } - return collided; -} - -bool collideShapesWithShapes(const QVector& shapesA, const QVector& shapesB, CollisionList& collisions) { - bool collided = false; - int numShapesA = shapesA.size(); - for (int i = 0; i < numShapesA; ++i) { - Shape* shapeA = shapesA.at(i); - if (!shapeA) { - continue; - } - if (collideShapeWithShapes(shapeA, shapesB, 0, collisions)) { - collided = true; - if (collisions.isFull()) { - break; - } - } - } - return collided; -} - -bool collideShapeWithAACubeLegacy(const Shape* shapeA, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions) { - Shape::Type typeA = shapeA->getType(); - if (typeA == SPHERE_SHAPE) { - return sphereVsAACubeLegacy(static_cast(shapeA), cubeCenter, cubeSide, collisions); - } else if (typeA == CAPSULE_SHAPE) { - return capsuleVsAACubeLegacy(static_cast(shapeA), cubeCenter, cubeSide, collisions); - } else if (typeA == LIST_SHAPE) { - const ListShape* listA = static_cast(shapeA); - bool touching = false; - for (int i = 0; i < listA->size() && !collisions.isFull(); ++i) { - const Shape* subShape = listA->getSubShape(i); - int subType = subShape->getType(); - if (subType == SPHERE_SHAPE) { - touching = sphereVsAACubeLegacy(static_cast(subShape), cubeCenter, cubeSide, collisions) || touching; - } else if (subType == CAPSULE_SHAPE) { - touching = capsuleVsAACubeLegacy(static_cast(subShape), cubeCenter, cubeSide, collisions) || touching; - } - } - return touching; - } - return false; -} - -bool sphereVsSphere(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - const SphereShape* sphereA = static_cast(shapeA); - const SphereShape* sphereB = static_cast(shapeB); - glm::vec3 BA = sphereB->getTranslation() - sphereA->getTranslation(); - float distanceSquared = glm::dot(BA, BA); - float totalRadius = sphereA->getRadius() + sphereB->getRadius(); - if (distanceSquared < totalRadius * totalRadius) { - // normalize BA - float distance = sqrtf(distanceSquared); - if (distance < EPSILON) { - // the spheres are on top of each other, so we pick an arbitrary penetration direction - BA = glm::vec3(0.0f, 1.0f, 0.0f); - distance = totalRadius; - } else { - BA /= distance; - } - // penetration points from A into B - CollisionInfo* collision = collisions.getNewCollision(); - if (collision) { - collision->_penetration = BA * (totalRadius - distance); - // contactPoint is on surface of A - collision->_contactPoint = sphereA->getTranslation() + sphereA->getRadius() * BA; - collision->_shapeA = shapeA; - collision->_shapeB = shapeB; - return true; - } - } - return false; -} - -bool sphereVsCapsule(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - const SphereShape* sphereA = static_cast(shapeA); - const CapsuleShape* capsuleB = static_cast(shapeB); - // find sphereA's closest approach to axis of capsuleB - glm::vec3 BA = capsuleB->getTranslation() - sphereA->getTranslation(); - glm::vec3 capsuleAxis; - capsuleB->computeNormalizedAxis(capsuleAxis); - float axialDistance = - glm::dot(BA, capsuleAxis); - float absAxialDistance = fabsf(axialDistance); - float totalRadius = sphereA->getRadius() + capsuleB->getRadius(); - if (absAxialDistance < totalRadius + capsuleB->getHalfHeight()) { - glm::vec3 radialAxis = BA + axialDistance * capsuleAxis; // points from A to axis of B - float radialDistance2 = glm::length2(radialAxis); - float totalRadius2 = totalRadius * totalRadius; - if (radialDistance2 > totalRadius2) { - // sphere is too far from capsule axis - return false; - } - if (absAxialDistance > capsuleB->getHalfHeight()) { - // sphere hits capsule on a cap --> recompute radialAxis to point from spherA to cap center - float sign = (axialDistance > 0.0f) ? 1.0f : -1.0f; - radialAxis = BA + (sign * capsuleB->getHalfHeight()) * capsuleAxis; - radialDistance2 = glm::length2(radialAxis); - if (radialDistance2 > totalRadius2) { - return false; - } - } - if (radialDistance2 > EPSILON * EPSILON) { - CollisionInfo* collision = collisions.getNewCollision(); - if (!collision) { - // collisions list is full - return false; - } - // normalize the radialAxis - float radialDistance = sqrtf(radialDistance2); - radialAxis /= radialDistance; - // penetration points from A into B - collision->_penetration = (totalRadius - radialDistance) * radialAxis; // points from A into B - // contactPoint is on surface of sphereA - collision->_contactPoint = sphereA->getTranslation() + sphereA->getRadius() * radialAxis; - collision->_shapeA = shapeA; - collision->_shapeB = shapeB; - } else { - // A is on B's axis, so the penetration is undefined... - if (absAxialDistance > capsuleB->getHalfHeight()) { - // ...for the cylinder case (for now we pretend the collision doesn't exist) - return false; - } - CollisionInfo* collision = collisions.getNewCollision(); - if (!collision) { - // collisions list is full - return false; - } - // ... but still defined for the cap case - if (axialDistance < 0.0f) { - // we're hitting the start cap, so we negate the capsuleAxis - capsuleAxis *= -1; - } - // penetration points from A into B - float sign = (axialDistance > 0.0f) ? -1.0f : 1.0f; - collision->_penetration = (sign * (totalRadius + capsuleB->getHalfHeight() - absAxialDistance)) * capsuleAxis; - // contactPoint is on surface of sphereA - collision->_contactPoint = sphereA->getTranslation() + (sign * sphereA->getRadius()) * capsuleAxis; - collision->_shapeA = shapeA; - collision->_shapeB = shapeB; - } - return true; - } - return false; -} - -bool sphereVsPlane(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - const SphereShape* sphereA = static_cast(shapeA); - const PlaneShape* planeB = static_cast(shapeB); - glm::vec3 penetration; - if (findSpherePlanePenetration(sphereA->getTranslation(), sphereA->getRadius(), planeB->getCoefficients(), penetration)) { - CollisionInfo* collision = collisions.getNewCollision(); - if (!collision) { - return false; // collision list is full - } - collision->_penetration = penetration; - collision->_contactPoint = sphereA->getTranslation() + sphereA->getRadius() * glm::normalize(penetration); - collision->_shapeA = shapeA; - collision->_shapeB = shapeB; - return true; - } - return false; -} - -bool capsuleVsSphere(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - return sphereVsCapsule(shapeB, shapeA, collisions); -} - -/// \param lineP point on line -/// \param lineDir normalized direction of line -/// \param cylinderP point on cylinder axis -/// \param cylinderDir normalized direction of cylinder axis -/// \param cylinderRadius radius of cylinder -/// \param hitLow[out] distance from point on line to first intersection with cylinder -/// \param hitHigh[out] distance from point on line to second intersection with cylinder -/// \return true if line hits cylinder -bool lineCylinder(const glm::vec3& lineP, const glm::vec3& lineDir, - const glm::vec3& cylinderP, const glm::vec3& cylinderDir, float cylinderRadius, - float& hitLow, float& hitHigh) { - - // first handle parallel case - float uDotV = glm::dot(lineDir, cylinderDir); - if (fabsf(1.0f - fabsf(uDotV)) < EPSILON) { - // line and cylinder are parallel - if (glm::distance2(lineP, cylinderP) <= cylinderRadius * cylinderRadius) { - // line is inside cylinder, which we consider a hit - hitLow = 0.0f; - hitHigh = 0.0f; - return true; - } - return false; - } - - // Given a line with point 'p' and normalized direction 'u' and - // a cylinder with axial point 's', radius 'r', and normalized direction 'v' - // the intersection of the two is on the line at distance 't' from 'p'. - // - // Determining the values of t reduces to solving a quadratic equation: At^2 + Bt + C = 0 - // - // where: - // - // P = p-s - // w = u-(u.v)v - // Q = P-(P.v)v - // - // A = w^2 - // B = 2(w.Q) - // C = Q^2 - r^2 - - glm::vec3 P = lineP - cylinderP; - glm::vec3 w = lineDir - uDotV * cylinderDir; - glm::vec3 Q = P - glm::dot(P, cylinderDir) * cylinderDir; - - // we save a few multiplies by storing 2*A rather than just A - float A2 = 2.0f * glm::dot(w, w); - float B = 2.0f * glm::dot(w, Q); - - // since C is only ever used once (in the determinant) we compute it inline - float determinant = B * B - 2.0f * A2 * (glm::dot(Q, Q) - cylinderRadius * cylinderRadius); - if (determinant < 0.0f) { - return false; - } - hitLow = (-B - sqrtf(determinant)) / A2; - hitHigh = -(hitLow + 2.0f * B / A2); - - if (hitLow > hitHigh) { - // re-arrange so hitLow is always the smaller value - float temp = hitHigh; - hitHigh = hitLow; - hitLow = temp; - } - return true; -} - -bool capsuleVsCapsule(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - const CapsuleShape* capsuleA = static_cast(shapeA); - const CapsuleShape* capsuleB = static_cast(shapeB); - glm::vec3 axisA; - capsuleA->computeNormalizedAxis(axisA); - glm::vec3 axisB; - capsuleB->computeNormalizedAxis(axisB); - glm::vec3 centerA = capsuleA->getTranslation(); - glm::vec3 centerB = capsuleB->getTranslation(); - - // NOTE: The formula for closest approach between two lines is: - // d = [(B - A) . (a - (a.b)b)] / (1 - (a.b)^2) - - float aDotB = glm::dot(axisA, axisB); - float denominator = 1.0f - aDotB * aDotB; - float totalRadius = capsuleA->getRadius() + capsuleB->getRadius(); - if (denominator > EPSILON) { - // perform line-cylinder intesection test between axis of cylinderA and cylinderB with exanded radius - float hitLow = 0.0f; - float hitHigh = 0.0f; - if (!lineCylinder(centerA, axisA, centerB, axisB, totalRadius, hitLow, hitHigh)) { - return false; - } - - float halfHeightA = capsuleA->getHalfHeight(); - if (hitLow > halfHeightA || hitHigh < -halfHeightA) { - // the intersections are off the ends of capsuleA - return false; - } - - // compute nearest approach on axisA of axisB - float distanceA = glm::dot((centerB - centerA), (axisA - (aDotB) * axisB)) / denominator; - // clamp to intersection zone - if (distanceA > hitLow) { - if (distanceA > hitHigh) { - distanceA = hitHigh; - } - } else { - distanceA = hitLow; - } - // clamp to capsule segment - distanceA = glm::clamp(distanceA, -halfHeightA, halfHeightA); - - // find the closest point on capsuleB to sphere on capsuleA - float distanceB = glm::dot(centerA + distanceA * axisA - centerB, axisB); - float halfHeightB = capsuleB->getHalfHeight(); - if (fabsf(distanceB) > halfHeightB) { - // we must clamp distanceB... - distanceB = glm::clamp(distanceB, -halfHeightB, halfHeightB); - // ...and therefore must recompute distanceA - distanceA = glm::clamp(glm::dot(centerB + distanceB * axisB - centerA, axisA), -halfHeightA, halfHeightA); - } - - // collide like two spheres (do most of the math relative to B) - glm::vec3 BA = (centerB + distanceB * axisB) - (centerA + distanceA * axisA); - float distanceSquared = glm::dot(BA, BA); - if (distanceSquared < totalRadius * totalRadius) { - CollisionInfo* collision = collisions.getNewCollision(); - if (!collision) { - // collisions list is full - return false; - } - // normalize BA - float distance = sqrtf(distanceSquared); - if (distance < EPSILON) { - // the contact spheres are on top of each other, so we need to pick a penetration direction... - // try vector between the capsule centers... - BA = centerB - centerA; - distanceSquared = glm::length2(BA); - if (distanceSquared > EPSILON * EPSILON) { - distance = sqrtf(distanceSquared); - BA /= distance; - } else - { - // the capsule centers are on top of each other! - // give up on a valid penetration direction and just use the yAxis - BA = glm::vec3(0.0f, 1.0f, 0.0f); - distance = glm::max(capsuleB->getRadius(), capsuleA->getRadius()); - } - } else { - BA /= distance; - } - // penetration points from A into B - collision->_penetration = BA * (totalRadius - distance); - // contactPoint is on surface of A - collision->_contactPoint = centerA + distanceA * axisA + capsuleA->getRadius() * BA; - collision->_shapeA = shapeA; - collision->_shapeB = shapeB; - return true; - } - } else { - // capsules are approximiately parallel but might still collide - glm::vec3 BA = centerB - centerA; - float axialDistance = glm::dot(BA, axisB); - if (fabsf(axialDistance) > totalRadius + capsuleA->getHalfHeight() + capsuleB->getHalfHeight()) { - return false; - } - BA = BA - axialDistance * axisB; // BA now points from centerA to axisB (perp to axis) - float distanceSquared = glm::length2(BA); - if (distanceSquared < totalRadius * totalRadius) { - CollisionInfo* collision = collisions.getNewCollision(); - if (!collision) { - // collisions list is full - return false; - } - // We have all the info we need to compute the penetration vector... - // normalize BA - float distance = sqrtf(distanceSquared); - if (distance < EPSILON) { - // the spheres are on top of each other, so we pick an arbitrary penetration direction - BA = glm::vec3(0.0f, 1.0f, 0.0f); - } else { - BA /= distance; - } - // penetration points from A into B - collision->_penetration = BA * (totalRadius - distance); - - // However we need some more world-frame info to compute the contactPoint, - // which is on the surface of capsuleA... - // - // Find the overlapping secion of the capsules --> they collide as if there were - // two spheres at the midpoint of this overlapping section. - // So we project all endpoints to axisB, find the interior pair, - // and put A's proxy sphere on axisA at the midpoint of this section. - - // sort the projections as much as possible during calculation - float points[5]; - points[0] = -capsuleB->getHalfHeight(); - points[1] = axialDistance - capsuleA->getHalfHeight(); - points[2] = axialDistance + capsuleA->getHalfHeight(); - points[3] = capsuleB->getHalfHeight(); - - // Since there are only three comparisons to do we unroll the sort algorithm... - // and use a fifth slot as temp during swap. - if (points[1] > points[2]) { - points[4] = points[1]; - points[1] = points[2]; - points[2] = points[4]; - } - if (points[2] > points[3]) { - points[4] = points[2]; - points[2] = points[3]; - points[3] = points[4]; - } - if (points[0] > points[1]) { - points[4] = points[0]; - points[0] = points[1]; - points[1] = points[4]; - } - - // average the internal pair, and then do the math from centerB - collision->_contactPoint = centerB + (0.5f * (points[1] + points[2])) * axisB - + (capsuleA->getRadius() - distance) * BA; - collision->_shapeA = shapeA; - collision->_shapeB = shapeB; - return true; - } - } - return false; -} - -bool capsuleVsPlane(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - const CapsuleShape* capsuleA = static_cast(shapeA); - const PlaneShape* planeB = static_cast(shapeB); - glm::vec3 start, end, penetration; - capsuleA->getStartPoint(start); - capsuleA->getEndPoint(end); - glm::vec4 plane = planeB->getCoefficients(); - if (findCapsulePlanePenetration(start, end, capsuleA->getRadius(), plane, penetration)) { - CollisionInfo* collision = collisions.getNewCollision(); - if (!collision) { - return false; // collision list is full - } - collision->_penetration = penetration; - glm::vec3 deepestEnd = (glm::dot(start, glm::vec3(plane)) < glm::dot(end, glm::vec3(plane))) ? start : end; - collision->_contactPoint = deepestEnd + capsuleA->getRadius() * glm::normalize(penetration); - collision->_shapeA = shapeA; - collision->_shapeB = shapeB; - return true; - } - return false; -} - -bool planeVsSphere(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - return sphereVsPlane(shapeB, shapeA, collisions); -} - -bool planeVsCapsule(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - return capsuleVsPlane(shapeB, shapeA, collisions); -} - -bool planeVsPlane(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - // technically, planes always collide unless they're parallel and not coincident; however, that's - // not going to give us any useful information - return false; -} - -// helper function -bool sphereVsAACubeLegacy(const glm::vec3& sphereCenter, float sphereRadius, const glm::vec3& cubeCenter, - float cubeSide, CollisionList& collisions) { - // sphere is A - // cube is B - // BA = B - A = from center of A to center of B - float halfCubeSide = 0.5f * cubeSide; - glm::vec3 BA = cubeCenter - sphereCenter; - float distance = glm::length(BA); - if (distance > EPSILON) { - float maxBA = glm::max(glm::max(glm::abs(BA.x), glm::abs(BA.y)), glm::abs(BA.z)); - if (maxBA > halfCubeSide + sphereRadius) { - // sphere misses cube entirely - return false; - } - CollisionInfo* collision = collisions.getNewCollision(); - if (!collision) { - return false; - } - if (maxBA > halfCubeSide) { - // sphere hits cube but its center is outside cube - - // compute contact anti-pole on cube (in cube frame) - glm::vec3 cubeContact = glm::abs(BA); - if (cubeContact.x > halfCubeSide) { - cubeContact.x = halfCubeSide; - } - if (cubeContact.y > halfCubeSide) { - cubeContact.y = halfCubeSide; - } - if (cubeContact.z > halfCubeSide) { - cubeContact.z = halfCubeSide; - } - glm::vec3 signs = glm::sign(BA); - cubeContact.x *= signs.x; - cubeContact.y *= signs.y; - cubeContact.z *= signs.z; - - // compute penetration direction - glm::vec3 direction = BA - cubeContact; - float lengthDirection = glm::length(direction); - if (lengthDirection < EPSILON) { - // sphereCenter is touching cube surface, so we can't use the difference between those two - // points to compute the penetration direction. Instead we use the unitary components of - // cubeContact. - direction = cubeContact / halfCubeSide; - glm::modf(BA, direction); - lengthDirection = glm::length(direction); - } else if (lengthDirection > sphereRadius) { - collisions.deleteLastCollision(); - return false; - } - direction /= lengthDirection; - - // compute collision details - collision->_contactPoint = sphereCenter + sphereRadius * direction; - collision->_penetration = sphereRadius * direction - (BA - cubeContact); - } else { - // sphere center is inside cube - // --> push out nearest face - glm::vec3 direction; - BA /= maxBA; - glm::modf(BA, direction); - float lengthDirection = glm::length(direction); - direction /= lengthDirection; - - // compute collision details - collision->_floatData = cubeSide; - collision->_vecData = cubeCenter; - collision->_penetration = (halfCubeSide * lengthDirection + sphereRadius - maxBA * glm::dot(BA, direction)) * direction; - collision->_contactPoint = sphereCenter + sphereRadius * direction; - } - collision->_floatData = cubeSide; - collision->_vecData = cubeCenter; - collision->_shapeA = NULL; - collision->_shapeB = NULL; - return true; - } else if (sphereRadius + halfCubeSide > distance) { - // NOTE: for cocentric approximation we collide sphere and cube as two spheres which means - // this algorithm will probably be wrong when both sphere and cube are very small (both ~EPSILON) - CollisionInfo* collision = collisions.getNewCollision(); - if (collision) { - // the penetration and contactPoint are undefined, so we pick a penetration direction (-yAxis) - collision->_penetration = (sphereRadius + halfCubeSide) * glm::vec3(0.0f, -1.0f, 0.0f); - // contactPoint is on surface of A - collision->_contactPoint = sphereCenter + collision->_penetration; - - collision->_floatData = cubeSide; - collision->_vecData = cubeCenter; - collision->_shapeA = NULL; - collision->_shapeB = NULL; - return true; - } - } - return false; -} - -// helper function -CollisionInfo* sphereVsAACubeHelper(const glm::vec3& sphereCenter, float sphereRadius, const glm::vec3& cubeCenter, - float cubeSide, CollisionList& collisions) { - // sphere is A - // cube is B - // BA = B - A = from center of A to center of B - float halfCubeSide = 0.5f * cubeSide; - glm::vec3 BA = cubeCenter - sphereCenter; - float distance = glm::length(BA); - if (distance > EPSILON) { - float maxBA = glm::max(glm::max(glm::abs(BA.x), glm::abs(BA.y)), glm::abs(BA.z)); - if (maxBA > halfCubeSide + sphereRadius) { - // sphere misses cube entirely - return NULL; - } - CollisionInfo* collision = collisions.getNewCollision(); - if (!collision) { - return NULL; - } - if (maxBA > halfCubeSide) { - // sphere hits cube but its center is outside cube - - // compute contact anti-pole on cube (in cube frame) - glm::vec3 cubeContact = glm::abs(BA); - if (cubeContact.x > halfCubeSide) { - cubeContact.x = halfCubeSide; - } - if (cubeContact.y > halfCubeSide) { - cubeContact.y = halfCubeSide; - } - if (cubeContact.z > halfCubeSide) { - cubeContact.z = halfCubeSide; - } - glm::vec3 signs = glm::sign(BA); - cubeContact.x *= signs.x; - cubeContact.y *= signs.y; - cubeContact.z *= signs.z; - - // compute penetration direction - glm::vec3 direction = BA - cubeContact; - float lengthDirection = glm::length(direction); - if (lengthDirection < EPSILON) { - // sphereCenter is touching cube surface, so we can't use the difference between those two - // points to compute the penetration direction. Instead we use the unitary components of - // cubeContact. - glm::modf(cubeContact / halfCubeSide, direction); - lengthDirection = glm::length(direction); - } else if (lengthDirection > sphereRadius) { - collisions.deleteLastCollision(); - return NULL; - } - direction /= lengthDirection; - - // compute collision details - collision->_contactPoint = sphereCenter + sphereRadius * direction; - collision->_penetration = sphereRadius * direction - (BA - cubeContact); - } else { - // sphere center is inside cube - // --> push out nearest face - glm::vec3 direction; - BA /= maxBA; - glm::modf(BA, direction); - float lengthDirection = glm::length(direction); - direction /= lengthDirection; - - // compute collision details - collision->_floatData = cubeSide; - collision->_vecData = cubeCenter; - collision->_penetration = (halfCubeSide * lengthDirection + sphereRadius - maxBA * glm::dot(BA, direction)) * direction; - collision->_contactPoint = sphereCenter + sphereRadius * direction; - } - collision->_shapeA = NULL; - collision->_shapeB = NULL; - return collision; - } else if (sphereRadius + halfCubeSide > distance) { - // NOTE: for cocentric approximation we collide sphere and cube as two spheres which means - // this algorithm will probably be wrong when both sphere and cube are very small (both ~EPSILON) - CollisionInfo* collision = collisions.getNewCollision(); - if (collision) { - // the penetration and contactPoint are undefined, so we pick a penetration direction (-yAxis) - collision->_penetration = (sphereRadius + halfCubeSide) * glm::vec3(0.0f, -1.0f, 0.0f); - // contactPoint is on surface of A - collision->_contactPoint = sphereCenter + collision->_penetration; - collision->_shapeA = NULL; - collision->_shapeB = NULL; - return collision; - } - } - return NULL; -} - -bool sphereVsAACube(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - // BA = B - A = from center of A to center of B - const SphereShape* sphereA = static_cast(shapeA); - const AACubeShape* cubeB = static_cast(shapeB); - - CollisionInfo* collision = sphereVsAACubeHelper( sphereA->getTranslation(), sphereA->getRadius(), - cubeB->getTranslation(), cubeB->getScale(), collisions); - if (collision) { - collision->_shapeA = shapeA; - collision->_shapeB = shapeB; - return true; - } - return false; -} - -glm::vec3 cubeNormals[3] = { glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f) }; - -// the wallIndices is a sequence of pairs that represent the OTHER directions for each cube face, -// hence the first pair is (1, 2) because the OTHER faces for xFace are (yFace, zFace) = (1, 2) -int wallIndices[] = { 1, 2, 0, 2, 0, 1 }; - -bool capsuleVsAACubeFace(const CapsuleShape* capsuleA, const AACubeShape* cubeB, int faceIndex, const glm::vec3& faceNormal, CollisionList& collisions) { - // we only fall in here when the capsuleAxis is nearly parallel to the face of a cube - glm::vec3 capsuleAxis; - capsuleA->computeNormalizedAxis(capsuleAxis); - glm::vec3 cubeCenter = cubeB->getTranslation(); - - // Revisualize the capsule as a line segment between two points. We'd like to collide the - // capsule as two spheres located at the endpoints or where the line segment hits the boundary - // of the face. - - // We raytrace forward into the four planes that neigbhor the face to find the boundary - // points of the line segment. - glm::vec3 capsuleStart; - capsuleA->getStartPoint(capsuleStart); - - // translate into cube-relative frame - capsuleStart -= cubeCenter; - float capsuleLength = 2.0f * capsuleA->getHalfHeight(); - float halfCubeSide = 0.5f * cubeB->getScale(); - float capsuleRadius = capsuleA->getRadius(); - - // preload distances with values that work for when the capsuleAxis runs parallel to neighbor face - float distances[] = {FLT_MAX, -FLT_MAX, FLT_MAX, -FLT_MAX, 0.0f}; - - // Loop over the directions that are NOT parallel to face (there are two of them). - // For each direction we'll raytrace against the positive and negative planes to find where - // the axis passes through. - for (int i = 0; i < 2; ++i) { - int wallIndex = wallIndices[2 * faceIndex + i]; - glm::vec3 wallNormal = cubeNormals[wallIndex]; - // each direction has two walls: positive and negative - float axisDotWall = glm::dot(capsuleAxis, wallNormal); - if (fabsf(axisDotWall) > EPSILON) { - // formula for distance to intersection between line (P,p) and plane (V,n) is: (V-P)*n / p*n - distances[2 * i] = (halfCubeSide - glm::dot(capsuleStart, wallNormal)) / axisDotWall; - distances[2 * i + 1] = -(halfCubeSide + glm::dot(capsuleStart, wallNormal)) / axisDotWall; - } - } - - // sort the distances from large to small - int j = 3; - while (j > 0) { - for (int i = 0; i <= j; ++i) { - if (distances[i] < distances[i+1]) { - // swap (using distances[4] as temp space) - distances[4] = distances[i]; - distances[i] = distances[i+1]; - distances[i+1] = distances[4]; - } - } - --j; - } - - // the capsule overlaps when the max of the mins is less than the min of the maxes - distances[0] = glm::min(capsuleLength, distances[1]); // maxDistance - distances[1] = glm::max(0.0f, distances[2]); // minDistance - bool hit = false; - if (distances[1] < distances[0]) { - // if we collide at all it will be at two points - for (int i = 0; i < 2; ++i) { - glm::vec3 sphereCenter = cubeCenter + capsuleStart + distances[i] * capsuleAxis; - // collide like a sphere at point0 with capsuleRadius - CollisionInfo* collision = sphereVsAACubeHelper(sphereCenter, capsuleRadius, - cubeCenter, 2.0f * halfCubeSide, collisions); - if (collision) { - // we hit! so store back pointers to the shapes - collision->_shapeA = capsuleA; - collision->_shapeB = cubeB; - hit = true; - } - } - return hit; - } else if (distances[1] < capsuleLength + capsuleRadius ) { - // we might collide at the end cap - glm::vec3 sphereCenter = cubeCenter + capsuleStart + capsuleLength * capsuleAxis; - // collide like a sphere at point0 with capsuleRadius - CollisionInfo* collision = sphereVsAACubeHelper(sphereCenter, capsuleRadius, - cubeCenter, 2.0f * halfCubeSide, collisions); - if (collision) { - // we hit! so store back pointers to the shapes - collision->_shapeA = capsuleA; - collision->_shapeB = cubeB; - hit = true; - } - } else if (distances[0] > -capsuleLength) { - // we might collide at the start cap - glm::vec3 sphereCenter = cubeCenter + capsuleStart; - // collide like a sphere at point0 with capsuleRadius - CollisionInfo* collision = sphereVsAACubeHelper(sphereCenter, capsuleRadius, - cubeCenter, 2.0f * halfCubeSide, collisions); - if (collision) { - // we hit! so store back pointers to the shapes - collision->_shapeA = capsuleA; - collision->_shapeB = cubeB; - hit = true; - } - } - return hit; -} - -bool capsuleVsAACube(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - const CapsuleShape* capsuleA = static_cast(shapeA); - const AACubeShape* cubeB = static_cast(shapeB); - - // find nearest approach of capsule's line segment to cube's center - glm::vec3 capsuleAxis; - capsuleA->computeNormalizedAxis(capsuleAxis); - float halfHeight = capsuleA->getHalfHeight(); - glm::vec3 cubeCenter = cubeB->getTranslation(); - glm::vec3 capsuleCenter = capsuleA->getTranslation(); - glm::vec3 BA = cubeCenter - capsuleCenter; - float axialOffset = glm::dot(capsuleAxis, BA); - if (fabsf(axialOffset) > halfHeight) { - axialOffset = (axialOffset < 0.0f) ? -halfHeight : halfHeight; - } - glm::vec3 nearestApproach = capsuleCenter + axialOffset * capsuleAxis; - - // transform nearestApproach into cube-relative frame - nearestApproach -= cubeCenter; - - // determine the face of nearest approach - glm::vec3 signs = glm::sign(nearestApproach); - int faceIndex = 0; - glm::vec3 faceNormal(signs.x, 0.0f, 0.0f); - float maxApproach = fabsf(nearestApproach.x); - if (maxApproach < fabsf(nearestApproach.y)) { - maxApproach = fabsf(nearestApproach.y); - faceIndex = 1; - faceNormal = glm::vec3(0.0f, signs.y, 0.0f); - } - if (maxApproach < fabsf(nearestApproach.z)) { - maxApproach = fabsf(nearestApproach.z); - faceIndex = 2; - faceNormal = glm::vec3(0.0f, 0.0f, signs.z); - } - - if (fabsf(glm::dot(faceNormal, capsuleAxis)) < EPSILON) { - if (glm::dot(nearestApproach, faceNormal) > cubeB->getScale() + capsuleA->getRadius()) { - return false; - } - // we expect this case to be rare but complicated enough that we split it out - // into its own helper function - return capsuleVsAACubeFace(capsuleA, cubeB, faceIndex, faceNormal, collisions); - } - - // Revisualize the capsule as a startPoint and an axis that points toward the cube face. - // We raytrace forward into the four planes that neigbhor the face to find the furthest - // point along the capsule's axis that might hit face. - glm::vec3 capsuleStart; - if (glm::dot(capsuleAxis, faceNormal) < 0.0f) { - capsuleA->getStartPoint(capsuleStart); - } else { - // NOTE: we want dot(capsuleAxis, faceNormal) to be negative which simplifies some - // logic below, so we pretend the end is the start thereby reversing its axis. - capsuleA->getEndPoint(capsuleStart); - capsuleAxis *= -1.0f; - } - // translate into cube-relative frame - capsuleStart -= cubeCenter; - float capsuleLength = 2.0f * halfHeight; - float halfCubeSide = 0.5f * cubeB->getScale(); - float capsuleRadius = capsuleA->getRadius(); - - // Loop over the directions that are NOT parallel to face (there are two of them). - // For each direction we'll raytrace along capsuleAxis to find where the axis passes - // through the furthest face and then we'll clamp to remain on the capsule's line segment - float shortestDistance = capsuleLength; - - for (int i = 0; i < 2; ++i) { - int wallIndex = wallIndices[2 * faceIndex + i]; - // each direction has two walls: positive and negative - for (float wallSign = -1.0f; wallSign < 2.0f; wallSign += 2.0f) { - glm::vec3 wallNormal = wallSign * cubeNormals[wallIndex]; - float axisDotWall = glm::dot(capsuleAxis, wallNormal); - if (axisDotWall > EPSILON) { - // formula for distance to intersection between line (P,p) and plane (V,n) is: (V-P)*n / p*n - float newDistance = (halfCubeSide - glm::dot(capsuleStart, wallNormal)) / axisDotWall; - if (newDistance < 0.0f) { - // The wall is behind the capsule, but there is still a possibility that it collides - // against the edge so we recast against the diagonal plane beteween the two faces. - // NOTE: it is impossible for the startPoint to be in front of the diagonal plane, - // therefore we know we'll get a valid distance. - glm::vec3 thirdNormal = glm::cross(faceNormal, wallNormal); - glm::vec3 diagonalNormal = glm::normalize(glm::cross(glm::cross(capsuleAxis, thirdNormal), thirdNormal)); - newDistance = glm::dot(halfCubeSide * (faceNormal + wallNormal) - capsuleStart, diagonalNormal) / - glm::dot(capsuleAxis, diagonalNormal); - } else if (newDistance < capsuleLength) { - // The wall truncates the capsule axis, but we must check the case where the capsule - // collides with an edge/corner rather than the face. The good news is that this gives us - // an opportunity to check for an early exit case. - float heightOfImpact = glm::dot(capsuleStart + newDistance * capsuleAxis, faceNormal); - if (heightOfImpact > halfCubeSide + SQUARE_ROOT_OF_2 * capsuleRadius) { - // it is impossible for the capsule to hit the face - return false; - } else { - // recast against the diagonal plane between the two faces - glm::vec3 thirdNormal = glm::cross(faceNormal, wallNormal); - glm::vec3 diagonalNormal = glm::normalize(glm::cross(glm::cross(capsuleAxis, thirdNormal), thirdNormal)); - newDistance = glm::dot(halfCubeSide * (faceNormal + wallNormal) - capsuleStart, diagonalNormal) / - glm::dot(capsuleAxis, diagonalNormal); - } - } - if (newDistance < shortestDistance) { - shortestDistance = newDistance; - } - // there can only be one hit per direction - break; - } - } - } - - // chose the point that produces the deepest penetration against face - // and translate back into real frame - glm::vec3 sphereCenter = cubeCenter + capsuleStart + shortestDistance * capsuleAxis; - - // collide like a sphere at point0 with capsuleRadius - CollisionInfo* collision = sphereVsAACubeHelper(sphereCenter, capsuleRadius, - cubeCenter, 2.0f * halfCubeSide, collisions); - if (collision) { - // we hit! so store back pointers to the shapes - collision->_shapeA = shapeA; - collision->_shapeB = shapeB; - return true; - } - return false; -} - -bool aaCubeVsSphere(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - return sphereVsAACube(shapeB, shapeA, collisions); -} - -bool aaCubeVsCapsule(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - return capsuleVsAACube(shapeB, shapeA, collisions); -} - -// helper function -CollisionInfo* aaCubeVsAACubeHelper(const glm::vec3& cubeCenterA, float cubeSideA, const glm::vec3& cubeCenterB, - float cubeSideB, CollisionList& collisions) { - // cube is A - // cube is B - // BA = B - A = from center of A to center of B - float halfCubeSideA = 0.5f * cubeSideA; - float halfCubeSideB = 0.5f * cubeSideB; - glm::vec3 BA = cubeCenterB - cubeCenterA; - - float distance = glm::length(BA); - - if (distance > EPSILON) { - float maxBA = glm::max(glm::max(glm::abs(BA.x), glm::abs(BA.y)), glm::abs(BA.z)); - if (maxBA > halfCubeSideB + halfCubeSideA) { - // cube misses cube entirely - return NULL; - } - CollisionInfo* collision = collisions.getNewCollision(); - if (!collision) { - return NULL; // no more room for collisions - } - if (maxBA > halfCubeSideB) { - // cube hits cube but its center is outside cube - // compute contact anti-pole on cube (in cube frame) - glm::vec3 cubeContact = glm::abs(BA); - if (cubeContact.x > halfCubeSideB) { - cubeContact.x = halfCubeSideB; - } - if (cubeContact.y > halfCubeSideB) { - cubeContact.y = halfCubeSideB; - } - if (cubeContact.z > halfCubeSideB) { - cubeContact.z = halfCubeSideB; - } - glm::vec3 signs = glm::sign(BA); - cubeContact.x *= signs.x; - cubeContact.y *= signs.y; - cubeContact.z *= signs.z; - - // compute penetration direction - glm::vec3 direction = BA - cubeContact; - - float lengthDirection = glm::length(direction); - - if (lengthDirection < EPSILON) { - // cubeCenterA is touching cube B surface, so we can't use the difference between those two - // points to compute the penetration direction. Instead we use the unitary components of - // cubeContact. - glm::modf(cubeContact / halfCubeSideB, direction); - lengthDirection = glm::length(direction); - } else if (lengthDirection > halfCubeSideA) { - collisions.deleteLastCollision(); - return NULL; - } - direction /= lengthDirection; - - // compute collision details - collision->_contactPoint = cubeCenterA + halfCubeSideA * direction; - collision->_penetration = halfCubeSideA * direction - (BA - cubeContact); - } else { - // cube center is inside cube - // --> push out nearest face - glm::vec3 direction; - BA /= maxBA; - glm::modf(BA, direction); - float lengthDirection = glm::length(direction); - direction /= lengthDirection; - - // compute collision details - collision->_floatData = cubeSideB; - collision->_vecData = cubeCenterB; - collision->_penetration = (halfCubeSideB * lengthDirection + halfCubeSideA - maxBA * glm::dot(BA, direction)) * direction; - collision->_contactPoint = cubeCenterA + halfCubeSideA * direction; - } - collision->_shapeA = NULL; - collision->_shapeB = NULL; - return collision; - } else if (halfCubeSideA + halfCubeSideB > distance) { - // NOTE: for cocentric approximation we collide sphere and cube as two spheres which means - // this algorithm will probably be wrong when both sphere and cube are very small (both ~EPSILON) - CollisionInfo* collision = collisions.getNewCollision(); - if (collision) { - // the penetration and contactPoint are undefined, so we pick a penetration direction (-yAxis) - collision->_penetration = (halfCubeSideA + halfCubeSideB) * glm::vec3(0.0f, -1.0f, 0.0f); - // contactPoint is on surface of A - collision->_contactPoint = cubeCenterA + collision->_penetration; - collision->_shapeA = NULL; - collision->_shapeB = NULL; - return collision; - } - } - return NULL; -} - -bool aaCubeVsAACube(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - // BA = B - A = from center of A to center of B - const AACubeShape* cubeA = static_cast(shapeA); - const AACubeShape* cubeB = static_cast(shapeB); - CollisionInfo* collision = aaCubeVsAACubeHelper( cubeA->getTranslation(), cubeA->getScale(), - cubeB->getTranslation(), cubeB->getScale(), collisions); - if (collision) { - collision->_shapeA = shapeA; - collision->_shapeB = shapeB; - return true; - } - return false; -} - -bool shapeVsList(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - bool touching = false; - const ListShape* listB = static_cast(shapeB); - for (int i = 0; i < listB->size() && !collisions.isFull(); ++i) { - const Shape* subShape = listB->getSubShape(i); - touching = collideShapes(shapeA, subShape, collisions) || touching; - } - return touching; -} - -bool listVsShape(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - bool touching = false; - const ListShape* listA = static_cast(shapeA); - for (int i = 0; i < listA->size() && !collisions.isFull(); ++i) { - const Shape* subShape = listA->getSubShape(i); - touching = collideShapes(subShape, shapeB, collisions) || touching; - } - return touching; -} - -bool listVsList(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions) { - bool touching = false; - const ListShape* listA = static_cast(shapeA); - const ListShape* listB = static_cast(shapeB); - for (int i = 0; i < listA->size() && !collisions.isFull(); ++i) { - const Shape* subShape = listA->getSubShape(i); - for (int j = 0; j < listB->size() && !collisions.isFull(); ++j) { - touching = collideShapes(subShape, listB->getSubShape(j), collisions) || touching; - } - } - return touching; -} - -// helper function -/* KEEP THIS CODE -- this is how to collide the cube with stark face normals (no rounding). -* We might want to use this code later for sealing boundaries between adjacent voxels. -bool sphereAACube_StarkAngles(const glm::vec3& sphereCenter, float sphereRadius, const glm::vec3& cubeCenter, - float cubeSide, CollisionList& collisions) { - glm::vec3 BA = cubeCenter - sphereCenter; - float distance = glm::length(BA); - if (distance > EPSILON) { - BA /= distance; // BA is now normalized - // compute the nearest point on sphere - glm::vec3 surfaceA = sphereCenter + sphereRadius * BA; - // compute the nearest point on cube - float maxBA = glm::max(glm::max(fabsf(BA.x), fabsf(BA.y)), fabsf(BA.z)); - glm::vec3 surfaceB = cubeCenter - (0.5f * cubeSide / maxBA) * BA; - // collision happens when "vector to surfaceA from surfaceB" dots with BA to produce a positive value - glm::vec3 surfaceAB = surfaceA - surfaceB; - if (glm::dot(surfaceAB, BA) > 0.0f) { - CollisionInfo* collision = collisions.getNewCollision(); - if (collision) { - // penetration is parallel to box side direction - BA /= maxBA; - glm::vec3 direction; - glm::modf(BA, direction); - direction = glm::normalize(direction); - - // penetration is the projection of surfaceAB on direction - collision->_penetration = glm::dot(surfaceAB, direction) * direction; - // contactPoint is on surface of A - collision->_contactPoint = sphereCenter + sphereRadius * direction; - collision->_shapeA = NULL; - collision->_shapeB = NULL; - return true; - } - } - } else if (sphereRadius + 0.5f * cubeSide > distance) { - // NOTE: for cocentric approximation we collide sphere and cube as two spheres which means - // this algorithm will probably be wrong when both sphere and cube are very small (both ~EPSILON) - CollisionInfo* collision = collisions.getNewCollision(); - if (collision) { - // the penetration and contactPoint are undefined, so we pick a penetration direction (-yAxis) - collision->_penetration = (sphereRadius + 0.5f * cubeSide) * glm::vec3(0.0f, -1.0f, 0.0f); - // contactPoint is on surface of A - collision->_contactPoint = sphereCenter + collision->_penetration; - collision->_shapeA = NULL; - collision->_shapeB = NULL; - return true; - } - } - return false; -} -*/ - -bool sphereVsAACubeLegacy(const SphereShape* sphereA, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions) { - return sphereVsAACubeLegacy(sphereA->getTranslation(), sphereA->getRadius(), cubeCenter, cubeSide, collisions); -} - -bool capsuleVsAACubeLegacy(const CapsuleShape* capsuleA, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions) { - // find nerest approach of capsule line segment to cube - glm::vec3 capsuleAxis; - capsuleA->computeNormalizedAxis(capsuleAxis); - float offset = glm::dot(cubeCenter - capsuleA->getTranslation(), capsuleAxis); - float halfHeight = capsuleA->getHalfHeight(); - if (offset > halfHeight) { - offset = halfHeight; - } else if (offset < -halfHeight) { - offset = -halfHeight; - } - glm::vec3 nearestApproach = capsuleA->getTranslation() + offset * capsuleAxis; - // collide nearest approach like a sphere at that point - return sphereVsAACubeLegacy(nearestApproach, capsuleA->getRadius(), cubeCenter, cubeSide, collisions); -} - -bool findRayIntersection(const QVector& shapes, RayIntersectionInfo& intersection) { - int numShapes = shapes.size(); - bool hit = false; - for (int i = 0; i < numShapes; ++i) { - Shape* shape = shapes.at(i); - if (shape) { - if (shape->findRayIntersection(intersection)) { - hit = true; - } - } - } - return hit; -} - -} // namespace ShapeCollider diff --git a/libraries/shared/src/ShapeCollider.h b/libraries/shared/src/ShapeCollider.h deleted file mode 100644 index da37adcd49..0000000000 --- a/libraries/shared/src/ShapeCollider.h +++ /dev/null @@ -1,156 +0,0 @@ -// -// ShapeCollider.h -// libraries/shared/src -// -// Created by Andrew Meadows on 02/20/2014. -// 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_ShapeCollider_h -#define hifi_ShapeCollider_h - -#include - -#include "CollisionInfo.h" -#include "RayIntersectionInfo.h" -#include "SharedUtil.h" - - -class Shape; -class SphereShape; -class CapsuleShape; - -namespace ShapeCollider { - - /// MUST CALL this FIRST before using the ShapeCollider - void initDispatchTable(); - - /// \param shapeA pointer to first shape (cannot be NULL) - /// \param shapeB pointer to second shape (cannot be NULL) - /// \param collisions[out] collision details - /// \return true if shapes collide - bool collideShapes(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions); - - bool collideShapeWithShapes(const Shape* shapeA, const QVector& shapes, int startIndex, CollisionList& collisions); - bool collideShapesWithShapes(const QVector& shapesA, const QVector& shapesB, CollisionList& collisions); - - /// \param shapeA a pointer to a shape (cannot be NULL) - /// \param cubeCenter center of cube - /// \param cubeSide lenght of side of cube - /// \param collisions[out] average collision details - /// \return true if shapeA collides with axis aligned cube - bool collideShapeWithAACubeLegacy(const Shape* shapeA, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions); - - /// \param sphereA pointer to first shape (cannot be NULL) - /// \param sphereB pointer to second shape (cannot be NULL) - /// \param[out] collisions where to append collision details - /// \return true if shapes collide - bool sphereVsSphere(const Shape* sphereA, const Shape* sphereB, CollisionList& collisions); - - /// \param sphereA pointer to first shape (cannot be NULL) - /// \param capsuleB pointer to second shape (cannot be NULL) - /// \param[out] collisions where to append collision details - /// \return true if shapes collide - bool sphereVsCapsule(const Shape* sphereA, const Shape* capsuleB, CollisionList& collisions); - - /// \param sphereA pointer to first shape (cannot be NULL) - /// \param planeB pointer to second shape (cannot be NULL) - /// \param[out] collisions where to append collision details - /// \return true if shapes collide - bool sphereVsPlane(const Shape* sphereA, const Shape* planeB, CollisionList& collisions); - - /// \param capsuleA pointer to first shape (cannot be NULL) - /// \param sphereB pointer to second shape (cannot be NULL) - /// \param[out] collisions where to append collision details - /// \return true if shapes collide - bool capsuleVsSphere(const Shape* capsuleA, const Shape* sphereB, CollisionList& collisions); - - /// \param capsuleA pointer to first shape (cannot be NULL) - /// \param capsuleB pointer to second shape (cannot be NULL) - /// \param[out] collisions where to append collision details - /// \return true if shapes collide - bool capsuleVsCapsule(const Shape* capsuleA, const Shape* capsuleB, CollisionList& collisions); - - /// \param capsuleA pointer to first shape (cannot be NULL) - /// \param planeB pointer to second shape (cannot be NULL) - /// \param[out] collisions where to append collision details - /// \return true if shapes collide - bool capsuleVsPlane(const Shape* capsuleA, const Shape* planeB, CollisionList& collisions); - - /// \param planeA pointer to first shape (cannot be NULL) - /// \param sphereB pointer to second shape (cannot be NULL) - /// \param[out] collisions where to append collision details - /// \return true if shapes collide - bool planeVsSphere(const Shape* planeA, const Shape* sphereB, CollisionList& collisions); - - /// \param planeA pointer to first shape (cannot be NULL) - /// \param capsuleB pointer to second shape (cannot be NULL) - /// \param[out] collisions where to append collision details - /// \return true if shapes collide - bool planeVsCapsule(const Shape* planeA, const Shape* capsuleB, CollisionList& collisions); - - /// \param planeA pointer to first shape (cannot be NULL) - /// \param planeB pointer to second shape (cannot be NULL) - /// \param[out] collisions where to append collision details - /// \return true if shapes collide - bool planeVsPlane(const Shape* planeA, const Shape* planeB, CollisionList& collisions); - - /// helper function for *VsAACube() methods - /// \param sphereCenter center of sphere - /// \param sphereRadius radius of sphere - /// \param cubeCenter center of AACube - /// \param cubeSide scale of cube - /// \param[out] collisions where to append collision details - /// \return valid pointer to CollisionInfo if sphere and cube overlap or NULL if not - CollisionInfo* sphereVsAACubeHelper(const glm::vec3& sphereCenter, float sphereRadius, - const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions); - - bool sphereVsAACube(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions); - bool capsuleVsAACube(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions); - bool aaCubeVsSphere(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions); - bool aaCubeVsCapsule(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions); - bool aaCubeVsAACube(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions); - - /// \param shapeA pointer to first shape (cannot be NULL) - /// \param listB pointer to second shape (cannot be NULL) - /// \param[out] collisions where to append collision details - /// \return true if shapes collide - bool shapeVsList(const Shape* shapeA, const Shape* listB, CollisionList& collisions); - - /// \param listA pointer to first shape (cannot be NULL) - /// \param shapeB pointer to second shape (cannot be NULL) - /// \param[out] collisions where to append collision details - /// \return true if shapes collide - bool listVsShape(const Shape* listA, const Shape* shapeB, CollisionList& collisions); - - /// \param listA pointer to first shape (cannot be NULL) - /// \param listB pointer to second shape (cannot be NULL) - /// \param[out] collisions where to append collision details - /// \return true if shapes collide - bool listVsList(const Shape* listA, const Shape* listB, CollisionList& collisions); - - /// \param sphereA pointer to sphere (cannot be NULL) - /// \param cubeCenter center of cube - /// \param cubeSide lenght of side of cube - /// \param[out] collisions where to append collision details - /// \return true if sphereA collides with axis aligned cube - bool sphereVsAACubeLegacy(const SphereShape* sphereA, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions); - - /// \param capsuleA pointer to capsule (cannot be NULL) - /// \param cubeCenter center of cube - /// \param cubeSide lenght of side of cube - /// \param[out] collisions where to append collision details - /// \return true if capsuleA collides with axis aligned cube - bool capsuleVsAACubeLegacy(const CapsuleShape* capsuleA, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions); - - /// \param shapes list of pointers to shapes (shape pointers may be NULL) - /// \param intersection[out] struct with info about Ray and hit - /// \return true if ray hits any shape in shapes - bool findRayIntersection(const QVector& shapes, RayIntersectionInfo& intersection); - -} // namespace ShapeCollider - -#endif // hifi_ShapeCollider_h diff --git a/libraries/shared/src/SphereShape.cpp b/libraries/shared/src/SphereShape.cpp deleted file mode 100644 index 4c47ae91c0..0000000000 --- a/libraries/shared/src/SphereShape.cpp +++ /dev/null @@ -1,51 +0,0 @@ -// -// SphereShape.cpp -// libraries/shared/src -// -// Created by Andrew Meadows on 2014.06.17 -// 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 "SphereShape.h" - -bool SphereShape::findRayIntersection(RayIntersectionInfo& intersection) const { - float r2 = _boundingRadius * _boundingRadius; - - // compute closest approach (CA) - float a = glm::dot(_translation - intersection._rayStart, intersection._rayDirection); // a = distance from ray-start to CA - float b2 = glm::distance2(_translation, intersection._rayStart + a * intersection._rayDirection); // b2 = squared distance from sphere-center to CA - if (b2 > r2) { - // ray does not hit sphere - return false; - } - float c = sqrtf(r2 - b2); // c = distance from CA to sphere surface along intersection._rayDirection - float d2 = glm::distance2(intersection._rayStart, _translation); // d2 = squared distance from sphere-center to ray-start - float distance = FLT_MAX; - if (a < 0.0f) { - // ray points away from sphere-center - if (d2 > r2) { - // ray starts outside sphere - return false; - } - // ray starts inside sphere - distance = c + a; - } else if (d2 > r2) { - // ray starts outside sphere - distance = a - c; - } else { - // ray starts inside sphere - distance = a + c; - } - if (distance > 0.0f && distance < intersection._rayLength && distance < intersection._hitDistance) { - intersection._hitDistance = distance; - intersection._hitNormal = glm::normalize(intersection._rayStart + distance * intersection._rayDirection - _translation); - intersection._hitShape = const_cast(this); - return true; - } - return false; -} diff --git a/libraries/shared/src/SphereShape.h b/libraries/shared/src/SphereShape.h deleted file mode 100644 index 964881a715..0000000000 --- a/libraries/shared/src/SphereShape.h +++ /dev/null @@ -1,63 +0,0 @@ -// -// SphereShape.h -// libraries/shared/src -// -// Created by Andrew Meadows on 2014. -// 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_SphereShape_h -#define hifi_SphereShape_h - -#include "NumericalConstants.h" -#include "Shape.h" - -class SphereShape : public Shape { -public: - SphereShape() : Shape(SPHERE_SHAPE) {} - - SphereShape(float radius) : Shape(SPHERE_SHAPE) { - _boundingRadius = radius; - } - - SphereShape(float radius, const glm::vec3& position) : Shape(SPHERE_SHAPE, position) { - _boundingRadius = radius; - } - - virtual ~SphereShape() {} - - float getRadius() const { return _boundingRadius; } - - void setRadius(float radius) { _boundingRadius = radius; } - - bool findRayIntersection(RayIntersectionInfo& intersection) const; - - float getVolume() const { return 1.3333333333f * PI * _boundingRadius * _boundingRadius * _boundingRadius; } -}; - -inline QDebug operator<<(QDebug debug, const SphereShape& shape) { - debug << "SphereShape[ (" - << "position: " - << shape.getTranslation().x << ", " << shape.getTranslation().y << ", " << shape.getTranslation().z - << "radius: " - << shape.getRadius() - << "]"; - - return debug; -} - -inline QDebug operator<<(QDebug debug, const SphereShape* shape) { - debug << "SphereShape[ (" - << "center: " - << shape->getTranslation().x << ", " << shape->getTranslation().y << ", " << shape->getTranslation().z - << "radius: " - << shape->getRadius() - << "]"; - - return debug; -} - -#endif // hifi_SphereShape_h diff --git a/libraries/shared/src/StreamUtils.cpp b/libraries/shared/src/StreamUtils.cpp index 5726728f30..d4378d82b3 100644 --- a/libraries/shared/src/StreamUtils.cpp +++ b/libraries/shared/src/StreamUtils.cpp @@ -65,38 +65,6 @@ QDataStream& operator>>(QDataStream& in, glm::quat& quaternion) { return in >> quaternion.x >> quaternion.y >> quaternion.z >> quaternion.w; } -// less common utils can be enabled with DEBUG -// FIXME, remove the second defined clause once these compile, or remove the -// functions. -#if defined(DEBUG) && defined(FIXED_STREAMS) - -std::ostream& operator<<(std::ostream& s, const CollisionInfo& c) { - s << "{penetration=" << c._penetration - << ", contactPoint=" << c._contactPoint - << ", addedVelocity=" << c._addedVelocity - << "}"; - return s; -} - -std::ostream& operator<<(std::ostream& s, const SphereShape& sphere) { - s << "{type='sphere', center=" << sphere.getPosition() - << ", radius=" << sphere.getRadius() - << "}"; - return s; -} - -std::ostream& operator<<(std::ostream& s, const CapsuleShape& capsule) { - s << "{type='capsule', center=" << capsule.getPosition() - << ", radius=" << capsule.getRadius() - << ", length=" << (2.0f * capsule.getHalfHeight()) - << ", begin=" << capsule.getStartPoint() - << ", end=" << capsule.getEndPoint() - << "}"; - return s; -} - -#endif // DEBUG - #ifndef QT_NO_DEBUG_STREAM #include diff --git a/libraries/shared/src/StreamUtils.h b/libraries/shared/src/StreamUtils.h index fd22f7c068..caf7b565f4 100644 --- a/libraries/shared/src/StreamUtils.h +++ b/libraries/shared/src/StreamUtils.h @@ -37,16 +37,6 @@ QDataStream& operator>>(QDataStream& in, glm::vec3& vector); QDataStream& operator<<(QDataStream& out, const glm::quat& quaternion); QDataStream& operator>>(QDataStream& in, glm::quat& quaternion); -// less common utils can be enabled with DEBUG -#ifdef DEBUG -class CollisionInfo; -class SphereShape; -class CapsuleShape; -std::ostream& operator<<(std::ostream& s, const CollisionInfo& c); -std::ostream& operator<<(std::ostream& s, const SphereShape& shape); -std::ostream& operator<<(std::ostream& s, const CapsuleShape& capsule); -#endif // DEBUG - #ifndef QT_NO_DEBUG_STREAM class QDebug; // Add support for writing these to qDebug(). diff --git a/tests/physics/src/CollisionInfoTests.cpp b/tests/physics/src/CollisionInfoTests.cpp deleted file mode 100644 index 70e2e14bb0..0000000000 --- a/tests/physics/src/CollisionInfoTests.cpp +++ /dev/null @@ -1,70 +0,0 @@ -// -// CollisionInfoTests.cpp -// tests/physics/src -// -// Created by Andrew Meadows on 2/21/2014. -// 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 -#include -#include - -#include "CollisionInfoTests.h" - - - -QTEST_MAIN(CollisionInfoTests) -/* -static glm::vec3 xAxis(1.0f, 0.0f, 0.0f); -static glm::vec3 xZxis(0.0f, 1.0f, 0.0f); -static glm::vec3 xYxis(0.0f, 0.0f, 1.0f); - -void CollisionInfoTests::rotateThenTranslate() { - CollisionInfo collision; - collision._penetration = xAxis; - collision._contactPoint = xYxis; - collision._addedVelocity = xAxis + xYxis + xZxis; - - glm::quat rotation = glm::angleAxis(PI_OVER_TWO, zAxis); - float distance = 3.0f; - glm::vec3 translation = distance * xYxis; - - collision.rotateThenTranslate(rotation, translation); - QCOMPARE(collision._penetration, xYxis); - - glm::vec3 expectedContactPoint = -xAxis + translation; - QCOMPARE(collision._contactPoint, expectedContactPoint); - - glm::vec3 expectedAddedVelocity = xYxis - xAxis + xZxis; - QCOMPARE(collision._addedVelocity, expectedAddedVelocity); -} - -void CollisionInfoTests::translateThenRotate() { - CollisionInfo collision; - collision._penetration = xAxis; - collision._contactPoint = xYxis; - collision._addedVelocity = xAxis + xYxis + xZxis; - - glm::quat rotation = glm::angleAxis( -PI_OVER_TWO, zAxis); - float distance = 3.0f; - glm::vec3 translation = distance * xYxis; - - collision.translateThenRotate(translation, rotation); - QCOMPARE(collision._penetration, -xYxis); - - glm::vec3 expectedContactPoint = (1.0f + distance) * xAxis; - QCOMPARE(collision._contactPoint, expectedContactPoint); - - glm::vec3 expectedAddedVelocity = - xYxis + xAxis + xYxis; - QCOMPARE(collision._addedVelocity, expectedAddedVelocity); -}*/ - diff --git a/tests/physics/src/CollisionInfoTests.h b/tests/physics/src/CollisionInfoTests.h deleted file mode 100644 index d26d39be4b..0000000000 --- a/tests/physics/src/CollisionInfoTests.h +++ /dev/null @@ -1,29 +0,0 @@ -// -// CollisionInfoTests.h -// tests/physics/src -// -// Created by Andrew Meadows on 2/21/2014. -// 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_CollisionInfoTests_h -#define hifi_CollisionInfoTests_h - -#include - -// Add additional qtest functionality (the include order is important!) -#include "GlmTestUtils.h" -#include "../QTestExtensions.h" - -class CollisionInfoTests : public QObject { - Q_OBJECT - -private slots: -// void rotateThenTranslate(); -// void translateThenRotate(); -}; - -#endif // hifi_CollisionInfoTests_h diff --git a/tests/physics/src/ShapeColliderTests.cpp b/tests/physics/src/ShapeColliderTests.cpp deleted file mode 100644 index cb42f534cb..0000000000 --- a/tests/physics/src/ShapeColliderTests.cpp +++ /dev/null @@ -1,1936 +0,0 @@ -// -// ShapeColliderTests.cpp -// tests/physics/src -// -// Created by Andrew Meadows on 02/21/2014. -// 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 -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ShapeColliderTests.h" - - -const glm::vec3 origin(0.0f); -static const glm::vec3 xAxis(1.0f, 0.0f, 0.0f); -static const glm::vec3 yAxis(0.0f, 1.0f, 0.0f); -static const glm::vec3 zAxis(0.0f, 0.0f, 1.0f); - -QTEST_MAIN(ShapeColliderTests) - -void ShapeColliderTests::initTestCase() { - ShapeCollider::initDispatchTable(); -} - -void ShapeColliderTests::sphereMissesSphere() { - // non-overlapping spheres of unequal size - float radiusA = 7.0f; - float radiusB = 3.0f; - float alpha = 1.2f; - float beta = 1.3f; - glm::vec3 offsetDirection = glm::normalize(glm::vec3(1.0f, 2.0f, 3.0f)); - float offsetDistance = alpha * radiusA + beta * radiusB; - - SphereShape sphereA(radiusA, origin); - SphereShape sphereB(radiusB, offsetDistance * offsetDirection); - CollisionList collisions(16); - - // collide A to B and vice versa - QCOMPARE(ShapeCollider::collideShapes(&sphereA, &sphereB, collisions), false); - QCOMPARE(ShapeCollider::collideShapes(&sphereB, &sphereA, collisions), false); - - // Collision list should be empty - QCOMPARE(collisions.size(), 0); -} - -void ShapeColliderTests::sphereTouchesSphere() { - // overlapping spheres of unequal size - float radiusA = 7.0f; - float radiusB = 3.0f; - float alpha = 0.2f; - float beta = 0.3f; - glm::vec3 offsetDirection = glm::normalize(glm::vec3(1.0f, 2.0f, 3.0f)); - float offsetDistance = alpha * radiusA + beta * radiusB; - float expectedPenetrationDistance = (1.0f - alpha) * radiusA + (1.0f - beta) * radiusB; - glm::vec3 expectedPenetration = expectedPenetrationDistance * offsetDirection; - - SphereShape sphereA(radiusA, origin); - SphereShape sphereB(radiusB, offsetDistance * offsetDirection); - CollisionList collisions(16); - int numCollisions = 0; - - // collide A to B... - { - QCOMPARE(ShapeCollider::collideShapes(&sphereA, &sphereB, collisions), true); - ++numCollisions; - - // verify state of collisions - QCOMPARE(collisions.size(), numCollisions); - CollisionInfo* collision = collisions.getCollision(numCollisions - 1); - QVERIFY(collision != nullptr); - - // penetration points from sphereA into sphereB - QCOMPARE(collision->_penetration, expectedPenetration); - - // contactPoint is on surface of sphereA - glm::vec3 AtoB = sphereB.getTranslation() - sphereA.getTranslation(); - glm::vec3 expectedContactPoint = sphereA.getTranslation() + radiusA * glm::normalize(AtoB); - QCOMPARE(collision->_contactPoint, expectedContactPoint); - - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, EPSILON); - } - - // collide B to A... - { - QCOMPARE(ShapeCollider::collideShapes(&sphereB, &sphereA, collisions), true); - ++numCollisions; - - // penetration points from sphereA into sphereB - CollisionInfo* collision = collisions.getCollision(numCollisions - 1); - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, -expectedPenetration, EPSILON); - - // contactPoint is on surface of sphereB - glm::vec3 BtoA = sphereA.getTranslation() - sphereB.getTranslation(); - glm::vec3 expectedContactPoint = sphereB.getTranslation() + radiusB * glm::normalize(BtoA); - - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, EPSILON); - } -} - -void ShapeColliderTests::sphereMissesCapsule() { - // non-overlapping sphere and capsule - float radiusA = 1.5f; - float radiusB = 2.3f; - float totalRadius = radiusA + radiusB; - float halfHeightB = 1.7f; - float axialOffset = totalRadius + 1.1f * halfHeightB; - float radialOffset = 1.2f * radiusA + 1.3f * radiusB; - - SphereShape sphereA(radiusA); - CapsuleShape capsuleB(radiusB, halfHeightB); - - // give the capsule some arbirary transform - float angle = 37.8f; - glm::vec3 axis = glm::normalize( glm::vec3(-7.0f, 2.8f, 9.3f) ); - glm::quat rotation = glm::angleAxis(angle, axis); - glm::vec3 translation(15.1f, -27.1f, -38.6f); - capsuleB.setRotation(rotation); - capsuleB.setTranslation(translation); - - CollisionList collisions(16); - - // walk sphereA along the local yAxis next to, but not touching, capsuleB - glm::vec3 localStartPosition(radialOffset, axialOffset, 0.0f); - int numberOfSteps = 10; - float delta = 1.3f * (totalRadius + halfHeightB) / (numberOfSteps - 1); - for (int i = 0; i < numberOfSteps; ++i) { - // translate sphereA into world-frame - glm::vec3 localPosition = localStartPosition + ((float)i * delta) * yAxis; - sphereA.setTranslation(rotation * localPosition + translation); - - // sphereA agains capsuleB and vice versa - QCOMPARE(ShapeCollider::collideShapes(&sphereA, &capsuleB, collisions), false); - QCOMPARE(ShapeCollider::collideShapes(&capsuleB, &sphereA, collisions), false); - } - - QCOMPARE(collisions.size(), 0); -} - -void ShapeColliderTests::sphereTouchesCapsule() { - // overlapping sphere and capsule - float radiusA = 2.0f; - float radiusB = 1.0f; - float totalRadius = radiusA + radiusB; - float halfHeightB = 2.0f; - float alpha = 0.5f; - float beta = 0.5f; - float radialOffset = alpha * radiusA + beta * radiusB; - - SphereShape sphereA(radiusA); - CapsuleShape capsuleB(radiusB, halfHeightB); - - CollisionList collisions(16); - int numCollisions = 0; - - { // sphereA collides with capsuleB's cylindrical wall - sphereA.setTranslation(radialOffset * xAxis); - - QCOMPARE(ShapeCollider::collideShapes(&sphereA, &capsuleB, collisions), true); - ++numCollisions; - - // penetration points from sphereA into capsuleB - CollisionInfo* collision = collisions.getCollision(numCollisions - 1); - glm::vec3 expectedPenetration = (radialOffset - totalRadius) * xAxis; - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, EPSILON); - - // contactPoint is on surface of sphereA - glm::vec3 expectedContactPoint = sphereA.getTranslation() - radiusA * xAxis; - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, EPSILON); - - // capsuleB collides with sphereA - QCOMPARE(ShapeCollider::collideShapes(&capsuleB, &sphereA, collisions), true); - ++numCollisions; - - // penetration points from sphereA into capsuleB - collision = collisions.getCollision(numCollisions - 1); - expectedPenetration = - (radialOffset - totalRadius) * xAxis; - if (collision->_shapeA == &sphereA) { - // the ShapeCollider swapped the order of the shapes - expectedPenetration *= -1.0f; - } - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, EPSILON); - - // contactPoint is on surface of capsuleB - glm::vec3 BtoA = sphereA.getTranslation() - capsuleB.getTranslation(); - glm::vec3 closestApproach = capsuleB.getTranslation() + glm::dot(BtoA, yAxis) * yAxis; - expectedContactPoint = closestApproach + radiusB * glm::normalize(BtoA - closestApproach); - if (collision->_shapeA == &sphereA) { - // the ShapeCollider swapped the order of the shapes - closestApproach = sphereA.getTranslation() - glm::dot(BtoA, yAxis) * yAxis; - expectedContactPoint = closestApproach - radiusB * glm::normalize(BtoA - closestApproach); - } - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, EPSILON); - } - { // sphereA hits end cap at axis - glm::vec3 axialOffset = (halfHeightB + alpha * radiusA + beta * radiusB) * yAxis; - sphereA.setTranslation(axialOffset); - - QCOMPARE(ShapeCollider::collideShapes(&sphereA, &capsuleB, collisions), true); - ++numCollisions; - - // penetration points from sphereA into capsuleB - CollisionInfo* collision = collisions.getCollision(numCollisions - 1); - glm::vec3 expectedPenetration = - ((1.0f - alpha) * radiusA + (1.0f - beta) * radiusB) * yAxis; - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, EPSILON); - - // contactPoint is on surface of sphereA - glm::vec3 expectedContactPoint = sphereA.getTranslation() - radiusA * yAxis; - - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, EPSILON); - - // capsuleB collides with sphereA - if (!ShapeCollider::collideShapes(&capsuleB, &sphereA, collisions)) - { - std::cout << __FILE__ << ":" << __LINE__ - << " ERROR: capsule and sphere should touch" << std::endl; - } else { - ++numCollisions; - } - - // penetration points from sphereA into capsuleB - collision = collisions.getCollision(numCollisions - 1); - expectedPenetration = ((1.0f - alpha) * radiusA + (1.0f - beta) * radiusB) * yAxis; - if (collision->_shapeA == &sphereA) { - // the ShapeCollider swapped the order of the shapes - expectedPenetration *= -1.0f; - } - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, EPSILON); - - // contactPoint is on surface of capsuleB - glm::vec3 endPoint; - capsuleB.getEndPoint(endPoint); - expectedContactPoint = endPoint + radiusB * yAxis; - if (collision->_shapeA == &sphereA) { - // the ShapeCollider swapped the order of the shapes - expectedContactPoint = axialOffset - radiusA * yAxis; - } - - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, EPSILON); - } - { // sphereA hits start cap at axis - glm::vec3 axialOffset = - (halfHeightB + alpha * radiusA + beta * radiusB) * yAxis; - sphereA.setTranslation(axialOffset); - - if (!ShapeCollider::collideShapes(&sphereA, &capsuleB, collisions)) - { - std::cout << __FILE__ << ":" << __LINE__ - << " ERROR: sphere and capsule should touch" << std::endl; - } else { - ++numCollisions; - } - - // penetration points from sphereA into capsuleB - CollisionInfo* collision = collisions.getCollision(numCollisions - 1); - glm::vec3 expectedPenetration = ((1.0f - alpha) * radiusA + (1.0f - beta) * radiusB) * yAxis; - - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, EPSILON); - - // contactPoint is on surface of sphereA - glm::vec3 expectedContactPoint = sphereA.getTranslation() + radiusA * yAxis; - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, EPSILON); - - // capsuleB collides with sphereA - if (!ShapeCollider::collideShapes(&capsuleB, &sphereA, collisions)) - { - std::cout << __FILE__ << ":" << __LINE__ - << " ERROR: capsule and sphere should touch" << std::endl; - } else { - ++numCollisions; - } - - // penetration points from sphereA into capsuleB - collision = collisions.getCollision(numCollisions - 1); - expectedPenetration = - ((1.0f - alpha) * radiusA + (1.0f - beta) * radiusB) * yAxis; - if (collision->_shapeA == &sphereA) { - // the ShapeCollider swapped the order of the shapes - expectedPenetration *= -1.0f; - } - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, EPSILON); - - // contactPoint is on surface of capsuleB - glm::vec3 startPoint; - capsuleB.getStartPoint(startPoint); - expectedContactPoint = startPoint - radiusB * yAxis; - if (collision->_shapeA == &sphereA) { - // the ShapeCollider swapped the order of the shapes - expectedContactPoint = axialOffset + radiusA * yAxis; - } - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, EPSILON); - } - if (collisions.size() != numCollisions) { - std::cout << __FILE__ << ":" << __LINE__ - << " ERROR: expected " << numCollisions << " collisions but actual number is " << collisions.size() - << std::endl; - } -} - -void ShapeColliderTests::capsuleMissesCapsule() { - // non-overlapping capsules - float radiusA = 2.0f; - float halfHeightA = 3.0f; - float radiusB = 3.0f; - float halfHeightB = 4.0f; - - float totalRadius = radiusA + radiusB; - float totalHalfLength = totalRadius + halfHeightA + halfHeightB; - - CapsuleShape capsuleA(radiusA, halfHeightA); - CapsuleShape capsuleB(radiusB, halfHeightB); - - CollisionList collisions(16); - - // side by side - capsuleB.setTranslation((1.01f * totalRadius) * xAxis); - QCOMPARE(ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions), false); - QCOMPARE(ShapeCollider::collideShapes(&capsuleB, &capsuleA, collisions), false); - - // end to end - capsuleB.setTranslation((1.01f * totalHalfLength) * xAxis); - QCOMPARE(ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions), false); - QCOMPARE(ShapeCollider::collideShapes(&capsuleB, &capsuleA, collisions), false); - - // rotate B and move it to the side - glm::quat rotation = glm::angleAxis(PI_OVER_TWO, zAxis); - capsuleB.setRotation(rotation); - capsuleB.setTranslation((1.01f * (totalRadius + capsuleB.getHalfHeight())) * xAxis); - - QCOMPARE(ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions), false); - QCOMPARE(ShapeCollider::collideShapes(&capsuleB, &capsuleA, collisions), false); - - QCOMPARE(collisions.size(), 0); -} - -void ShapeColliderTests::capsuleTouchesCapsule() { - // overlapping capsules - float radiusA = 2.0f; - float halfHeightA = 3.0f; - float radiusB = 3.0f; - float halfHeightB = 4.0f; - - float totalRadius = radiusA + radiusB; - float totalHalfLength = totalRadius + halfHeightA + halfHeightB; - - CapsuleShape capsuleA(radiusA, halfHeightA); - CapsuleShape capsuleB(radiusB, halfHeightB); - - CollisionList collisions(16); - int numCollisions = 0; - - { // side by side - capsuleB.setTranslation((0.99f * totalRadius) * xAxis); - QCOMPARE(ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions), true); - QCOMPARE(ShapeCollider::collideShapes(&capsuleB, &capsuleA, collisions), true); - numCollisions += 2; - } - - { // end to end - capsuleB.setTranslation((0.99f * totalHalfLength) * yAxis); - - QCOMPARE(ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions), true); - QCOMPARE(ShapeCollider::collideShapes(&capsuleB, &capsuleA, collisions), true); - numCollisions += 2; - } - - { // rotate B and move it to the side - glm::quat rotation = glm::angleAxis(PI_OVER_TWO, zAxis); - capsuleB.setRotation(rotation); - capsuleB.setTranslation((0.99f * (totalRadius + capsuleB.getHalfHeight())) * xAxis); - - QCOMPARE(ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions), true); - QCOMPARE(ShapeCollider::collideShapes(&capsuleB, &capsuleA, collisions), true); - numCollisions += 2; - } - - { // again, but this time check collision details - float overlap = 0.1f; - glm::quat rotation = glm::angleAxis(PI_OVER_TWO, zAxis); - capsuleB.setRotation(rotation); - glm::vec3 positionB = ((totalRadius + capsuleB.getHalfHeight()) - overlap) * xAxis; - capsuleB.setTranslation(positionB); - - // capsuleA vs capsuleB - QCOMPARE(ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions), true); - ++numCollisions; - - CollisionInfo* collision = collisions.getCollision(numCollisions - 1); - glm::vec3 expectedPenetration = overlap * xAxis; - - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, EPSILON); - - glm::vec3 expectedContactPoint = capsuleA.getTranslation() + radiusA * xAxis; - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, EPSILON); - - // capsuleB vs capsuleA - QCOMPARE(ShapeCollider::collideShapes(&capsuleB, &capsuleA, collisions), true); - ++numCollisions; - - collision = collisions.getCollision(numCollisions - 1); - expectedPenetration = - overlap * xAxis; - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, EPSILON); - - expectedContactPoint = capsuleB.getTranslation() - (radiusB + halfHeightB) * xAxis; - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, EPSILON); - } - - { // collide cylinder wall against cylinder wall - float overlap = 0.137f; - float shift = 0.317f * halfHeightA; - glm::quat rotation = glm::angleAxis(PI_OVER_TWO, zAxis); - capsuleB.setRotation(rotation); - glm::vec3 positionB = (totalRadius - overlap) * zAxis + shift * yAxis; - capsuleB.setTranslation(positionB); - - // capsuleA vs capsuleB - QCOMPARE(ShapeCollider::collideShapes(&capsuleA, &capsuleB, collisions), true); - ++numCollisions; - - CollisionInfo* collision = collisions.getCollision(numCollisions - 1); - glm::vec3 expectedPenetration = overlap * zAxis; - - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, EPSILON); - - glm::vec3 expectedContactPoint = capsuleA.getTranslation() + radiusA * zAxis + shift * yAxis; - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, EPSILON); - } -} - -void ShapeColliderTests::sphereMissesAACube() { - CollisionList collisions(16); - - float sphereRadius = 1.0f; - glm::vec3 sphereCenter(0.0f); - - glm::vec3 cubeCenter(1.5f, 0.0f, 0.0f); - - float cubeSide = 2.0f; - - glm::vec3 faceNormals[] = {xAxis, yAxis, zAxis}; - int numDirections = 3; - - float offset = 2.0f * EPSILON; - - // faces - for (int i = 0; i < numDirections; ++i) { - for (float sign = -1.0f; sign < 2.0f; sign += 2.0f) { - glm::vec3 faceNormal = sign * faceNormals[i]; - - sphereCenter = cubeCenter + (0.5f * cubeSide + sphereRadius + offset) * faceNormal; - - CollisionInfo* collision = ShapeCollider::sphereVsAACubeHelper(sphereCenter, sphereRadius, - cubeCenter, cubeSide, collisions); - - if (collision) { - QFAIL_WITH_MESSAGE("sphere should NOT collide with cube face.\n\t\t" - << "faceNormal = " << faceNormal); -// std::cout << __FILE__ << ":" << __LINE__ << " ERROR: sphere should NOT collide with cube face." -// << " faceNormal = " << faceNormal << std::endl; - } - } - } - - // edges - int numSteps = 5; - // loop over each face... - for (int i = 0; i < numDirections; ++i) { - for (float faceSign = -1.0f; faceSign < 2.0f; faceSign += 2.0f) { - glm::vec3 faceNormal = faceSign * faceNormals[i]; - - // loop over each neighboring face... - for (int j = (i + 1) % numDirections; j != i; j = (j + 1) % numDirections) { - // Compute the index to the third direction, which points perpendicular to both the face - // and the neighbor face. - int k = (j + 1) % numDirections; - if (k == i) { - k = (i + 1) % numDirections; - } - glm::vec3 thirdNormal = faceNormals[k]; - - for (float neighborSign = -1.0f; neighborSign < 2.0f; neighborSign += 2.0f) { - collisions.clear(); - glm::vec3 neighborNormal = neighborSign * faceNormals[j]; - - // combine the face and neighbor normals to get the edge normal - glm::vec3 edgeNormal = glm::normalize(faceNormal + neighborNormal); - // Step the sphere along the edge in the direction of thirdNormal, starting at one corner and - // moving to the other. Test the penetration (invarient) and contact (changing) at each point. - float delta = cubeSide / (float)(numSteps - 1); - glm::vec3 startPosition = cubeCenter + (0.5f * cubeSide) * (faceNormal + neighborNormal - thirdNormal); - for (int m = 0; m < numSteps; ++m) { - sphereCenter = startPosition + ((float)m * delta) * thirdNormal + (sphereRadius + offset) * edgeNormal; - - CollisionInfo* collision = ShapeCollider::sphereVsAACubeHelper(sphereCenter, sphereRadius, - cubeCenter, cubeSide, collisions); - if (collision) { - QFAIL_WITH_MESSAGE("sphere should NOT collide with cube edge.\n\t\t" - << "edgeNormal = " << edgeNormal); - } - } - - } - } - } - } - - // corners - for (float firstSign = -1.0f; firstSign < 2.0f; firstSign += 2.0f) { - glm::vec3 firstNormal = firstSign * faceNormals[0]; - for (float secondSign = -1.0f; secondSign < 2.0f; secondSign += 2.0f) { - glm::vec3 secondNormal = secondSign * faceNormals[1]; - for (float thirdSign = -1.0f; thirdSign < 2.0f; thirdSign += 2.0f) { - collisions.clear(); - glm::vec3 thirdNormal = thirdSign * faceNormals[2]; - - // the cornerNormal is the normalized sum of the three faces - glm::vec3 cornerNormal = glm::normalize(firstNormal + secondNormal + thirdNormal); - - // compute a direction that is slightly offset from cornerNormal - glm::vec3 perpAxis = glm::normalize(glm::cross(cornerNormal, firstNormal)); - glm::vec3 nearbyAxis = glm::normalize(cornerNormal + 0.3f * perpAxis); - - // swing the sphere on a small cone that starts at the corner and is centered on the cornerNormal - float delta = TWO_PI / (float)(numSteps - 1); - for (int i = 0; i < numSteps; i++) { - float angle = (float)i * delta; - glm::quat rotation = glm::angleAxis(angle, cornerNormal); - glm::vec3 offsetAxis = rotation * nearbyAxis; - sphereCenter = cubeCenter + (SQUARE_ROOT_OF_3 * 0.5f * cubeSide) * cornerNormal + (sphereRadius + offset) * offsetAxis; - - CollisionInfo* collision = ShapeCollider::sphereVsAACubeHelper(sphereCenter, sphereRadius, - cubeCenter, cubeSide, collisions); - - if (collision) { - - QFAIL_WITH_MESSAGE("sphere should NOT collide with cube corner\n\t\t" << - "cornerNormal = " << cornerNormal); - break; - } - } - } - } - } -} - -void ShapeColliderTests::sphereTouchesAACubeFaces() { - CollisionList collisions(16); - - float sphereRadius = 1.13f; - glm::vec3 sphereCenter(0.0f); - - glm::vec3 cubeCenter(1.23f, 4.56f, 7.89f); - float cubeSide = 4.34f; - - glm::vec3 faceNormals[] = {xAxis, yAxis, zAxis}; - int numDirections = 3; - - for (int i = 0; i < numDirections; ++i) { - // loop over both sides of cube positive and negative - for (float sign = -1.0f; sign < 2.0f; sign += 2.0f) { - glm::vec3 faceNormal = sign * faceNormals[i]; - // outside - { - collisions.clear(); - float overlap = 0.25f * sphereRadius; - float parallelOffset = 0.5f * cubeSide + sphereRadius - overlap; - float perpOffset = 0.25f * cubeSide; - glm::vec3 expectedPenetration = - overlap * faceNormal; - - // We rotate the position of the sphereCenter about a circle on the cube face so that - // it hits the same face in multiple spots. The penetration should be invarient for - // all collisions. - float delta = TWO_PI / 4.0f; - for (float angle = 0; angle < TWO_PI + EPSILON; angle += delta) { - glm::quat rotation = glm::angleAxis(angle, faceNormal); - glm::vec3 perpAxis = rotation * faceNormals[(i + 1) % numDirections]; - - sphereCenter = cubeCenter + parallelOffset * faceNormal + perpOffset * perpAxis; - - CollisionInfo* collision = ShapeCollider::sphereVsAACubeHelper(sphereCenter, sphereRadius, - cubeCenter, cubeSide, collisions); - - if (!collision) { - - QFAIL_WITH_MESSAGE("sphere should collide outside cube face\n\t\t" << - "faceNormal = " << faceNormal); - break; - } - - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, EPSILON); - - glm::vec3 expectedContact = sphereCenter - sphereRadius * faceNormal; - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContact, EPSILON); - QCOMPARE(collision->getShapeA(), (Shape*)nullptr); - QCOMPARE(collision->getShapeB(), (Shape*)nullptr); - } - } - - // inside - { - collisions.clear(); - float overlap = 1.25f * sphereRadius; - float sphereOffset = 0.5f * cubeSide + sphereRadius - overlap; - sphereCenter = cubeCenter + sphereOffset * faceNormal; - - CollisionInfo* collision = ShapeCollider::sphereVsAACubeHelper(sphereCenter, sphereRadius, - cubeCenter, cubeSide, collisions); - - if (!collision) { - QFAIL_WITH_MESSAGE("sphere should collide inside cube face.\n\t\t" - << "faceNormal = " << faceNormal); - break; - } - - glm::vec3 expectedPenetration = - overlap * faceNormal; - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, EPSILON); - - glm::vec3 expectedContact = sphereCenter - sphereRadius * faceNormal; - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContact, EPSILON); - } - } - } -} - -void ShapeColliderTests::sphereTouchesAACubeEdges() { - CollisionList collisions(20); - - float sphereRadius = 1.37f; - glm::vec3 sphereCenter(0.0f); - - glm::vec3 cubeCenter(1.23f, 4.56f, 7.89f); - float cubeSide = 2.98f; - - float overlap = 0.25f * sphereRadius; - int numSteps = 5; - - glm::vec3 faceNormals[] = {xAxis, yAxis, zAxis}; - int numDirections = 3; - - // loop over each face... - for (int i = 0; i < numDirections; ++i) { - for (float faceSign = -1.0f; faceSign < 2.0f; faceSign += 2.0f) { - glm::vec3 faceNormal = faceSign * faceNormals[i]; - - // loop over each neighboring face... - for (int j = (i + 1) % numDirections; j != i; j = (j + 1) % numDirections) { - // Compute the index to the third direction, which points perpendicular to both the face - // and the neighbor face. - int k = (j + 1) % numDirections; - if (k == i) { - k = (i + 1) % numDirections; - } - glm::vec3 thirdNormal = faceNormals[k]; - - for (float neighborSign = -1.0f; neighborSign < 2.0f; neighborSign += 2.0f) { - collisions.clear(); - glm::vec3 neighborNormal = neighborSign * faceNormals[j]; - - // combine the face and neighbor normals to get the edge normal - glm::vec3 edgeNormal = glm::normalize(faceNormal + neighborNormal); - - // Step the sphere along the edge in the direction of thirdNormal, starting at one corner and - // moving to the other. Test the penetration (invarient) and contact (changing) at each point. - glm::vec3 expectedPenetration = - overlap * edgeNormal; - float delta = cubeSide / (float)(numSteps - 1); - glm::vec3 startPosition = cubeCenter + (0.5f * cubeSide) * (faceNormal + neighborNormal - thirdNormal); - for (int m = 0; m < numSteps; ++m) { - sphereCenter = startPosition + ((float)m * delta) * thirdNormal + (sphereRadius - overlap) * edgeNormal; - - CollisionInfo* collision = ShapeCollider::sphereVsAACubeHelper(sphereCenter, sphereRadius, - cubeCenter, cubeSide, collisions); - if (!collision) { - QFAIL_WITH_MESSAGE("sphere should collide with cube edge.\n\t\t" - << "edgeNormal = " << edgeNormal); - break; - } - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, EPSILON); - - glm::vec3 expectedContact = sphereCenter - sphereRadius * edgeNormal; - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContact, EPSILON); - } - } - } - } - } -} - -void ShapeColliderTests::sphereTouchesAACubeCorners() { - CollisionList collisions(20); - - float sphereRadius = 1.37f; - glm::vec3 sphereCenter(0.0f); - - glm::vec3 cubeCenter(1.23f, 4.56f, 7.89f); - float cubeSide = 2.98f; - - float overlap = 0.25f * sphereRadius; - int numSteps = 5; - - glm::vec3 faceNormals[] = {xAxis, yAxis, zAxis}; - - for (float firstSign = -1.0f; firstSign < 2.0f; firstSign += 2.0f) { - glm::vec3 firstNormal = firstSign * faceNormals[0]; - for (float secondSign = -1.0f; secondSign < 2.0f; secondSign += 2.0f) { - glm::vec3 secondNormal = secondSign * faceNormals[1]; - for (float thirdSign = -1.0f; thirdSign < 2.0f; thirdSign += 2.0f) { - collisions.clear(); - glm::vec3 thirdNormal = thirdSign * faceNormals[2]; - - // the cornerNormal is the normalized sum of the three faces - glm::vec3 cornerNormal = glm::normalize(firstNormal + secondNormal + thirdNormal); - - // compute a direction that is slightly offset from cornerNormal - glm::vec3 perpAxis = glm::normalize(glm::cross(cornerNormal, firstNormal)); - glm::vec3 nearbyAxis = glm::normalize(cornerNormal + 0.1f * perpAxis); - - // swing the sphere on a small cone that starts at the corner and is centered on the cornerNormal - float delta = TWO_PI / (float)(numSteps - 1); - for (int i = 0; i < numSteps; i++) { - float angle = (float)i * delta; - glm::quat rotation = glm::angleAxis(angle, cornerNormal); - glm::vec3 offsetAxis = rotation * nearbyAxis; - sphereCenter = cubeCenter + (SQUARE_ROOT_OF_3 * 0.5f * cubeSide) * cornerNormal + (sphereRadius - overlap) * offsetAxis; - - CollisionInfo* collision = ShapeCollider::sphereVsAACubeHelper(sphereCenter, sphereRadius, - cubeCenter, cubeSide, collisions); - - if (!collision) { - QFAIL_WITH_MESSAGE("sphere should collide with cube corner.\n\t\t" - << "cornerNormal = " << cornerNormal); - break; - } - - glm::vec3 expectedPenetration = - overlap * offsetAxis; - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, EPSILON); - - glm::vec3 expectedContact = sphereCenter - sphereRadius * offsetAxis; - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContact, EPSILON); - } - } - } - } -} - -void ShapeColliderTests::capsuleMissesAACube() { - CollisionList collisions(16); - - float capsuleRadius = 1.0f; - - glm::vec3 cubeCenter(1.23f, 4.56f, 7.89f); - float cubeSide = 2.0f; - AACubeShape cube(cubeSide, cubeCenter); - - glm::vec3 faceNormals[] = {xAxis, yAxis, zAxis}; - int numDirections = 3; - - float offset = 2.0f * EPSILON; - - // capsule caps miss cube faces - for (int i = 0; i < numDirections; ++i) { - for (float sign = -1.0f; sign < 2.0f; sign += 2.0f) { - glm::vec3 faceNormal = sign * faceNormals[i]; - glm::vec3 secondNormal = faceNormals[(i + 1) % numDirections]; - glm::vec3 thirdNormal = faceNormals[(i + 2) % numDirections]; - - // pick a random point somewhere above the face - glm::vec3 startPoint = cubeCenter + (cubeSide + capsuleRadius) * faceNormal + - (cubeSide * (randFloat() - 0.5f)) * secondNormal + - (cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // pick a second random point slightly more than one radius above the face - glm::vec3 endPoint = cubeCenter + (0.5f * cubeSide + capsuleRadius + offset) * faceNormal + - (cubeSide * (randFloat() - 0.5f)) * secondNormal + - (cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // randomly swap the points so capsule axis may point toward or away from face - if (randFloat() > 0.5f) { - glm::vec3 temp = startPoint; - startPoint = endPoint; - endPoint = temp; - } - - // create a capsule between the points - CapsuleShape capsule(capsuleRadius, startPoint, endPoint); - - // collide capsule with cube - QCOMPARE(ShapeCollider::capsuleVsAACube(&capsule, &cube, collisions), false); - } - } - - // capsule caps miss cube edges - // loop over each face... - for (int i = 0; i < numDirections; ++i) { - for (float faceSign = -1.0f; faceSign < 2.0f; faceSign += 2.0f) { - glm::vec3 faceNormal = faceSign * faceNormals[i]; - - // loop over each neighboring face... - for (int j = (i + 1) % numDirections; j != i; j = (j + 1) % numDirections) { - // Compute the index to the third direction, which points perpendicular to both the face - // and the neighbor face. - int k = (j + 1) % numDirections; - if (k == i) { - k = (i + 1) % numDirections; - } - glm::vec3 thirdNormal = faceNormals[k]; - - collisions.clear(); - for (float neighborSign = -1.0f; neighborSign < 2.0f; neighborSign += 2.0f) { - glm::vec3 neighborNormal = neighborSign * faceNormals[j]; - - // combine the face and neighbor normals to get the edge normal - glm::vec3 edgeNormal = glm::normalize(faceNormal + neighborNormal); - - // pick a random point somewhere above the edge - glm::vec3 startPoint = cubeCenter + (SQUARE_ROOT_OF_2 * cubeSide + capsuleRadius) * edgeNormal + - (cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // pick a second random point slightly more than one radius above the edge - glm::vec3 endPoint = cubeCenter + (SQUARE_ROOT_OF_2 * 0.5f * cubeSide + capsuleRadius + offset) * edgeNormal + - (cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // randomly swap the points so capsule axis may point toward or away from edge - if (randFloat() > 0.5f) { - glm::vec3 temp = startPoint; - startPoint = endPoint; - endPoint = temp; - } - - // create a capsule between the points - CapsuleShape capsule(capsuleRadius, startPoint, endPoint); - - // collide capsule with cube - QCOMPARE(ShapeCollider::capsuleVsAACube(&capsule, &cube, collisions), false); - } - } - } - } - - // capsule caps miss cube corners - for (float firstSign = -1.0f; firstSign < 2.0f; firstSign += 2.0f) { - glm::vec3 firstNormal = firstSign * faceNormals[0]; - for (float secondSign = -1.0f; secondSign < 2.0f; secondSign += 2.0f) { - glm::vec3 secondNormal = secondSign * faceNormals[1]; - for (float thirdSign = -1.0f; thirdSign < 2.0f; thirdSign += 2.0f) { - collisions.clear(); - glm::vec3 thirdNormal = thirdSign * faceNormals[2]; - - // the cornerNormal is the normalized sum of the three faces - glm::vec3 cornerNormal = glm::normalize(firstNormal + secondNormal + thirdNormal); - - // pick a random point somewhere above the corner - glm::vec3 startPoint = cubeCenter + (SQUARE_ROOT_OF_3 * cubeSide + capsuleRadius) * cornerNormal + - (0.25f * cubeSide * (randFloat() - 0.5f)) * firstNormal + - (0.25f * cubeSide * (randFloat() - 0.5f)) * secondNormal + - (0.25f * cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // pick a second random point slightly more than one radius above the corner - glm::vec3 endPoint = cubeCenter + (SQUARE_ROOT_OF_3 * 0.5f * cubeSide + capsuleRadius + offset) * cornerNormal; - - // randomly swap the points so capsule axis may point toward or away from corner - if (randFloat() > 0.5f) { - glm::vec3 temp = startPoint; - startPoint = endPoint; - endPoint = temp; - } - - // create a capsule between the points - CapsuleShape capsule(capsuleRadius, startPoint, endPoint); - - // collide capsule with cube - QCOMPARE(ShapeCollider::capsuleVsAACube(&capsule, &cube, collisions), false); - } - } - } - - // capsule sides almost hit cube edges - // loop over each face... - float capsuleLength = 2.0f; - for (int i = 0; i < numDirections; ++i) { - for (float faceSign = -1.0f; faceSign < 2.0f; faceSign += 2.0f) { - glm::vec3 faceNormal = faceSign * faceNormals[i]; - - // loop over each neighboring face... - for (int j = (i + 1) % numDirections; j != i; j = (j + 1) % numDirections) { - // Compute the index to the third direction, which points perpendicular to both the face - // and the neighbor face. - int k = (j + 1) % numDirections; - if (k == i) { - k = (i + 1) % numDirections; - } - glm::vec3 thirdNormal = faceNormals[k]; - - collisions.clear(); - for (float neighborSign = -1.0f; neighborSign < 2.0f; neighborSign += 2.0f) { - glm::vec3 neighborNormal = neighborSign * faceNormals[j]; - - // combine the face and neighbor normals to get the edge normal - glm::vec3 edgeNormal = glm::normalize(faceNormal + neighborNormal); - - // pick a random point somewhere along the edge - glm::vec3 edgePoint = cubeCenter + (SQUARE_ROOT_OF_2 * 0.5f * cubeSide) * edgeNormal + - ((cubeSide - 2.0f * offset) * (randFloat() - 0.5f)) * thirdNormal; - - // pick a random normal that is deflected slightly from edgeNormal - glm::vec3 deflectedNormal = glm::normalize(edgeNormal + - (0.1f * (randFloat() - 0.5f)) * faceNormal + - (0.1f * (randFloat() - 0.5f)) * neighborNormal); - - // compute the axis direction, which will be perp to deflectedNormal and thirdNormal - glm::vec3 axisDirection = glm::normalize(glm::cross(deflectedNormal, thirdNormal)); - - // compute a point for the capsule's axis along deflection normal away from edgePoint - glm::vec3 axisPoint = edgePoint + (capsuleRadius + offset) * deflectedNormal; - - // now we can compute the capsule endpoints - glm::vec3 endPoint = axisPoint + (0.5f * capsuleLength * randFloat()) * axisDirection; - glm::vec3 startPoint = axisPoint - (0.5f * capsuleLength * randFloat()) * axisDirection; - - // create a capsule between the points - CapsuleShape capsule(capsuleRadius, startPoint, endPoint); - - // collide capsule with cube - QCOMPARE(ShapeCollider::capsuleVsAACube(&capsule, &cube, collisions), false); - } - } - } - } - - // capsule sides almost hit cube corners - for (float firstSign = -1.0f; firstSign < 2.0f; firstSign += 2.0f) { - glm::vec3 firstNormal = firstSign * faceNormals[0]; - for (float secondSign = -1.0f; secondSign < 2.0f; secondSign += 2.0f) { - glm::vec3 secondNormal = secondSign * faceNormals[1]; - for (float thirdSign = -1.0f; thirdSign < 2.0f; thirdSign += 2.0f) { - collisions.clear(); - glm::vec3 thirdNormal = thirdSign * faceNormals[2]; - - // the cornerNormal is the normalized sum of the three faces - glm::vec3 cornerNormal = glm::normalize(firstNormal + secondNormal + thirdNormal); - - // compute a penetration normal that is somewhat randomized about cornerNormal - glm::vec3 penetrationNormal = - glm::normalize(cornerNormal + - (0.05f * cubeSide * (randFloat() - 0.5f)) * firstNormal + - (0.05f * cubeSide * (randFloat() - 0.5f)) * secondNormal + - (0.05f * cubeSide * (randFloat() - 0.5f)) * thirdNormal); - - // pick a random point somewhere above the corner - glm::vec3 corner = cubeCenter + (0.5f * cubeSide) * (firstNormal + secondNormal + thirdNormal); - glm::vec3 startPoint = corner + (3.0f * cubeSide) * cornerNormal + - (0.25f * cubeSide * (randFloat() - 0.5f)) * firstNormal + - (0.25f * cubeSide * (randFloat() - 0.5f)) * secondNormal + - (0.25f * cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // pick a second random point slightly less than one radius above the corner - // with some sight perp motion - glm::vec3 endPoint = corner - (capsuleRadius + offset) * penetrationNormal; - - // randomly swap the points so capsule axis may point toward or away from corner - if (randFloat() > 0.5f) { - glm::vec3 temp = startPoint; - startPoint = endPoint; - endPoint = temp; - } - - // create a capsule between the points - CapsuleShape capsule(capsuleRadius, startPoint, endPoint); - - // collide capsule with cube - QCOMPARE(ShapeCollider::capsuleVsAACube(&capsule, &cube, collisions), false); - } - } - } - - // capsule sides almost hit cube faces - // these are the steps along the capsuleAxis where we'll put the capsule endpoints - float steps[] = { -1.0f, 2.0f, 0.25f, 0.75f, -1.0f }; - - for (int i = 0; i < numDirections; ++i) { - for (float sign = -1.0f; sign < 2.0f; sign += 2.0f) { - glm::vec3 faceNormal = sign * faceNormals[i]; - glm::vec3 secondNormal = faceNormals[(i + 1) % numDirections]; - glm::vec3 thirdNormal = faceNormals[(i + 2) % numDirections]; - - // pick two random point on opposite edges of the face - glm::vec3 firstEdgeIntersection = cubeCenter + (0.5f * cubeSide) * (faceNormal + secondNormal) + - (cubeSide * (randFloat() - 0.5f)) * thirdNormal; - glm::vec3 secondEdgeIntersection = cubeCenter + (0.5f * cubeSide) * (faceNormal - secondNormal) + - (cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // compute the un-normalized axis for the capsule - glm::vec3 capsuleAxis = secondEdgeIntersection - firstEdgeIntersection; - // there are three pairs in steps[] - for (int j = 0; j < 4; j++) { - collisions.clear(); - glm::vec3 startPoint = firstEdgeIntersection + steps[j] * capsuleAxis + (capsuleRadius + offset) * faceNormal; - glm::vec3 endPoint = firstEdgeIntersection + steps[j + 1] * capsuleAxis + (capsuleRadius + offset) * faceNormal; - - // create a capsule between the points - CapsuleShape capsule(capsuleRadius, startPoint, endPoint); - - // collide capsule with cube - QCOMPARE(ShapeCollider::capsuleVsAACube(&capsule, &cube, collisions), false); - } - } - } -} - -void ShapeColliderTests::capsuleTouchesAACube() { - CollisionList collisions(16); - - float capsuleRadius = 1.0f; - - glm::vec3 cubeCenter(1.23f, 4.56f, 7.89f); - float cubeSide = 2.0f; - AACubeShape cube(cubeSide, cubeCenter); - - glm::vec3 faceNormals[] = {xAxis, yAxis, zAxis}; - int numDirections = 3; - - float overlap = 0.25f * capsuleRadius; - float allowableError = 10.0f * EPSILON; - - // capsule caps hit cube faces - for (int i = 0; i < numDirections; ++i) { - for (float sign = -1.0f; sign < 2.0f; sign += 2.0f) { - glm::vec3 faceNormal = sign * faceNormals[i]; - glm::vec3 secondNormal = faceNormals[(i + 1) % numDirections]; - glm::vec3 thirdNormal = faceNormals[(i + 2) % numDirections]; - - // pick a random point somewhere above the face - glm::vec3 startPoint = cubeCenter + (cubeSide + capsuleRadius) * faceNormal + - (cubeSide * (randFloat() - 0.5f)) * secondNormal + - (cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // pick a second random point slightly less than one radius above the face - // (but reduce width of range by 2*overlap to prevent the penetration from - // registering against other faces) - glm::vec3 endPoint = cubeCenter + (0.5f * cubeSide + capsuleRadius - overlap) * faceNormal + - ((cubeSide - 2.0f * overlap) * (randFloat() - 0.5f)) * secondNormal + - ((cubeSide - 2.0f * overlap) * (randFloat() - 0.5f)) * thirdNormal; - glm::vec3 collidingPoint = endPoint; - - // randomly swap the points so capsule axis may point toward or away from face - if (randFloat() > 0.5f) { - glm::vec3 temp = startPoint; - startPoint = endPoint; - endPoint = temp; - } - - // create a capsule between the points - CapsuleShape capsule(capsuleRadius, startPoint, endPoint); - - // collide capsule with cube - QCOMPARE(ShapeCollider::capsuleVsAACube(&capsule, &cube, collisions), true); - - CollisionInfo* collision = collisions.getLastCollision(); - - // penetration points from capsule into cube - glm::vec3 expectedPenetration = - overlap * faceNormal; - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, allowableError); - - // contactPoint is on surface of capsule - glm::vec3 expectedContactPoint = collidingPoint - capsuleRadius * faceNormal; - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, allowableError); - } - } - - // capsule caps hit cube edges - // loop over each face... - for (int i = 0; i < numDirections; ++i) { - for (float faceSign = -1.0f; faceSign < 2.0f; faceSign += 2.0f) { - glm::vec3 faceNormal = faceSign * faceNormals[i]; - - // loop over each neighboring face... - for (int j = (i + 1) % numDirections; j != i; j = (j + 1) % numDirections) { - // Compute the index to the third direction, which points perpendicular to both the face - // and the neighbor face. - int k = (j + 1) % numDirections; - if (k == i) { - k = (i + 1) % numDirections; - } - glm::vec3 thirdNormal = faceNormals[k]; - - collisions.clear(); - for (float neighborSign = -1.0f; neighborSign < 2.0f; neighborSign += 2.0f) { - glm::vec3 neighborNormal = neighborSign * faceNormals[j]; - - // combine the face and neighbor normals to get the edge normal - glm::vec3 edgeNormal = glm::normalize(faceNormal + neighborNormal); - - // pick a random point somewhere above the edge - glm::vec3 startPoint = cubeCenter + (SQUARE_ROOT_OF_2 * cubeSide + capsuleRadius) * edgeNormal + - (cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // pick a second random point slightly less than one radius above the edge - glm::vec3 endPoint = cubeCenter + (SQUARE_ROOT_OF_2 * 0.5f * cubeSide + capsuleRadius - overlap) * edgeNormal + - (cubeSide * (randFloat() - 0.5f)) * thirdNormal; - glm::vec3 collidingPoint = endPoint; - - // randomly swap the points so capsule axis may point toward or away from edge - if (randFloat() > 0.5f) { - glm::vec3 temp = startPoint; - startPoint = endPoint; - endPoint = temp; - } - - // create a capsule between the points - CapsuleShape capsule(capsuleRadius, startPoint, endPoint); - - // collide capsule with cube - QCOMPARE(ShapeCollider::capsuleVsAACube(&capsule, &cube, collisions), true); - - CollisionInfo* collision = collisions.getLastCollision(); - QCOMPARE(collision != nullptr, true); - - // penetration points from capsule into cube - glm::vec3 expectedPenetration = - overlap * edgeNormal; - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, allowableError); - - // contactPoint is on surface of capsule - glm::vec3 expectedContactPoint = collidingPoint - capsuleRadius * edgeNormal; - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, allowableError); - } - } - } - } - - // capsule caps hit cube corners - for (float firstSign = -1.0f; firstSign < 2.0f; firstSign += 2.0f) { - glm::vec3 firstNormal = firstSign * faceNormals[0]; - for (float secondSign = -1.0f; secondSign < 2.0f; secondSign += 2.0f) { - glm::vec3 secondNormal = secondSign * faceNormals[1]; - for (float thirdSign = -1.0f; thirdSign < 2.0f; thirdSign += 2.0f) { - collisions.clear(); - glm::vec3 thirdNormal = thirdSign * faceNormals[2]; - - // the cornerNormal is the normalized sum of the three faces - glm::vec3 cornerNormal = glm::normalize(firstNormal + secondNormal + thirdNormal); - - // pick a random point somewhere above the corner - glm::vec3 startPoint = cubeCenter + (SQUARE_ROOT_OF_3 * cubeSide + capsuleRadius) * cornerNormal + - (0.25f * cubeSide * (randFloat() - 0.5f)) * firstNormal + - (0.25f * cubeSide * (randFloat() - 0.5f)) * secondNormal + - (0.25f * cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // pick a second random point slightly less than one radius above the corner - glm::vec3 endPoint = cubeCenter + (SQUARE_ROOT_OF_3 * 0.5f * cubeSide + capsuleRadius - overlap) * cornerNormal; - glm::vec3 collidingPoint = endPoint; - - // randomly swap the points so capsule axis may point toward or away from corner - if (randFloat() > 0.5f) { - glm::vec3 temp = startPoint; - startPoint = endPoint; - endPoint = temp; - } - - // create a capsule between the points - CapsuleShape capsule(capsuleRadius, startPoint, endPoint); - - // collide capsule with cube - QCOMPARE(ShapeCollider::capsuleVsAACube(&capsule, &cube, collisions), true); - - CollisionInfo* collision = collisions.getLastCollision(); - QCOMPARE(collision != nullptr, true); - - // penetration points from capsule into cube - glm::vec3 expectedPenetration = - overlap * cornerNormal; - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, allowableError); - - // contactPoint is on surface of capsule - glm::vec3 expectedContactPoint = collidingPoint - capsuleRadius * cornerNormal; - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, allowableError); - } - } - } - - // capsule sides hit cube edges - // loop over each face... - float capsuleLength = 2.0f; - for (int i = 0; i < numDirections; ++i) { - for (float faceSign = -1.0f; faceSign < 2.0f; faceSign += 2.0f) { - glm::vec3 faceNormal = faceSign * faceNormals[i]; - - // loop over each neighboring face... - for (int j = (i + 1) % numDirections; j != i; j = (j + 1) % numDirections) { - // Compute the index to the third direction, which points perpendicular to both the face - // and the neighbor face. - int k = (j + 1) % numDirections; - if (k == i) { - k = (i + 1) % numDirections; - } - glm::vec3 thirdNormal = faceNormals[k]; - - collisions.clear(); - for (float neighborSign = -1.0f; neighborSign < 2.0f; neighborSign += 2.0f) { - glm::vec3 neighborNormal = neighborSign * faceNormals[j]; - - // combine the face and neighbor normals to get the edge normal - glm::vec3 edgeNormal = glm::normalize(faceNormal + neighborNormal); - - // pick a random point somewhere along the edge - glm::vec3 edgePoint = cubeCenter + (SQUARE_ROOT_OF_2 * 0.5f * cubeSide) * edgeNormal + - ((cubeSide - 2.0f * overlap) * (randFloat() - 0.5f)) * thirdNormal; - - // pick a random normal that is deflected slightly from edgeNormal - glm::vec3 deflectedNormal = glm::normalize(edgeNormal + - (0.1f * (randFloat() - 0.5f)) * faceNormal + - (0.1f * (randFloat() - 0.5f)) * neighborNormal); - - // compute the axis direction, which will be perp to deflectedNormal and thirdNormal - glm::vec3 axisDirection = glm::normalize(glm::cross(deflectedNormal, thirdNormal)); - - // compute a point for the capsule's axis along deflection normal away from edgePoint - glm::vec3 axisPoint = edgePoint + (capsuleRadius - overlap) * deflectedNormal; - - // now we can compute the capsule endpoints - glm::vec3 endPoint = axisPoint + (0.5f * capsuleLength * randFloat()) * axisDirection; - glm::vec3 startPoint = axisPoint - (0.5f * capsuleLength * randFloat()) * axisDirection; - - // create a capsule between the points - CapsuleShape capsule(capsuleRadius, startPoint, endPoint); - - // collide capsule with cube - QCOMPARE(ShapeCollider::capsuleVsAACube(&capsule, &cube, collisions), true); - - CollisionInfo* collision = collisions.getLastCollision(); - QCOMPARE(collision != nullptr, true); - - // penetration points from capsule into cube - glm::vec3 expectedPenetration = - overlap * deflectedNormal; - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, allowableError); - - // contactPoint is on surface of capsule - glm::vec3 expectedContactPoint = axisPoint - capsuleRadius * deflectedNormal; - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, allowableError); } - } - } - } - - // capsule sides hit cube corners - for (float firstSign = -1.0f; firstSign < 2.0f; firstSign += 2.0f) { - glm::vec3 firstNormal = firstSign * faceNormals[0]; - for (float secondSign = -1.0f; secondSign < 2.0f; secondSign += 2.0f) { - glm::vec3 secondNormal = secondSign * faceNormals[1]; - for (float thirdSign = -1.0f; thirdSign < 2.0f; thirdSign += 2.0f) { - collisions.clear(); - glm::vec3 thirdNormal = thirdSign * faceNormals[2]; - - // the cornerNormal is the normalized sum of the three faces - glm::vec3 cornerNormal = glm::normalize(firstNormal + secondNormal + thirdNormal); - - // compute a penetration normal that is somewhat randomized about cornerNormal - glm::vec3 penetrationNormal = - glm::normalize(cornerNormal + - (0.05f * cubeSide * (randFloat() - 0.5f)) * firstNormal + - (0.05f * cubeSide * (randFloat() - 0.5f)) * secondNormal + - (0.05f * cubeSide * (randFloat() - 0.5f)) * thirdNormal); - - // pick a random point somewhere above the corner - glm::vec3 corner = cubeCenter + (0.5f * cubeSide) * (firstNormal + secondNormal + thirdNormal); - glm::vec3 startPoint = corner + (3.0f * cubeSide) * cornerNormal + - (0.25f * cubeSide * (randFloat() - 0.5f)) * firstNormal + - (0.25f * cubeSide * (randFloat() - 0.5f)) * secondNormal + - (0.25f * cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // pick a second random point slightly less than one radius above the corner - // with some sight perp motion - glm::vec3 endPoint = corner - (capsuleRadius - overlap) * penetrationNormal; - glm::vec3 collidingPoint = endPoint; - - // randomly swap the points so capsule axis may point toward or away from corner - if (randFloat() > 0.5f) { - glm::vec3 temp = startPoint; - startPoint = endPoint; - endPoint = temp; - } - - // create a capsule between the points - CapsuleShape capsule(capsuleRadius, startPoint, endPoint); - - // collide capsule with cube - QCOMPARE(ShapeCollider::capsuleVsAACube(&capsule, &cube, collisions), true); - - CollisionInfo* collision = collisions.getLastCollision(); - QCOMPARE(collision != nullptr, true); - - // penetration points from capsule into cube - glm::vec3 expectedPenetration = overlap * penetrationNormal; - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, allowableError); - - // contactPoint is on surface of capsule - glm::vec3 expectedContactPoint = collidingPoint + capsuleRadius * penetrationNormal; - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, allowableError); - } - } - } - - // capsule sides hit cube faces - // these are the steps along the capsuleAxis where we'll put the capsule endpoints - float steps[] = { -1.0f, 2.0f, 0.25f, 0.75f, -1.0f }; - - for (int i = 0; i < numDirections; ++i) { - for (float sign = -1.0f; sign < 2.0f; sign += 2.0f) { - glm::vec3 faceNormal = sign * faceNormals[i]; - glm::vec3 secondNormal = faceNormals[(i + 1) % numDirections]; - glm::vec3 thirdNormal = faceNormals[(i + 2) % numDirections]; - - // pick two random point on opposite edges of the face - glm::vec3 firstEdgeIntersection = cubeCenter + (0.5f * cubeSide) * (faceNormal + secondNormal) + - (cubeSide * (randFloat() - 0.5f)) * thirdNormal; - glm::vec3 secondEdgeIntersection = cubeCenter + (0.5f * cubeSide) * (faceNormal - secondNormal) + - (cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // compute the un-normalized axis for the capsule - glm::vec3 capsuleAxis = secondEdgeIntersection - firstEdgeIntersection; - // there are three pairs in steps[] - for (int j = 0; j < 4; j++) { - collisions.clear(); - glm::vec3 startPoint = firstEdgeIntersection + steps[j] * capsuleAxis + (capsuleRadius - overlap) * faceNormal; - glm::vec3 endPoint = firstEdgeIntersection + steps[j + 1] * capsuleAxis + (capsuleRadius - overlap) * faceNormal; - - // create a capsule between the points - CapsuleShape capsule(capsuleRadius, startPoint, endPoint); - - // collide capsule with cube - QCOMPARE(ShapeCollider::capsuleVsAACube(&capsule, &cube, collisions), true); - QCOMPARE(collisions.size(), 2); - - // compute the expected contact points - // NOTE: whether the startPoint or endPoint are expected to collide depends the relative values - // of the steps[] that were used to compute them above. - glm::vec3 expectedContactPoints[2]; - if (j == 0) { - expectedContactPoints[0] = firstEdgeIntersection - overlap * faceNormal; - expectedContactPoints[1] = secondEdgeIntersection - overlap * faceNormal; - } else if (j == 1) { - expectedContactPoints[0] = secondEdgeIntersection - overlap * faceNormal; - expectedContactPoints[1] = endPoint - capsuleRadius * faceNormal; - } else if (j == 2) { - expectedContactPoints[0] = startPoint - capsuleRadius * faceNormal; - expectedContactPoints[1] = endPoint - capsuleRadius * faceNormal; - } else if (j == 3) { - expectedContactPoints[0] = startPoint - capsuleRadius * faceNormal; - expectedContactPoints[1] = firstEdgeIntersection - overlap * faceNormal; - } - - // verify each contact - for (int k = 0; k < 2; ++k) { - CollisionInfo* collision = collisions.getCollision(k); - // penetration points from capsule into cube - glm::vec3 expectedPenetration = - overlap * faceNormal; - QCOMPARE_WITH_ABS_ERROR(collision->_penetration, expectedPenetration, allowableError); - - // the order of the final contact points is undefined, so we - // figure out which expected contact point is the closest to the real one - // and then verify accuracy on that - float length0 = glm::length(collision->_contactPoint - expectedContactPoints[0]); - float length1 = glm::length(collision->_contactPoint - expectedContactPoints[1]); - glm::vec3 expectedContactPoint = (length0 < length1) ? expectedContactPoints[0] : expectedContactPoints[1]; - // contactPoint is on surface of capsule - QCOMPARE_WITH_ABS_ERROR(collision->_contactPoint, expectedContactPoint, allowableError); - } - } - } - } -} - - -void ShapeColliderTests::rayHitsSphere() { - float startDistance = 3.0f; - - float radius = 1.0f; - glm::vec3 center(0.0f); - SphereShape sphere(radius, center); - - // very simple ray along xAxis - { - RayIntersectionInfo intersection; - intersection._rayStart = -startDistance * xAxis; - intersection._rayDirection = xAxis; - - QCOMPARE(sphere.findRayIntersection(intersection), true); - - float expectedDistance = startDistance - radius; - QCOMPARE_WITH_ABS_ERROR(intersection._hitDistance, expectedDistance, startDistance * EPSILON); - QCOMPARE(intersection._hitShape, &sphere); - } - - // ray along a diagonal axis - { - RayIntersectionInfo intersection; - intersection._rayStart = glm::vec3(startDistance, startDistance, 0.0f); - intersection._rayDirection = - glm::normalize(intersection._rayStart); - QCOMPARE(sphere.findRayIntersection(intersection), true); - - float expectedDistance = SQUARE_ROOT_OF_2 * startDistance - radius; - QCOMPARE_WITH_ABS_ERROR(intersection._hitDistance, expectedDistance, startDistance * EPSILON); - } - - // rotated and displaced ray and sphere - { - startDistance = 7.41f; - radius = 3.917f; - - glm::vec3 axis = glm::normalize(glm::vec3(1.0f, 2.0f, 3.0f)); - glm::quat rotation = glm::angleAxis(0.987654321f, axis); - glm::vec3 translation(35.7f, 2.46f, -1.97f); - - glm::vec3 unrotatedRayDirection = -xAxis; - glm::vec3 untransformedRayStart = startDistance * xAxis; - - RayIntersectionInfo intersection; - intersection._rayStart = rotation * (untransformedRayStart + translation); - intersection._rayDirection = rotation * unrotatedRayDirection; - - sphere.setRadius(radius); - sphere.setTranslation(rotation * translation); - - QCOMPARE(sphere.findRayIntersection(intersection), true); - float expectedDistance = startDistance - radius; - QCOMPARE_WITH_ABS_ERROR(intersection._hitDistance, expectedDistance, startDistance * EPSILON); - } -} - -void ShapeColliderTests::rayBarelyHitsSphere() { - float radius = 1.0f; - glm::vec3 center(0.0f); - float delta = 2.0f * EPSILON; - - SphereShape sphere(radius, center); - float startDistance = 3.0f; - - { - RayIntersectionInfo intersection; - intersection._rayStart = glm::vec3(-startDistance, radius - delta, 0.0f); - intersection._rayDirection = xAxis; - - // very simple ray along xAxis - QCOMPARE(sphere.findRayIntersection(intersection), true); - QCOMPARE(intersection._hitShape, &sphere); - } - - { - // translate and rotate the whole system... - glm::vec3 axis = glm::normalize(glm::vec3(1.0f, 2.0f, 3.0f)); - glm::quat rotation = glm::angleAxis(0.987654321f, axis); - glm::vec3 translation(35.7f, 0.46f, -1.97f); - - RayIntersectionInfo intersection; - intersection._rayStart = rotation * (intersection._rayStart + translation); - intersection._rayDirection = rotation * intersection._rayDirection; - - sphere.setTranslation(rotation * translation); - - // ...and test again - QCOMPARE(sphere.findRayIntersection(intersection), true); - } -} - - -void ShapeColliderTests::rayBarelyMissesSphere() { - // same as the barely-hits case, but this time we move the ray away from sphere - float radius = 1.0f; - glm::vec3 center(0.0f); - float delta = 2.0f * EPSILON; - - SphereShape sphere(radius, center); - float startDistance = 3.0f; - - { - RayIntersectionInfo intersection; - intersection._rayStart = glm::vec3(-startDistance, radius + delta, 0.0f); - intersection._rayDirection = xAxis; - - // very simple ray along xAxis - QCOMPARE(sphere.findRayIntersection(intersection), false); - QCOMPARE(intersection._hitDistance, FLT_MAX); - } - - { - // translate and rotate the whole system... - float angle = 0.987654321f; - glm::vec3 axis = glm::normalize(glm::vec3(1.0f, 2.0f, 3.0f)); - glm::quat rotation = glm::angleAxis(angle, axis); - glm::vec3 translation(35.7f, 2.46f, -1.97f); - - RayIntersectionInfo intersection; - intersection._rayStart = rotation * (glm::vec3(-startDistance, radius + delta, 0.0f) + translation); - intersection._rayDirection = rotation * xAxis; - sphere.setTranslation(rotation * translation); - - // ...and test again - QCOMPARE(sphere.findRayIntersection(intersection), false); - QCOMPARE(intersection._hitDistance == FLT_MAX, true); - QCOMPARE(intersection._hitShape == nullptr, true); - } -} - -void ShapeColliderTests::rayHitsCapsule() { - float startDistance = 3.0f; - float radius = 1.0f; - float halfHeight = 2.0f; - glm::vec3 center(0.0f); - CapsuleShape capsule(radius, halfHeight); - - // simple tests along xAxis - { // toward capsule center - RayIntersectionInfo intersection; - intersection._rayStart = glm::vec3(startDistance, 0.0f, 0.0f); - intersection._rayDirection = - xAxis; - QCOMPARE(capsule.findRayIntersection(intersection), true); - float expectedDistance = startDistance - radius; - QCOMPARE_WITH_ABS_ERROR(intersection._hitDistance, expectedDistance, startDistance * EPSILON); - QCOMPARE(intersection._hitShape, &capsule); - } - - { // toward top of cylindrical wall - RayIntersectionInfo intersection; - intersection._rayStart = glm::vec3(startDistance, halfHeight, 0.0f); - intersection._rayDirection = - xAxis; - QCOMPARE(capsule.findRayIntersection(intersection), true); - float expectedDistance = startDistance - radius; - QCOMPARE_WITH_ABS_ERROR(intersection._hitDistance, expectedDistance, startDistance * EPSILON); - } - - float delta = 2.0f * EPSILON; - { // toward top cap - RayIntersectionInfo intersection; - intersection._rayStart = glm::vec3(startDistance, halfHeight + delta, 0.0f); - intersection._rayDirection = - xAxis; - QCOMPARE(capsule.findRayIntersection(intersection), true); - float expectedDistance = startDistance - radius; - QCOMPARE_WITH_ABS_ERROR(intersection._hitDistance, expectedDistance, startDistance * EPSILON); - } - - const float EDGE_CASE_SLOP_FACTOR = 20.0f; - { // toward tip of top cap - RayIntersectionInfo intersection; - intersection._rayStart = glm::vec3(startDistance, halfHeight + radius - delta, 0.0f); - intersection._rayDirection = - xAxis; - QCOMPARE(capsule.findRayIntersection(intersection), true); - float expectedDistance = startDistance - radius * sqrtf(2.0f * delta); // using small angle approximation of cosine -// float relativeError = fabsf(intersection._hitDistance - expectedDistance) / startDistance; - // for edge cases we allow a LOT of error - QCOMPARE_WITH_ABS_ERROR(intersection._hitDistance, expectedDistance, startDistance * EDGE_CASE_SLOP_FACTOR * EPSILON); - } - - { // toward tip of bottom cap - RayIntersectionInfo intersection; - intersection._rayStart = glm::vec3(startDistance, - halfHeight - radius + delta, 0.0f); - intersection._rayDirection = - xAxis; - QCOMPARE(capsule.findRayIntersection(intersection), true); - float expectedDistance = startDistance - radius * sqrtf(2.0f * delta); // using small angle approximation of cosine -// float relativeError = fabsf(intersection._hitDistance - expectedDistance) / startDistance; - // for edge cases we allow a LOT of error - QCOMPARE_WITH_ABS_ERROR(intersection._hitDistance, expectedDistance, startDistance * EPSILON * EDGE_CASE_SLOP_FACTOR); - } - - { // toward edge of capsule cylindrical face - RayIntersectionInfo intersection; - intersection._rayStart = glm::vec3(startDistance, 0.0f, radius - delta); - intersection._rayDirection = - xAxis; - QCOMPARE(capsule.findRayIntersection(intersection), true); - float expectedDistance = startDistance - radius * sqrtf(2.0f * delta); // using small angle approximation of cosine - float relativeError = fabsf(intersection._hitDistance - expectedDistance) / startDistance; - // for edge cases we allow a LOT of error - QCOMPARE_WITH_ABS_ERROR(intersection._hitDistance, expectedDistance, startDistance * EPSILON * EDGE_CASE_SLOP_FACTOR); - } - // TODO: test at steep angles near cylinder/cap junction -} - -void ShapeColliderTests::rayMissesCapsule() { - // same as edge case hit tests, but shifted in the opposite direction - float startDistance = 3.0f; - float radius = 1.0f; - float halfHeight = 2.0f; - glm::vec3 center(0.0f); - CapsuleShape capsule(radius, halfHeight); - - { // simple test along xAxis - // toward capsule center - RayIntersectionInfo intersection; - intersection._rayStart = glm::vec3(startDistance, 0.0f, 0.0f); - intersection._rayDirection = -xAxis; - float delta = 2.0f * EPSILON; - - // over top cap - intersection._rayStart.y = halfHeight + radius + delta; - intersection._hitDistance = FLT_MAX; - QCOMPARE(capsule.findRayIntersection(intersection), false); - QCOMPARE(intersection._hitDistance, FLT_MAX); - - // below bottom cap - intersection._rayStart.y = - halfHeight - radius - delta; - intersection._hitDistance = FLT_MAX; - QCOMPARE(capsule.findRayIntersection(intersection), false); - QCOMPARE(intersection._hitDistance, FLT_MAX); - - // past edge of capsule cylindrical face - intersection._rayStart.y = 0.0f; - intersection._rayStart.z = radius + delta; - intersection._hitDistance = FLT_MAX; - QCOMPARE(capsule.findRayIntersection(intersection), false); - QCOMPARE(intersection._hitDistance, FLT_MAX); - QCOMPARE(intersection._hitShape, (Shape*)nullptr); - } - // TODO: test at steep angles near edge -} - -void ShapeColliderTests::rayHitsPlane() { - // make a simple plane - float planeDistanceFromOrigin = 3.579f; - glm::vec3 planePosition(0.0f, planeDistanceFromOrigin, 0.0f); - PlaneShape plane; - plane.setPoint(planePosition); - plane.setNormal(yAxis); - - // make a simple ray - float startDistance = 1.234f; - { - RayIntersectionInfo intersection; - intersection._rayStart = -startDistance * xAxis; - intersection._rayDirection = glm::normalize(glm::vec3(1.0f, 1.0f, 1.0f)); - - QCOMPARE(plane.findRayIntersection(intersection), true); - float expectedDistance = SQUARE_ROOT_OF_3 * planeDistanceFromOrigin; - - QCOMPARE_WITH_ABS_ERROR(intersection._hitDistance, expectedDistance, planeDistanceFromOrigin * EPSILON); - QCOMPARE(intersection._hitShape, &plane); - } - - { // rotate the whole system and try again - float angle = 37.8f; - glm::vec3 axis = glm::normalize( glm::vec3(-7.0f, 2.8f, 9.3f) ); - glm::quat rotation = glm::angleAxis(angle, axis); - - plane.setNormal(rotation * yAxis); - plane.setPoint(rotation * planePosition); - RayIntersectionInfo intersection; - intersection._rayStart = rotation * (-startDistance * xAxis); - intersection._rayDirection = rotation * glm::normalize(glm::vec3(1.0f, 1.0f, 1.0f)); - - QCOMPARE(plane.findRayIntersection(intersection), true); - - float expectedDistance = SQUARE_ROOT_OF_3 * planeDistanceFromOrigin; - QCOMPARE_WITH_ABS_ERROR(intersection._hitDistance, expectedDistance, planeDistanceFromOrigin * EPSILON); - } -} - -void ShapeColliderTests::rayMissesPlane() { - // make a simple plane - float planeDistanceFromOrigin = 3.579f; - glm::vec3 planePosition(0.0f, planeDistanceFromOrigin, 0.0f); - PlaneShape plane; - plane.setTranslation(planePosition); - - { // parallel rays should miss - float startDistance = 1.234f; - RayIntersectionInfo intersection; - intersection._rayStart = glm::vec3(-startDistance, 0.0f, 0.0f); - intersection._rayDirection = glm::normalize(glm::vec3(-1.0f, 0.0f, -1.0f)); - - QCOMPARE(plane.findRayIntersection(intersection), false); - QCOMPARE(intersection._hitDistance, FLT_MAX); - - // rotate the whole system and try again - float angle = 37.8f; - glm::vec3 axis = glm::normalize( glm::vec3(-7.0f, 2.8f, 9.3f) ); - glm::quat rotation = glm::angleAxis(angle, axis); - - plane.setTranslation(rotation * planePosition); - plane.setRotation(rotation); - - intersection._rayStart = rotation * intersection._rayStart; - intersection._rayDirection = rotation * intersection._rayDirection; - intersection._hitDistance = FLT_MAX; - - QCOMPARE(plane.findRayIntersection(intersection), false); - QCOMPARE(intersection._hitDistance, FLT_MAX); - QCOMPARE(intersection._hitShape, (Shape*)nullptr); - } - - { // make a simple ray that points away from plane - float startDistance = 1.234f; - - RayIntersectionInfo intersection; - intersection._rayStart = glm::vec3(-startDistance, 0.0f, 0.0f); - intersection._rayDirection = glm::normalize(glm::vec3(-1.0f, -1.0f, -1.0f)); - intersection._hitDistance = FLT_MAX; - - QCOMPARE(plane.findRayIntersection(intersection), false); - QCOMPARE(intersection._hitDistance, FLT_MAX); - - // rotate the whole system and try again - float angle = 37.8f; - glm::vec3 axis = glm::normalize( glm::vec3(-7.0f, 2.8f, 9.3f) ); - glm::quat rotation = glm::angleAxis(angle, axis); - - plane.setTranslation(rotation * planePosition); - plane.setRotation(rotation); - - intersection._rayStart = rotation * intersection._rayStart; - intersection._rayDirection = rotation * intersection._rayDirection; - intersection._hitDistance = FLT_MAX; - - QCOMPARE(plane.findRayIntersection(intersection), false); - QCOMPARE(intersection._hitDistance, FLT_MAX); - } -} - -void ShapeColliderTests::rayHitsAACube() { - glm::vec3 cubeCenter(1.23f, 4.56f, 7.89f); - float cubeSide = 2.127f; - AACubeShape cube(cubeSide, cubeCenter); - - float rayOffset = 3.796f; - - glm::vec3 faceNormals[] = {xAxis, yAxis, zAxis}; - int numDirections = 3; - int numRayCasts = 5; - - for (int i = 0; i < numDirections; ++i) { - for (float sign = -1.0f; sign < 2.0f; sign += 2.0f) { - glm::vec3 faceNormal = sign * faceNormals[i]; - glm::vec3 secondNormal = faceNormals[(i + 1) % numDirections]; - glm::vec3 thirdNormal = faceNormals[(i + 2) % numDirections]; - - // pick a random point somewhere above the face - glm::vec3 rayStart = cubeCenter + - (cubeSide + rayOffset) * faceNormal + - (cubeSide * (randFloat() - 0.5f)) * secondNormal + - (cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // cast multiple rays toward the face - for (int j = 0; j < numRayCasts; ++j) { - // pick a random point on the face - glm::vec3 facePoint = cubeCenter + - 0.5f * cubeSide * faceNormal + - (cubeSide * (randFloat() - 0.5f)) * secondNormal + - (cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // construct a ray from first point through second point - RayIntersectionInfo intersection; - intersection._rayStart = rayStart; - intersection._rayDirection = glm::normalize(facePoint - rayStart); - intersection._rayLength = 1.0001f * glm::distance(rayStart, facePoint); - - // cast the ray -// bool hit = cube.findRayIntersection(intersection); - - // validate - QCOMPARE(cube.findRayIntersection(intersection), true); - QCOMPARE_WITH_ABS_ERROR(glm::dot(faceNormal, intersection._hitNormal), 1.0f, EPSILON); - QCOMPARE_WITH_ABS_ERROR(facePoint, intersection.getIntersectionPoint(), EPSILON); - QCOMPARE(intersection._hitShape, &cube); - } - } - } -} - -void ShapeColliderTests::rayMissesAACube() { - //glm::vec3 cubeCenter(1.23f, 4.56f, 7.89f); - //float cubeSide = 2.127f; - glm::vec3 cubeCenter(0.0f); - float cubeSide = 2.0f; - AACubeShape cube(cubeSide, cubeCenter); - - float rayOffset = 3.796f; - - glm::vec3 faceNormals[] = {xAxis, yAxis, zAxis}; - int numDirections = 3; - int numRayCasts = 5; - - const float SOME_SMALL_NUMBER = 0.0001f; - - { // ray misses cube for being too short - for (int i = 0; i < numDirections; ++i) { - for (float sign = -1.0f; sign < 2.0f; sign += 2.0f) { - glm::vec3 faceNormal = sign * faceNormals[i]; - glm::vec3 secondNormal = faceNormals[(i + 1) % numDirections]; - glm::vec3 thirdNormal = faceNormals[(i + 2) % numDirections]; - - // pick a random point somewhere above the face - glm::vec3 rayStart = cubeCenter + - (cubeSide + rayOffset) * faceNormal + - (cubeSide * (randFloat() - 0.5f)) * secondNormal + - (cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // cast multiple rays toward the face - for (int j = 0; j < numRayCasts; ++j) { - // pick a random point on the face - glm::vec3 facePoint = cubeCenter + - 0.5f * cubeSide * faceNormal + - (cubeSide * (randFloat() - 0.5f)) * secondNormal + - (cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // construct a ray from first point to almost second point - RayIntersectionInfo intersection; - intersection._rayStart = rayStart; - intersection._rayDirection = glm::normalize(facePoint - rayStart); - intersection._rayLength = (1.0f - SOME_SMALL_NUMBER) * glm::distance(rayStart, facePoint); - - // cast the ray - QCOMPARE(cube.findRayIntersection(intersection), false); - } - } - } - } - { // long ray misses cube - for (int i = 0; i < numDirections; ++i) { - for (float sign = -1.0f; sign < 2.0f; sign += 2.0f) { - glm::vec3 faceNormal = sign * faceNormals[i]; - glm::vec3 secondNormal = faceNormals[(i + 1) % numDirections]; - glm::vec3 thirdNormal = faceNormals[(i + 2) % numDirections]; - - // pick a random point somewhere above the face - glm::vec3 rayStart = cubeCenter + - (cubeSide + rayOffset) * faceNormal + - (cubeSide * (randFloat() - 0.5f)) * secondNormal + - (cubeSide * (randFloat() - 0.5f)) * thirdNormal; - - // cast multiple rays that miss the face - for (int j = 0; j < numRayCasts; ++j) { - // pick a random point just outside of face - float inside = (cubeSide * (randFloat() - 0.5f)); - float outside = 0.5f * cubeSide + SOME_SMALL_NUMBER * randFloat(); - if (randFloat() - 0.5f < 0.0f) { - outside *= -1.0f; - } - glm::vec3 sidePoint = cubeCenter + 0.5f * cubeSide * faceNormal; - if (randFloat() - 0.5f < 0.0f) { - sidePoint += outside * secondNormal + inside * thirdNormal; - } else { - sidePoint += inside * secondNormal + outside * thirdNormal; - } - - // construct a ray from first point through second point - RayIntersectionInfo intersection; - intersection._rayStart = rayStart; - intersection._rayDirection = glm::normalize(sidePoint - rayStart); - - // cast the ray - QCOMPARE(cube.findRayIntersection(intersection), false); - } - } - } - } - { // ray parallel to face barely misses cube - for (int i = 0; i < numDirections; ++i) { - for (float sign = -1.0f; sign < 2.0f; sign += 2.0f) { - glm::vec3 faceNormal = sign * faceNormals[i]; - glm::vec3 secondNormal = faceNormals[(i + 1) % numDirections]; - glm::vec3 thirdNormal = faceNormals[(i + 2) % numDirections]; - - // cast multiple rays that miss the face - for (int j = 0; j < numRayCasts; ++j) { - // rayStart is above the face - glm::vec3 rayStart = cubeCenter + (0.5f + SOME_SMALL_NUMBER) * cubeSide * faceNormal; - - // move rayStart to some random edge and choose the ray direction to point across the face - float inside = (cubeSide * (randFloat() - 0.5f)); - float outside = 0.5f * cubeSide + SOME_SMALL_NUMBER * randFloat(); - if (randFloat() - 0.5f < 0.0f) { - outside *= -1.0f; - } - glm::vec3 rayDirection = secondNormal; - if (randFloat() - 0.5f < 0.0f) { - rayStart += outside * secondNormal + inside * thirdNormal; - } else { - rayStart += inside * secondNormal + outside * thirdNormal; - rayDirection = thirdNormal; - } - if (outside > 0.0f) { - rayDirection *= -1.0f; - } - - // construct a ray from first point through second point - RayIntersectionInfo intersection; - intersection._rayStart = rayStart; - intersection._rayDirection = rayDirection; - - // cast the ray - QCOMPARE(cube.findRayIntersection(intersection), false); - } - } - } - } - -} - -void ShapeColliderTests::measureTimeOfCollisionDispatch() { - - // TODO: use QBENCHMARK for this - - /* KEEP for future manual testing - // create two non-colliding spheres - float radiusA = 7.0f; - float radiusB = 3.0f; - float alpha = 1.2f; - float beta = 1.3f; - glm::vec3 offsetDirection = glm::normalize(glm::vec3(1.0f, 2.0f, 3.0f)); - float offsetDistance = alpha * radiusA + beta * radiusB; - - SphereShape sphereA(radiusA, origin); - SphereShape sphereB(radiusB, offsetDistance * offsetDirection); - CollisionList collisions(16); - - //int numTests = 1; - quint64 oldTime; - quint64 newTime; - int numTests = 100000000; - { - quint64 startTime = usecTimestampNow(); - for (int i = 0; i < numTests; ++i) { - ShapeCollider::collideShapes(&sphereA, &sphereB, collisions); - } - quint64 endTime = usecTimestampNow(); - std::cout << numTests << " non-colliding collisions in " << (endTime - startTime) << " usec" << std::endl; - newTime = endTime - startTime; - } - */ -} - diff --git a/tests/physics/src/ShapeColliderTests.h b/tests/physics/src/ShapeColliderTests.h deleted file mode 100644 index 48d9cbd742..0000000000 --- a/tests/physics/src/ShapeColliderTests.h +++ /dev/null @@ -1,60 +0,0 @@ -// -// ShapeColliderTests.h -// tests/physics/src -// -// Created by Andrew Meadows on 02/21/2014. -// 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_ShapeColliderTests_h -#define hifi_ShapeColliderTests_h - -#include -#include - -// Add additional qtest functionality (the include order is important!) -#include "BulletTestUtils.h" -#include "GlmTestUtils.h" -#include "../QTestExtensions.h" - - -class ShapeColliderTests : public QObject { - Q_OBJECT - -private slots: - void initTestCase(); - - void sphereMissesSphere(); - void sphereTouchesSphere(); - - void sphereMissesCapsule(); - void sphereTouchesCapsule(); - - void capsuleMissesCapsule(); - void capsuleTouchesCapsule(); - - void sphereTouchesAACubeFaces(); - void sphereTouchesAACubeEdges(); - void sphereTouchesAACubeCorners(); - void sphereMissesAACube(); - - void capsuleMissesAACube(); - void capsuleTouchesAACube(); - - void rayHitsSphere(); - void rayBarelyHitsSphere(); - void rayBarelyMissesSphere(); - void rayHitsCapsule(); - void rayMissesCapsule(); - void rayHitsPlane(); - void rayMissesPlane(); - void rayHitsAACube(); - void rayMissesAACube(); - - void measureTimeOfCollisionDispatch(); -}; - -#endif // hifi_ShapeColliderTests_h From 327ecfe62ccba1d78f5bbdb933e2382dddddb786 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 29 Jul 2015 09:11:58 -0700 Subject: [PATCH 151/242] Be less noisy. --- interface/src/avatar/MyAvatar.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 5f0b5f03da..054365b2b1 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -602,7 +602,6 @@ void MyAvatar::saveData() { for (int i = 0; i < animationHandles.size(); i++) { settings.setArrayIndex(i); const AnimationHandlePointer& pointer = animationHandles.at(i); - qCDebug(interfaceapp) << "Save animation" << pointer->getURL().toString(); settings.setValue("role", pointer->getRole()); settings.setValue("url", pointer->getURL()); settings.setValue("fps", pointer->getFPS()); From 7805cf4e481c07a78298c7774cb1792d55f06713 Mon Sep 17 00:00:00 2001 From: bwent Date: Wed, 29 Jul 2015 09:19:33 -0700 Subject: [PATCH 152/242] Format fixes, UI logos for collapse/expand toggle --- examples/utilities/tools/cookies.js | 35 ++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/examples/utilities/tools/cookies.js b/examples/utilities/tools/cookies.js index 24cda69ba2..751008fd99 100644 --- a/examples/utilities/tools/cookies.js +++ b/examples/utilities/tools/cookies.js @@ -12,7 +12,7 @@ HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; -var SCALE = 1000; +var SLIDER_RANGE_INCREMENT_SCALE = 1 / 1000; var THUMB_COLOR = { red: 150, green: 150, @@ -59,7 +59,6 @@ var CHECK_MARK_COLOR = { }); this.thumbSize = thumbSize; - this.thumbHalfSize = 0.5 * thumbSize; this.minThumbX = x + this.thumbHalfSize; @@ -128,7 +127,7 @@ var CHECK_MARK_COLOR = { Slider.prototype.updateWithKeys = function(direction) { this.range = this.maxThumbX - this.minThumbX; - this.thumbX += direction * (this.range / SCALE); + this.thumbX += direction * (this.range * SCALE); this.updateThumb(); this.onValueChanged(this.getValue()); }; @@ -211,6 +210,25 @@ var CHECK_MARK_COLOR = { }); }; + this.setThumbColor = function(color) { + Overlays.editOverlay(this.thumb, { + backgroundColor: { + red: color.x * 255, + green: color.y * 255, + blue: color.z * 255 + } + }); + }; + this.setBackgroundColor = function(color) { + Overlays.editOverlay(this.background, { + backgroundColor: { + red: color.x * 255, + green: color.y * 255, + blue: color.z * 255 + } + }); + }; + Slider.prototype.hide = function() { Overlays.editOverlay(this.background, { visible: false @@ -748,7 +766,7 @@ var CHECK_MARK_COLOR = { green: 255, blue: 255 }, - imageURL: HIFI_PUBLIC_BUCKET + 'images/tools/min-max-toggle.svg', + imageURL: HIFI_PUBLIC_BUCKET + 'images/tools/expand-ui.svg', x: x, y: y, width: rawHeight, @@ -778,6 +796,7 @@ var CHECK_MARK_COLOR = { }); }; + CollapsablePanelItem.prototype.destroy = function() { Overlays.deleteOverlay(this.title); Overlays.deleteOverlay(this.thumb); @@ -1044,6 +1063,7 @@ var CHECK_MARK_COLOR = { x: event.x, y: event.y }); + this.handleCollapse(clickedOverlay); // If the user clicked any of the slider background then... @@ -1106,10 +1126,15 @@ var CHECK_MARK_COLOR = { } if (!item.isCollapsed && item.isCollapsable && clickedOverlay == item.thumb) { + Overlays.editOverlay(item.thumb, { + imageURL: HIFI_PUBLIC_BUCKET + 'images/tools/expand-right.svg' + }); this.collapse(clickedOverlay); item.isCollapsed = true; - break; } else if (item.isCollapsed && item.isCollapsable && clickedOverlay == item.thumb) { + Overlays.editOverlay(item.thumb, { + imageURL: HIFI_PUBLIC_BUCKET + 'images/tools/expand-ui.svg' + }); this.expand(clickedOverlay); item.isCollapsed = false; } From 76acbde5955c1cd3f33d8e7228e19ae8899b6f34 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Wed, 29 Jul 2015 09:54:10 -0700 Subject: [PATCH 153/242] Removing the opengl and glew links from Interface since now coming from gpu lib --- interface/CMakeLists.txt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 6af3cfa08b..4f99b6b34f 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -219,21 +219,22 @@ else (APPLE) $/resources ) - find_package(OpenGL REQUIRED) + # find_package(OpenGL REQUIRED) - if (${OPENGL_INCLUDE_DIR}) - include_directories(SYSTEM "${OPENGL_INCLUDE_DIR}") - endif () + # if (${OPENGL_INCLUDE_DIR}) + # include_directories(SYSTEM "${OPENGL_INCLUDE_DIR}") +# endif () - target_link_libraries(${TARGET_NAME} "${OPENGL_LIBRARY}") + # target_link_libraries(${TARGET_NAME} "${OPENGL_LIBRARY}") # link target to external libraries if (WIN32) - add_dependency_external_projects(glew) - find_package(GLEW REQUIRED) - target_include_directories(${TARGET_NAME} PRIVATE ${GLEW_INCLUDE_DIRS}) + # add_dependency_external_projects(glew) + # find_package(GLEW REQUIRED) + # target_include_directories(${TARGET_NAME} PRIVATE ${GLEW_INCLUDE_DIRS}) - target_link_libraries(${TARGET_NAME} ${GLEW_LIBRARIES} wsock32.lib opengl32.lib Winmm.lib) + # target_link_libraries(${TARGET_NAME} ${GLEW_LIBRARIES} wsock32.lib opengl32.lib Winmm.lib) + target_link_libraries(${TARGET_NAME} wsock32.lib Winmm.lib) if (USE_NSIGHT) # try to find the Nsight package and add it to the build if we find it From 7fdba9132e34ed987fd98faa40d8274f0235275e Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 29 Jul 2015 09:55:19 -0700 Subject: [PATCH 154/242] Revert Camera.h changes. --- interface/src/Camera.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Camera.h b/interface/src/Camera.h index bddb01ef21..6eed39cf16 100644 --- a/interface/src/Camera.h +++ b/interface/src/Camera.h @@ -28,7 +28,7 @@ enum CameraMode }; Q_DECLARE_METATYPE(CameraMode); -// static int cameraModeId = qRegisterMetaType(); +static int cameraModeId = qRegisterMetaType(); class Camera : public QObject { Q_OBJECT From 12d449f69d92a8eb85a5e9f4966fbd7b9acd23a1 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 29 Jul 2015 09:55:46 -0700 Subject: [PATCH 155/242] Update test case initialization post cauterization changes. --- tests/animation/src/RigTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/animation/src/RigTests.cpp b/tests/animation/src/RigTests.cpp index 0530ad5638..8dbba30031 100644 --- a/tests/animation/src/RigTests.cpp +++ b/tests/animation/src/RigTests.cpp @@ -73,7 +73,7 @@ void RigTests::initTestCase() { } _rig = std::make_shared(); - _rig->initJointStates(jointStates, glm::mat4(), geometry.neckJointIndex); + _rig->initJointStates(jointStates, glm::mat4()); std::cout << "Rig is ready " << geometry.joints.count() << " joints " << std::endl; } From d76cda396f84943b48da360b78438fd62916254a Mon Sep 17 00:00:00 2001 From: Niraj Venkat Date: Wed, 29 Jul 2015 11:05:19 -0700 Subject: [PATCH 156/242] HBAO implementation ported from NVidia HLSL --- .../render-utils/src/ambient_occlusion.slf | 259 +++++++++++++++++- 1 file changed, 258 insertions(+), 1 deletion(-) diff --git a/libraries/render-utils/src/ambient_occlusion.slf b/libraries/render-utils/src/ambient_occlusion.slf index 3a49accf58..96aba989a8 100644 --- a/libraries/render-utils/src/ambient_occlusion.slf +++ b/libraries/render-utils/src/ambient_occlusion.slf @@ -17,7 +17,7 @@ <@include gpu/Transform.slh@> <$declareStandardTransform()$> - +/* varying vec2 varTexcoord; uniform sampler2D depthTexture; @@ -105,5 +105,262 @@ void main(void) { occlusion = 1.0 - occlusion / float(SAMPLE_COUNT); occlusion = clamp(pow(occlusion, g_intensity), 0.0, 1.0); gl_FragColor = vec4(vec3(occlusion), 1.0); +}*/ + +// This is a HBAO-Shader for OpenGL, based upon nvidias directX implementation +// supplied in their SampleSDK available from nvidia.com +// The slides describing the implementation is available at +// http://www.nvidia.co.uk/object/siggraph-2008-HBAO.html + +varying vec2 varTexcoord; + +uniform sampler2D depthTexture; +uniform sampler2D normalTexture; + +uniform float g_scale; +uniform float g_bias; +uniform float g_sample_rad; +uniform float g_intensity; +uniform float bufferWidth; +uniform float bufferHeight; + +const float PI = 3.14159265; + +const vec2 FocalLen = vec2(1.0, 1.0); +//const vec2 UVToViewA = vec2(1.0, 1.0); +//const vec2 UVToViewB = vec2(1.0, 1.0); + +const vec2 LinMAD = vec2(0.1-10.0, 0.1+10.0) / (2.0*0.1*10.0); + +const vec2 AORes = vec2(1024.0, 768.0); +const vec2 InvAORes = vec2(1.0/1024.0, 1.0/768.0); +const vec2 NoiseScale = vec2(1024.0, 768.0) / 4.0; + +const float AOStrength = 1.9; +const float R = 0.3; +const float R2 = 0.3*0.3; +const float NegInvR2 = - 1.0 / (0.3*0.3); +const float TanBias = tan(30.0 * PI / 180.0); +const float MaxRadiusPixels = 100.0; + +const int NumDirections = 6; +const int NumSamples = 4; + +float ViewSpaceZFromDepth(float d) +{ + // [0,1] -> [-1,1] clip space + d = d * 2.0 - 1.0; + + // Get view space Z + return -1.0 / (LinMAD.x * d + LinMAD.y); } +vec3 UVToViewSpace(vec2 uv, float z) +{ + //uv = UVToViewA * uv + UVToViewB; + return vec3(uv * z, z); +} + +vec3 GetViewPos(vec2 uv) +{ + float z = ViewSpaceZFromDepth(texture2D(depthTexture, uv).r); + return UVToViewSpace(uv, z); +} + +vec3 GetViewPosPoint(ivec2 uv) +{ + vec2 coord = vec2(gl_FragCoord.xy) + uv; + //float z = texelFetch(texture0, coord, 0).r; + float z = texture2D(depthTexture, uv).r; + return UVToViewSpace(uv, z); +} + +float TanToSin(float x) +{ + return x * inversesqrt(x*x + 1.0); +} + +float InvLength(vec2 V) +{ + return inversesqrt(dot(V,V)); +} + +float Tangent(vec3 V) +{ + return V.z * InvLength(V.xy); +} + +float BiasedTangent(vec3 V) +{ + return V.z * InvLength(V.xy) + TanBias; +} + +float Tangent(vec3 P, vec3 S) +{ + return -(P.z - S.z) * InvLength(S.xy - P.xy); +} + +float Length2(vec3 V) +{ + return dot(V,V); +} + +vec3 MinDiff(vec3 P, vec3 Pr, vec3 Pl) +{ + vec3 V1 = Pr - P; + vec3 V2 = P - Pl; + return (Length2(V1) < Length2(V2)) ? V1 : V2; +} + +vec2 SnapUVOffset(vec2 uv) +{ + return round(uv * AORes) * InvAORes; +} + +float Falloff(float d2) +{ + return d2 * NegInvR2 + 1.0f; +} + +float HorizonOcclusion( vec2 deltaUV, + vec3 P, + vec3 dPdu, + vec3 dPdv, + float randstep, + float numSamples) +{ + float ao = 0; + + // Offset the first coord with some noise + vec2 uv = varTexcoord + SnapUVOffset(randstep*deltaUV); + deltaUV = SnapUVOffset( deltaUV ); + + // Calculate the tangent vector + vec3 T = deltaUV.x * dPdu + deltaUV.y * dPdv; + + // Get the angle of the tangent vector from the viewspace axis + float tanH = BiasedTangent(T); + float sinH = TanToSin(tanH); + + float tanS; + float d2; + vec3 S; + + // Sample to find the maximum angle + for(float s = 1; s <= numSamples; ++s) + { + uv += deltaUV; + S = GetViewPos(uv); + tanS = Tangent(P, S); + d2 = Length2(S - P); + + // Is the sample within the radius and the angle greater? + if(d2 < R2 && tanS > tanH) + { + float sinS = TanToSin(tanS); + // Apply falloff based on the distance + ao += Falloff(d2) * (sinS - sinH); + + tanH = tanS; + sinH = sinS; + } + } + + return ao; +} + +vec2 RotateDirections(vec2 Dir, vec2 CosSin) +{ + return vec2(Dir.x*CosSin.x - Dir.y*CosSin.y, + Dir.x*CosSin.y + Dir.y*CosSin.x); +} + +void ComputeSteps(inout vec2 stepSizeUv, inout float numSteps, float rayRadiusPix, float rand) +{ + // Avoid oversampling if numSteps is greater than the kernel radius in pixels + numSteps = min(NumSamples, rayRadiusPix); + + // Divide by Ns+1 so that the farthest samples are not fully attenuated + float stepSizePix = rayRadiusPix / (numSteps + 1); + + // Clamp numSteps if it is greater than the max kernel footprint + float maxNumSteps = MaxRadiusPixels / stepSizePix; + if (maxNumSteps < numSteps) + { + // Use dithering to avoid AO discontinuities + numSteps = floor(maxNumSteps + rand); + numSteps = max(numSteps, 1); + stepSizePix = MaxRadiusPixels / numSteps; + } + + // Step size in uv space + stepSizeUv = stepSizePix * InvAORes; +} + +float getRandom(vec2 uv) { + return fract(sin(dot(uv.xy ,vec2(12.9898,78.233))) * 43758.5453); +} + +void main(void) +{ + float numDirections = NumDirections; + + vec3 P, Pr, Pl, Pt, Pb; + P = GetViewPos(varTexcoord); + + // Sample neighboring pixels + Pr = GetViewPos(varTexcoord + vec2( InvAORes.x, 0)); + Pl = GetViewPos(varTexcoord + vec2(-InvAORes.x, 0)); + Pt = GetViewPos(varTexcoord + vec2( 0, InvAORes.y)); + Pb = GetViewPos(varTexcoord + vec2( 0,-InvAORes.y)); + + // Calculate tangent basis vectors using the minimu difference + vec3 dPdu = MinDiff(P, Pr, Pl); + vec3 dPdv = MinDiff(P, Pt, Pb) * (AORes.y * InvAORes.x); + + // Get the random samples from the noise texture + vec3 random = vec3(getRandom(varTexcoord.xy), getRandom(varTexcoord.yx), getRandom(varTexcoord.xx)); + + // Calculate the projected size of the hemisphere + vec2 rayRadiusUV = 0.5 * R * FocalLen / -P.z; + float rayRadiusPix = rayRadiusUV.x * AORes.x; + + float ao = 1.0; + + // Make sure the radius of the evaluated hemisphere is more than a pixel + if(rayRadiusPix > 1.0) + { + ao = 0.0; + float numSteps; + vec2 stepSizeUV; + + // Compute the number of steps + ComputeSteps(stepSizeUV, numSteps, rayRadiusPix, random.z); + + float alpha = 2.0 * PI / numDirections; + + // Calculate the horizon occlusion of each direction + for(float d = 0; d < numDirections; ++d) + { + float theta = alpha * d; + + // Apply noise to the direction + vec2 dir = RotateDirections(vec2(cos(theta), sin(theta)), random.xy); + vec2 deltaUV = dir * stepSizeUV; + + // Sample the pixels along the direction + ao += HorizonOcclusion( deltaUV, + P, + dPdu, + dPdv, + random.z, + numSteps); + } + + // Average the results and produce the final AO + ao = 1.0 - ao / numDirections * AOStrength; + } + + //out_frag0 = vec2(ao, 30.0 * P.z); + gl_FragColor = vec4(vec3(ao), 1.0); +} \ No newline at end of file From 65e04337bf7abb58ad15e0f3159d236173a13812 Mon Sep 17 00:00:00 2001 From: ericrius1 Date: Wed, 29 Jul 2015 11:15:20 -0700 Subject: [PATCH 157/242] removed synccache line --- libraries/render-utils/src/HitEffect.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/render-utils/src/HitEffect.cpp b/libraries/render-utils/src/HitEffect.cpp index 170a6eaf0b..f65bc7666f 100644 --- a/libraries/render-utils/src/HitEffect.cpp +++ b/libraries/render-utils/src/HitEffect.cpp @@ -80,7 +80,6 @@ void HitEffect::run(const render::SceneContextPointer& sceneContext, const rende // Ready to render - // renderContext->args->_context->syncCache(); args->_context->render((batch)); } From 17650dde74598fb1cdff9df8979d7f8db096546c Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Wed, 29 Jul 2015 11:49:22 -0700 Subject: [PATCH 158/242] Solving the coimpiling issue with Render-utils test --- tests/render-utils/src/main.cpp | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/tests/render-utils/src/main.cpp b/tests/render-utils/src/main.cpp index ab01d333ac..7c02c7e8a7 100644 --- a/tests/render-utils/src/main.cpp +++ b/tests/render-utils/src/main.cpp @@ -19,6 +19,10 @@ #include #include #include + +#include +#include + #include #include #include @@ -29,7 +33,7 @@ #include #include #include -#include + #include @@ -90,6 +94,7 @@ class QTestWindow : public QWindow { QSize _size; //TextRenderer* _textRenderer[4]; RateCounter fps; + gpu::ContextPointer _gpuContext; protected: void renderText(); @@ -119,6 +124,9 @@ public: show(); makeCurrent(); + _gpuContext.reset(new gpu::Context(new gpu::GLBackend())); + + { QOpenGLDebugLogger* logger = new QOpenGLDebugLogger(this); @@ -131,23 +139,6 @@ public: } qDebug() << (const char*)glGetString(GL_VERSION); -#ifdef WIN32 - glewExperimental = true; - GLenum err = glewInit(); - if (GLEW_OK != err) { - /* Problem: glewInit failed, something is seriously wrong. */ - const GLubyte * errStr = glewGetErrorString(err); - qDebug("Error: %s\n", errStr); - } - qDebug("Status: Using GLEW %s\n", glewGetString(GLEW_VERSION)); - - if (wglewGetExtension("WGL_EXT_swap_control")) { - int swapInterval = wglGetSwapIntervalEXT(); - qDebug("V-Sync is %s\n", (swapInterval > 0 ? "ON" : "OFF")); - } - glGetError(); -#endif - //_textRenderer[0] = TextRenderer::getInstance(SANS_FONT_FAMILY, 12, false); //_textRenderer[1] = TextRenderer::getInstance(SERIF_FONT_FAMILY, 12, false, // TextRenderer::SHADOW_EFFECT); From 661f29924fd79f6be76468ff5f6376b8153db557 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Wed, 29 Jul 2015 13:55:26 -0700 Subject: [PATCH 159/242] Clean up the cmakelist to normally onlly do th eminimal linking and include for gl --- interface/CMakeLists.txt | 29 ++++------------------------- libraries/gpu/CMakeLists.txt | 2 +- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 4f99b6b34f..b6c8e3f3f4 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -218,40 +218,19 @@ else (APPLE) "${PROJECT_SOURCE_DIR}/resources" $/resources ) - - # find_package(OpenGL REQUIRED) - - # if (${OPENGL_INCLUDE_DIR}) - # include_directories(SYSTEM "${OPENGL_INCLUDE_DIR}") -# endif () - - # target_link_libraries(${TARGET_NAME} "${OPENGL_LIBRARY}") - + # link target to external libraries if (WIN32) - # add_dependency_external_projects(glew) - # find_package(GLEW REQUIRED) - # target_include_directories(${TARGET_NAME} PRIVATE ${GLEW_INCLUDE_DIRS}) - - # target_link_libraries(${TARGET_NAME} ${GLEW_LIBRARIES} wsock32.lib opengl32.lib Winmm.lib) target_link_libraries(${TARGET_NAME} wsock32.lib Winmm.lib) - + if (USE_NSIGHT) - # try to find the Nsight package and add it to the build if we find it - find_package(NSIGHT) + # If required NSIGHT lib comes from gpu lib but still to add a precroc define if (NSIGHT_FOUND) - include_directories(${NSIGHT_INCLUDE_DIRS}) add_definitions(-DNSIGHT_FOUND) - target_link_libraries(${TARGET_NAME} "${NSIGHT_LIBRARIES}") endif () endif() - else (WIN32) - find_package(GLEW REQUIRED) - target_include_directories(${TARGET_NAME} PRIVATE ${GLEW_INCLUDE_DIRS}) - - target_link_libraries(${TARGET_NAME} ${GLEW_LIBRARIES}) - message(STATUS ${GLEW_LIBRARY}) + # Nothing else required on linux apparently endif() endif (APPLE) diff --git a/libraries/gpu/CMakeLists.txt b/libraries/gpu/CMakeLists.txt index eda168091e..1e8faf1bca 100644 --- a/libraries/gpu/CMakeLists.txt +++ b/libraries/gpu/CMakeLists.txt @@ -23,7 +23,7 @@ elseif (WIN32) # try to find the Nsight package and add it to the build if we find it find_package(NSIGHT) if (NSIGHT_FOUND) - include_directories(${NSIGHT_INCLUDE_DIRS}) + target_include_directories(${TARGET_NAME} PUBLIC ${NSIGHT_INCLUDE_DIRS}) add_definitions(-DNSIGHT_FOUND) target_link_libraries(${TARGET_NAME} "${NSIGHT_LIBRARIES}") endif () From 9601e09ba9301005057a608893e82e23f7734427 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Wed, 29 Jul 2015 14:42:24 -0700 Subject: [PATCH 160/242] A simpler way to add the NSIGHT_FOUND define to all the projects depending on GPU --- interface/CMakeLists.txt | 8 +------- libraries/gpu/CMakeLists.txt | 8 ++++---- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index d47395a810..f1ef38ade9 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -221,14 +221,8 @@ else (APPLE) # link target to external libraries if (WIN32) + # target_link_libraries(${TARGET_NAME} wsock32.lib Winmm.lib) target_link_libraries(${TARGET_NAME} wsock32.lib Winmm.lib) - - if (USE_NSIGHT) - # If required NSIGHT lib comes from gpu lib but still to add a precroc define - if (NSIGHT_FOUND) - add_definitions(-DNSIGHT_FOUND) - endif () - endif() else (WIN32) # Nothing else required on linux apparently endif() diff --git a/libraries/gpu/CMakeLists.txt b/libraries/gpu/CMakeLists.txt index 1e8faf1bca..89939535b0 100644 --- a/libraries/gpu/CMakeLists.txt +++ b/libraries/gpu/CMakeLists.txt @@ -21,10 +21,11 @@ elseif (WIN32) if (USE_NSIGHT) # try to find the Nsight package and add it to the build if we find it + # note that this will also enable NSIGHT profilers in all the projects linking gpu find_package(NSIGHT) if (NSIGHT_FOUND) target_include_directories(${TARGET_NAME} PUBLIC ${NSIGHT_INCLUDE_DIRS}) - add_definitions(-DNSIGHT_FOUND) + target_compile_definitions(${TARGET_NAME} PUBLIC NSIGHT_FOUND) target_link_libraries(${TARGET_NAME} "${NSIGHT_LIBRARIES}") endif () endif() @@ -39,8 +40,7 @@ else () if (${OPENGL_INCLUDE_DIR}) include_directories(SYSTEM "${OPENGL_INCLUDE_DIR}") endif () - + target_link_libraries(${TARGET_NAME} "${GLEW_LIBRARIES}" "${OPENGL_LIBRARY}") - - # target_include_directories(${TARGET_NAME} PUBLIC ${OPENGL_INCLUDE_DIR}) + endif (APPLE) From 3c934af297617fcceb90b5b70366c673be5dc768 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Wed, 29 Jul 2015 14:45:48 -0700 Subject: [PATCH 161/242] clean the gpuCOnfig.h for linux --- libraries/gpu/src/gpu/GPUConfig.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/libraries/gpu/src/gpu/GPUConfig.h b/libraries/gpu/src/gpu/GPUConfig.h index d9b6d18894..5590c4cc8d 100644 --- a/libraries/gpu/src/gpu/GPUConfig.h +++ b/libraries/gpu/src/gpu/GPUConfig.h @@ -36,9 +36,6 @@ #else #include -//#include -//#include -//#include #define GPU_FEATURE_PROFILE GPU_CORE #define GPU_TRANSFORM_PROFILE GPU_CORE From 838711e5261e1eccf73936f5c80242dff35e73e3 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 29 Jul 2015 15:27:50 -0700 Subject: [PATCH 162/242] Remove avatar->shift hips for idle animations. See https://app.asana.com/0/32622044445063/43127903156601 --- interface/src/Menu.cpp | 2 - interface/src/Menu.h | 1 - interface/src/avatar/MyAvatar.cpp | 11 +---- interface/src/avatar/MyAvatar.h | 1 - interface/src/avatar/SkeletonModel.cpp | 68 -------------------------- interface/src/avatar/SkeletonModel.h | 7 --- tests/ui/src/main.cpp | 1 - 7 files changed, 1 insertion(+), 90 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index d6f64d360a..1cbe127857 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -249,8 +249,6 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::BlueSpeechSphere, 0, true); addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::EnableCharacterController, 0, true, avatar, SLOT(updateMotionBehavior())); - addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::ShiftHipsForIdleAnimations, 0, false, - avatar, SLOT(updateMotionBehavior())); MenuWrapper* viewMenu = addMenu("View"); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 7a9fec4e78..62a1ae3b0f 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -273,7 +273,6 @@ namespace MenuOption { const QString SimpleShadows = "Simple"; const QString SixenseEnabled = "Enable Hydra Support"; const QString SixenseMouseInput = "Enable Sixense Mouse Input"; - const QString ShiftHipsForIdleAnimations = "Shift hips for idle animations"; const QString Stars = "Stars"; const QString Stats = "Stats"; const QString StopAllScripts = "Stop All Scripts"; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index f332173568..3ec6a2b1d6 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -98,7 +98,6 @@ MyAvatar::MyAvatar() : _lookAtTargetAvatar(), _shouldRender(true), _billboardValid(false), - _feetTouchFloor(true), _eyeContactTarget(LEFT_EYE), _realWorldFieldOfView("realWorldFieldOfView", DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), @@ -166,9 +165,6 @@ void MyAvatar::update(float deltaTime) { head->setAudioAverageLoudness(audio->getAudioAverageInputLoudness()); simulate(deltaTime); - if (_feetTouchFloor) { - _skeletonModel.updateStandingFoot(); - } } void MyAvatar::simulate(float deltaTime) { @@ -1140,11 +1136,7 @@ glm::vec3 MyAvatar::getSkeletonPosition() const { // The avatar is rotated PI about the yAxis, so we have to correct for it // to get the skeleton offset contribution in the world-frame. const glm::quat FLIP = glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); - glm::vec3 skeletonOffset = _skeletonOffset; - if (_feetTouchFloor) { - skeletonOffset += _skeletonModel.getStandingOffset(); - } - return _position + getOrientation() * FLIP * skeletonOffset; + return _position + getOrientation() * FLIP * _skeletonOffset; } return Avatar::getPosition(); } @@ -1622,7 +1614,6 @@ void MyAvatar::updateMotionBehavior() { _motionBehaviors &= ~AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED; } _characterController.setEnabled(menu->isOptionChecked(MenuOption::EnableCharacterController)); - _feetTouchFloor = menu->isOptionChecked(MenuOption::ShiftHipsForIdleAnimations); } //Renders sixense laser pointers for UI selection with controllers diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index b18b8df121..90d9b47e1c 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -258,7 +258,6 @@ private: QList _animationHandles; - bool _feetTouchFloor; eyeContactTarget _eyeContactTarget; RecorderPointer _recorder; diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index fa7c5f08bb..5deebca638 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -24,12 +24,6 @@ #include "Util.h" #include "InterfaceLogging.h" -enum StandingFootState { - LEFT_FOOT, - RIGHT_FOOT, - NO_FOOT -}; - SkeletonModel::SkeletonModel(Avatar* owningAvatar, QObject* parent) : Model(parent), _triangleFanID(DependencyManager::get()->allocateID()), @@ -37,9 +31,6 @@ SkeletonModel::SkeletonModel(Avatar* owningAvatar, QObject* parent) : _boundingShape(), _boundingShapeLocalOffset(0.0f), _defaultEyeModelPosition(glm::vec3(0.0f, 0.0f, 0.0f)), - _standingFoot(NO_FOOT), - _standingOffset(0.0f), - _clampedFootPosition(0.0f), _headClipDistance(DEFAULT_NEAR_CLIP), _isFirstPerson(false) { @@ -573,65 +564,6 @@ glm::vec3 SkeletonModel::getDefaultEyeModelPosition() const { return _owningAvatar->getScale() * _defaultEyeModelPosition; } -/// \return offset of hips after foot animation -void SkeletonModel::updateStandingFoot() { - if (_geometry == NULL) { - return; - } - glm::vec3 offset(0.0f); - int leftFootIndex = _geometry->getFBXGeometry().leftToeJointIndex; - int rightFootIndex = _geometry->getFBXGeometry().rightToeJointIndex; - - if (leftFootIndex != -1 && rightFootIndex != -1) { - glm::vec3 leftPosition, rightPosition; - getJointPosition(leftFootIndex, leftPosition); - getJointPosition(rightFootIndex, rightPosition); - - int lowestFoot = (leftPosition.y < rightPosition.y) ? LEFT_FOOT : RIGHT_FOOT; - const float MIN_STEP_HEIGHT_THRESHOLD = 0.05f; - bool oneFoot = fabsf(leftPosition.y - rightPosition.y) > MIN_STEP_HEIGHT_THRESHOLD; - int currentFoot = oneFoot ? lowestFoot : _standingFoot; - - if (_standingFoot == NO_FOOT) { - currentFoot = lowestFoot; - } - if (currentFoot != _standingFoot) { - if (_standingFoot == NO_FOOT) { - // pick the lowest foot - glm::vec3 lowestPosition = (currentFoot == LEFT_FOOT) ? leftPosition : rightPosition; - // we ignore zero length positions which can happen for a few frames until skeleton is fully loaded - if (glm::length(lowestPosition) > 0.0f) { - _standingFoot = currentFoot; - _clampedFootPosition = lowestPosition; - } - } else { - // swap feet - _standingFoot = currentFoot; - glm::vec3 nextPosition = leftPosition; - glm::vec3 prevPosition = rightPosition; - if (_standingFoot == RIGHT_FOOT) { - nextPosition = rightPosition; - prevPosition = leftPosition; - } - glm::vec3 oldOffset = _clampedFootPosition - prevPosition; - _clampedFootPosition = oldOffset + nextPosition; - offset = _clampedFootPosition - nextPosition; - } - } else { - glm::vec3 nextPosition = (_standingFoot == LEFT_FOOT) ? leftPosition : rightPosition; - offset = _clampedFootPosition - nextPosition; - } - - // clamp the offset to not exceed some max distance - const float MAX_STEP_OFFSET = 1.0f; - float stepDistance = glm::length(offset); - if (stepDistance > MAX_STEP_OFFSET) { - offset *= (MAX_STEP_OFFSET / stepDistance); - } - } - _standingOffset = offset; -} - float DENSITY_OF_WATER = 1000.0f; // kg/m^3 float MIN_JOINT_MASS = 1.0f; float VERY_BIG_MASS = 1.0e6f; diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 1044a16696..6f4dd096ad 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -96,10 +96,6 @@ public: /// \return whether or not the head was found. glm::vec3 getDefaultEyeModelPosition() const; - /// skeleton offset caused by moving feet - void updateStandingFoot(); - const glm::vec3& getStandingOffset() const { return _standingOffset; } - void computeBoundingShape(const FBXGeometry& geometry); void renderBoundingCollisionShapes(gpu::Batch& batch, float alpha); float getBoundingShapeRadius() const { return _boundingShape.getRadius(); } @@ -169,9 +165,6 @@ private: glm::vec3 _boundingShapeLocalOffset; glm::vec3 _defaultEyeModelPosition; - int _standingFoot; - glm::vec3 _standingOffset; - glm::vec3 _clampedFootPosition; float _headClipDistance; // Near clip distance to use if no separate head model diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index 614c733322..e8ab7e02df 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -215,7 +215,6 @@ public: SixenseEnabled, SixenseMouseInput, SixenseLasers, - ShiftHipsForIdleAnimations, Stars, Stats, StereoAudio, From 269db0ff6fc71aeca8179e173ad3e9e042f1da6c Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Wed, 29 Jul 2015 16:08:16 -0700 Subject: [PATCH 163/242] fixing the stars rendering that was vilently broken durign the hunt for GPUCOnfig.h includes --- interface/src/Stars.cpp | 6 +----- libraries/gpu/src/gpu/GLBackendState.cpp | 6 ++++++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/interface/src/Stars.cpp b/interface/src/Stars.cpp index 7b612acb68..68e0001996 100644 --- a/interface/src/Stars.cpp +++ b/interface/src/Stars.cpp @@ -141,6 +141,7 @@ void Stars::render(RenderArgs* renderArgs, float alpha) { auto state = gpu::StatePointer(new gpu::State()); // enable decal blend state->setDepthTest(gpu::State::DepthTest(false)); + state->setAntialiasedLineEnable(true); // line smoothing also smooth points state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); _gridPipeline.reset(gpu::Pipeline::create(program, state)); } @@ -205,8 +206,6 @@ void Stars::render(RenderArgs* renderArgs, float alpha) { batch._glUniform1f(_timeSlot, secs); geometryCache->renderUnitCube(batch); - //glHint(GL_POINT_SMOOTH_HINT, GL_NICEST); - static const size_t VERTEX_STRIDE = sizeof(StarVertex); size_t offset = offsetof(StarVertex, position); gpu::BufferView posView(vertexBuffer, offset, vertexBuffer->getSize(), VERTEX_STRIDE, positionElement); @@ -215,9 +214,6 @@ void Stars::render(RenderArgs* renderArgs, float alpha) { // Render the stars batch.setPipeline(_starsPipeline); - //batch._glEnable(GL_PROGRAM_POINT_SIZE_EXT); - //batch._glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); - //batch._glEnable(GL_POINT_SMOOTH); batch.setInputFormat(streamFormat); batch.setInputBuffer(VERTICES_SLOT, posView); diff --git a/libraries/gpu/src/gpu/GLBackendState.cpp b/libraries/gpu/src/gpu/GLBackendState.cpp index 18fc9ddd3c..e9dcd3aad3 100644 --- a/libraries/gpu/src/gpu/GLBackendState.cpp +++ b/libraries/gpu/src/gpu/GLBackendState.cpp @@ -482,6 +482,12 @@ void GLBackend::syncPipelineStateCache() { State::Data state; glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); + + // Point size is always on + glHint(GL_POINT_SMOOTH_HINT, GL_NICEST); + glEnable(GL_PROGRAM_POINT_SIZE_EXT); + glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); + getCurrentGLState(state); State::Signature signature = State::evalSignature(state); From 5baf993c24a70b26de159259a5a6fbb5d6ffbc0f Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Wed, 29 Jul 2015 16:27:45 -0700 Subject: [PATCH 164/242] fixing the stars again --- interface/src/Stars.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/Stars.cpp b/interface/src/Stars.cpp index 68e0001996..42b1a3f2e2 100644 --- a/interface/src/Stars.cpp +++ b/interface/src/Stars.cpp @@ -141,7 +141,6 @@ void Stars::render(RenderArgs* renderArgs, float alpha) { auto state = gpu::StatePointer(new gpu::State()); // enable decal blend state->setDepthTest(gpu::State::DepthTest(false)); - state->setAntialiasedLineEnable(true); // line smoothing also smooth points state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); _gridPipeline.reset(gpu::Pipeline::create(program, state)); } @@ -153,6 +152,7 @@ void Stars::render(RenderArgs* renderArgs, float alpha) { auto state = gpu::StatePointer(new gpu::State()); // enable decal blend state->setDepthTest(gpu::State::DepthTest(false)); + state->setAntialiasedLineEnable(true); // line smoothing also smooth points state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); _starsPipeline.reset(gpu::Pipeline::create(program, state)); @@ -219,6 +219,6 @@ void Stars::render(RenderArgs* renderArgs, float alpha) { batch.setInputBuffer(VERTICES_SLOT, posView); batch.setInputBuffer(COLOR_SLOT, colView); batch.draw(gpu::Primitive::POINTS, STARFIELD_NUM_STARS); - + renderArgs->_context->render(batch); } From 4972cb024f34eb5fe2f4cfa34080be52ae06f3b2 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Wed, 29 Jul 2015 16:48:23 -0700 Subject: [PATCH 165/242] Try to make the inlucde sequence simpler in gpu for GLBackend --- libraries/gpu/src/gpu/Batch.cpp | 18 ------------------ libraries/gpu/src/gpu/GLBackend.cpp | 1 - libraries/gpu/src/gpu/GLBackend.h | 2 -- libraries/gpu/src/gpu/GLBackendShared.h | 2 -- 4 files changed, 23 deletions(-) diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index b7f3ef444c..a10f5b3de7 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -10,12 +10,6 @@ // #include "Batch.h" -#include -#include - -#include "GPUConfig.h" - - #if defined(NSIGHT_FOUND) #include "nvToolsExt.h" @@ -288,15 +282,3 @@ void Batch::getQuery(const QueryPointer& query) { _params.push_back(_queries.cache(query)); } -void push_back(Batch::Params& params, const vec3& v) { - params.push_back(v.x); - params.push_back(v.y); - params.push_back(v.z); -} - -void push_back(Batch::Params& params, const vec4& v) { - params.push_back(v.x); - params.push_back(v.y); - params.push_back(v.z); - params.push_back(v.a); -} diff --git a/libraries/gpu/src/gpu/GLBackend.cpp b/libraries/gpu/src/gpu/GLBackend.cpp index 4bc8abadd0..51a767882c 100644 --- a/libraries/gpu/src/gpu/GLBackend.cpp +++ b/libraries/gpu/src/gpu/GLBackend.cpp @@ -11,7 +11,6 @@ #include "GLBackendShared.h" #include -#include "GPULogging.h" #include using namespace gpu; diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index 9d8c9ef805..3b1d4cde5e 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -18,8 +18,6 @@ #include "GPUConfig.h" #include "Context.h" -#include "Batch.h" - namespace gpu { diff --git a/libraries/gpu/src/gpu/GLBackendShared.h b/libraries/gpu/src/gpu/GLBackendShared.h index 75bef461f9..888fd1164d 100644 --- a/libraries/gpu/src/gpu/GLBackendShared.h +++ b/libraries/gpu/src/gpu/GLBackendShared.h @@ -16,8 +16,6 @@ #include "GPULogging.h" #include "GLBackend.h" -#include "Batch.h" - static const GLenum _primitiveToGLmode[gpu::NUM_PRIMITIVES] = { GL_POINTS, GL_LINES, From 8b7ad9cfcbaa68ab360c0a7561e5af5da8ee3c6e Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Wed, 29 Jul 2015 17:13:48 -0700 Subject: [PATCH 166/242] merge with upstream --- interface/src/Application.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5845267f72..1dc2878008 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1019,7 +1019,9 @@ void Application::paintGL() { // Back to the default framebuffer; gpu::Batch batch; batch.resetStages(); - // renderArgs._context->render(batch); + // TODO: Testing the water here, it is maybe not needed at all, + // i would like to keep it like that until we merge with DisplayPlugin + // renderArgs._context->render(batch); } void Application::runTests() { From c2934bdb5d3fc8e1b1fac2d60c0eec1430ae8d06 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 29 Jul 2015 17:35:29 -0700 Subject: [PATCH 167/242] Fix avatar head, eye and torso twist. * Updated SkeletionModel::updateRig to explicitly pass a set of HeadParameters to the rig to do procedural animation. * Moved torso lean procedural animation from SkeletonModel into Rig. * Moved eye tracking procedural animation from HeadModel into Rig. * Moved neck procedural animation from HeadModel into Rig. --- interface/src/avatar/FaceModel.cpp | 54 --------------------- interface/src/avatar/FaceModel.h | 10 ++-- interface/src/avatar/SkeletonModel.cpp | 57 +++++++--------------- libraries/animation/src/AvatarRig.cpp | 18 ------- libraries/animation/src/AvatarRig.h | 1 - libraries/animation/src/EntityRig.h | 1 - libraries/animation/src/Rig.cpp | 65 ++++++++++++++++++++++++++ libraries/animation/src/Rig.h | 25 ++++++++-- 8 files changed, 108 insertions(+), 123 deletions(-) diff --git a/interface/src/avatar/FaceModel.cpp b/interface/src/avatar/FaceModel.cpp index e35c70a8ab..d5d4da8665 100644 --- a/interface/src/avatar/FaceModel.cpp +++ b/interface/src/avatar/FaceModel.cpp @@ -50,60 +50,6 @@ void FaceModel::simulate(float deltaTime, bool fullUpdate) { } } -void FaceModel::maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, int index) { - // get the rotation axes in joint space and use them to adjust the rotation - glm::mat3 axes = glm::mat3_cast(glm::quat()); - glm::mat3 inverse = glm::mat3(glm::inverse(parentState.getTransform() * - glm::translate(_rig->getJointDefaultTranslationInConstrainedFrame(index)) * - joint.preTransform * glm::mat4_cast(joint.preRotation))); - glm::vec3 pitchYawRoll = safeEulerAngles(_owningHead->getFinalOrientationInLocalFrame()); - glm::vec3 lean = glm::radians(glm::vec3(_owningHead->getFinalLeanForward(), - _owningHead->getTorsoTwist(), - _owningHead->getFinalLeanSideways())); - pitchYawRoll -= lean; - _rig->setJointRotationInConstrainedFrame(index, - glm::angleAxis(-pitchYawRoll.z, glm::normalize(inverse * axes[2])) - * glm::angleAxis(pitchYawRoll.y, glm::normalize(inverse * axes[1])) - * glm::angleAxis(-pitchYawRoll.x, glm::normalize(inverse * axes[0])) - * joint.rotation, DEFAULT_PRIORITY); -} - -void FaceModel::maybeUpdateEyeRotation(Model* model, const JointState& parentState, const FBXJoint& joint, int index) { - // likewise with the eye joints - // NOTE: at the moment we do the math in the world-frame, hence the inverse transform is more complex than usual. - glm::mat4 inverse = glm::inverse(glm::mat4_cast(model->getRotation()) * parentState.getTransform() * - glm::translate(_rig->getJointDefaultTranslationInConstrainedFrame(index)) * - joint.preTransform * glm::mat4_cast(joint.preRotation * joint.rotation)); - glm::vec3 front = glm::vec3(inverse * glm::vec4(_owningHead->getFinalOrientationInWorldFrame() * IDENTITY_FRONT, 0.0f)); - glm::vec3 lookAtDelta = _owningHead->getCorrectedLookAtPosition() - model->getTranslation(); - glm::vec3 lookAt = glm::vec3(inverse * glm::vec4(lookAtDelta + glm::length(lookAtDelta) * _owningHead->getSaccade(), 1.0f)); - glm::quat between = rotationBetween(front, lookAt); - const float MAX_ANGLE = 30.0f * RADIANS_PER_DEGREE; - _rig->setJointRotationInConstrainedFrame(index, glm::angleAxis(glm::clamp(glm::angle(between), - -MAX_ANGLE, MAX_ANGLE), glm::axis(between)) * - joint.rotation, DEFAULT_PRIORITY); -} - -void FaceModel::updateJointState(int index) { - const JointState& state = _rig->getJointState(index); - const FBXJoint& joint = state.getFBXJoint(); - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - - // guard against out-of-bounds access to _jointStates - if (joint.parentIndex != -1 && joint.parentIndex >= 0 && joint.parentIndex < _rig->getJointStateCount()) { - const JointState& parentState = _rig->getJointState(joint.parentIndex); - if (index == geometry.neckJointIndex) { - maybeUpdateNeckRotation(parentState, joint, index); - - } else if (index == geometry.leftEyeJointIndex || index == geometry.rightEyeJointIndex) { - maybeUpdateEyeRotation(this, parentState, joint, index); - } - } - - glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - _rig->updateFaceJointState(index, parentTransform); -} - bool FaceModel::getEyePositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const { if (!isActive()) { return false; diff --git a/interface/src/avatar/FaceModel.h b/interface/src/avatar/FaceModel.h index ce78c51e70..5a19a8ea29 100644 --- a/interface/src/avatar/FaceModel.h +++ b/interface/src/avatar/FaceModel.h @@ -19,23 +19,19 @@ class Head; /// A face formed from a linear mix of blendshapes according to a set of coefficients. class FaceModel : public Model { Q_OBJECT - + public: FaceModel(Head* owningHead, RigPointer rig); virtual void simulate(float deltaTime, bool fullUpdate = true); - - virtual void maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, int index); - virtual void maybeUpdateEyeRotation(Model* model, const JointState& parentState, const FBXJoint& joint, int index); - virtual void updateJointState(int index); /// Retrieve the positions of up to two eye meshes. /// \return whether or not both eye meshes were found bool getEyePositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const; - + private: - + Head* _owningHead; }; diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 122559bedb..a766a80bab 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -118,6 +118,24 @@ void SkeletonModel::updateClusterMatrices() { void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { _rig->computeMotionAnimationState(deltaTime, _owningAvatar->getPosition(), _owningAvatar->getVelocity(), _owningAvatar->getOrientation()); Model::updateRig(deltaTime, parentTransform); + if (_owningAvatar->isMyAvatar()) { + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + + Rig::HeadParameters params; + params.leanSideways = _owningAvatar->getHead()->getFinalLeanSideways(); + params.leanForward = _owningAvatar->getHead()->getFinalLeanSideways(); + params.torsoTwist = _owningAvatar->getHead()->getTorsoTwist(); + params.localHeadOrientation = _owningAvatar->getHead()->getFinalOrientationInLocalFrame(); + params.worldHeadOrientation = _owningAvatar->getHead()->getFinalOrientationInWorldFrame(); + params.eyeLookAt = _owningAvatar->getHead()->getCorrectedLookAtPosition(); + params.eyeSaccade = _owningAvatar->getHead()->getSaccade(); + params.leanJointIndex = geometry.leanJointIndex; + params.neckJointIndex = geometry.neckJointIndex; + params.leftEyeJointIndex = geometry.leftEyeJointIndex; + params.rightEyeJointIndex = geometry.rightEyeJointIndex; + + _rig->updateFromHeadParameters(params); + } } void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { @@ -259,51 +277,12 @@ void SkeletonModel::updateJointState(int index) { const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - const JointState joint = _rig->getJointState(index); - if (joint.getParentIndex() >= 0 && joint.getParentIndex() < _rig->getJointStateCount()) { - const JointState parentState = _rig->getJointState(joint.getParentIndex()); - if (index == geometry.leanJointIndex) { - maybeUpdateLeanRotation(parentState, index); - - } else if (index == geometry.neckJointIndex) { - maybeUpdateNeckRotation(parentState, joint.getFBXJoint(), index); - - } else if (index == geometry.leftEyeJointIndex || index == geometry.rightEyeJointIndex) { - maybeUpdateEyeRotation(parentState, joint.getFBXJoint(), index); - } - } - _rig->updateJointState(index, parentTransform); if (index == _geometry->getFBXGeometry().rootJointIndex) { _rig->clearJointTransformTranslation(index); } } - -void SkeletonModel::maybeUpdateLeanRotation(const JointState& parentState, int index) { - if (!_owningAvatar->isMyAvatar()) { - return; - } - // get the rotation axes in joint space and use them to adjust the rotation - glm::vec3 xAxis(1.0f, 0.0f, 0.0f); - glm::vec3 yAxis(0.0f, 1.0f, 0.0f); - glm::vec3 zAxis(0.0f, 0.0f, 1.0f); - glm::quat inverse = glm::inverse(parentState.getRotation() * _rig->getJointDefaultRotationInParentFrame(index)); - _rig->setJointRotationInConstrainedFrame(index, - glm::angleAxis(- RADIANS_PER_DEGREE * _owningAvatar->getHead()->getFinalLeanSideways(), inverse * zAxis) - * glm::angleAxis(- RADIANS_PER_DEGREE * _owningAvatar->getHead()->getFinalLeanForward(), inverse * xAxis) - * glm::angleAxis(RADIANS_PER_DEGREE * _owningAvatar->getHead()->getTorsoTwist(), inverse * yAxis) - * _rig->getJointState(index).getFBXJoint().rotation, LEAN_PRIORITY); -} - -void SkeletonModel::maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, int index) { - _owningAvatar->getHead()->getFaceModel().maybeUpdateNeckRotation(parentState, joint, index); -} - -void SkeletonModel::maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, int index) { - _owningAvatar->getHead()->getFaceModel().maybeUpdateEyeRotation(this, parentState, joint, index); -} - void SkeletonModel::renderJointConstraints(gpu::Batch& batch, int jointIndex) { if (jointIndex == -1 || jointIndex >= _rig->getJointStateCount()) { return; diff --git a/libraries/animation/src/AvatarRig.cpp b/libraries/animation/src/AvatarRig.cpp index cf05e61cdb..3b48b0814f 100644 --- a/libraries/animation/src/AvatarRig.cpp +++ b/libraries/animation/src/AvatarRig.cpp @@ -31,21 +31,3 @@ void AvatarRig::updateJointState(int index, glm::mat4 parentTransform) { } } } - - -void AvatarRig::updateFaceJointState(int index, glm::mat4 parentTransform) { - JointState& state = _jointStates[index]; - const FBXJoint& joint = state.getFBXJoint(); - - // compute model transforms - int parentIndex = joint.parentIndex; - if (parentIndex == -1) { - state.computeTransform(parentTransform); - } else { - // guard against out-of-bounds access to _jointStates - if (joint.parentIndex >= 0 && joint.parentIndex < _jointStates.size()) { - const JointState& parentState = _jointStates.at(parentIndex); - state.computeTransform(parentState.getTransform(), parentState.getTransformChanged()); - } - } -} diff --git a/libraries/animation/src/AvatarRig.h b/libraries/animation/src/AvatarRig.h index dbffd8aa45..4a111a535b 100644 --- a/libraries/animation/src/AvatarRig.h +++ b/libraries/animation/src/AvatarRig.h @@ -22,7 +22,6 @@ class AvatarRig : public Rig { public: ~AvatarRig() {} virtual void updateJointState(int index, glm::mat4 parentTransform); - virtual void updateFaceJointState(int index, glm::mat4 parentTransform); }; #endif // hifi_AvatarRig_h diff --git a/libraries/animation/src/EntityRig.h b/libraries/animation/src/EntityRig.h index aa6a5fbd2b..e8e15a5a28 100644 --- a/libraries/animation/src/EntityRig.h +++ b/libraries/animation/src/EntityRig.h @@ -22,7 +22,6 @@ class EntityRig : public Rig { public: ~EntityRig() {} virtual void updateJointState(int index, glm::mat4 parentTransform); - virtual void updateFaceJointState(int index, glm::mat4 parentTransform) { } }; #endif // hifi_EntityRig_h diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index dc7b37129e..2ff6faa868 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -665,3 +665,68 @@ glm::quat Rig::getJointDefaultRotationInParentFrame(int jointIndex) { } return _jointStates[jointIndex].getDefaultRotationInParentFrame(); } + +void Rig::updateFromHeadParameters(const HeadParameters& params) { + updateLeanJoint(params.leanJointIndex, params.leanSideways, params.leanForward, params.torsoTwist); + updateNeckJoint(params.neckJointIndex, params.localHeadOrientation, params.leanSideways, params.leanForward, params.torsoTwist); + updateEyeJoint(params.leftEyeJointIndex, params.worldHeadOrientation, params.eyeLookAt, params.eyeSaccade); + updateEyeJoint(params.rightEyeJointIndex, params.worldHeadOrientation, params.eyeLookAt, params.eyeSaccade); +} + +void Rig::updateLeanJoint(int index, float leanSideways, float leanForward, float torsoTwist) { + if (index > 0 && _jointStates[index].getParentIndex() > 0) { + auto& parentState = _jointStates[_jointStates[index].getParentIndex()]; + + // get the rotation axes in joint space and use them to adjust the rotation + glm::vec3 xAxis(1.0f, 0.0f, 0.0f); + glm::vec3 yAxis(0.0f, 1.0f, 0.0f); + glm::vec3 zAxis(0.0f, 0.0f, 1.0f); + glm::quat inverse = glm::inverse(parentState.getRotation() * getJointDefaultRotationInParentFrame(index)); + setJointRotationInConstrainedFrame(index, + glm::angleAxis(- RADIANS_PER_DEGREE * leanSideways, inverse * zAxis) * + glm::angleAxis(- RADIANS_PER_DEGREE * leanForward, inverse * xAxis) * + glm::angleAxis(RADIANS_PER_DEGREE * torsoTwist, inverse * yAxis) * + getJointState(index).getFBXJoint().rotation, DEFAULT_PRIORITY); + } +} + +void Rig::updateNeckJoint(int index, const glm::quat& localHeadOrientation, float leanSideways, float leanForward, float torsoTwist) { + if (index > 0 && _jointStates[index].getParentIndex() > 0) { + auto& parentState = _jointStates[_jointStates[index].getParentIndex()]; + auto joint = _jointStates[index].getFBXJoint(); + + // get the rotation axes in joint space and use them to adjust the rotation + glm::mat3 axes = glm::mat3_cast(glm::quat()); + glm::mat3 inverse = glm::mat3(glm::inverse(parentState.getTransform() * + glm::translate(getJointDefaultTranslationInConstrainedFrame(index)) * + joint.preTransform * glm::mat4_cast(joint.preRotation))); + glm::vec3 pitchYawRoll = safeEulerAngles(localHeadOrientation); + glm::vec3 lean = glm::radians(glm::vec3(leanForward, torsoTwist, leanSideways)); + pitchYawRoll -= lean; + setJointRotationInConstrainedFrame(index, + glm::angleAxis(-pitchYawRoll.z, glm::normalize(inverse * axes[2])) * + glm::angleAxis(pitchYawRoll.y, glm::normalize(inverse * axes[1])) * + glm::angleAxis(-pitchYawRoll.x, glm::normalize(inverse * axes[0])) * + joint.rotation, DEFAULT_PRIORITY); + } +} + +void Rig::updateEyeJoint(int index, const glm::quat& worldHeadOrientation, const glm::vec3& lookAt, const glm::vec3& saccade) { + if ( index > 0 && _jointStates[index].getParentIndex() > 0) { + auto& parentState = _jointStates[_jointStates[index].getParentIndex()]; + auto joint = _jointStates[index].getFBXJoint(); + + // NOTE: at the moment we do the math in the world-frame, hence the inverse transform is more complex than usual. + glm::mat4 inverse = glm::inverse(parentState.getTransform() * + glm::translate(getJointDefaultTranslationInConstrainedFrame(index)) * + joint.preTransform * glm::mat4_cast(joint.preRotation * joint.rotation)); + glm::vec3 front = glm::vec3(inverse * glm::vec4(worldHeadOrientation * IDENTITY_FRONT, 0.0f)); + glm::vec3 lookAtDelta = lookAt; + glm::vec3 lookAt = glm::vec3(inverse * glm::vec4(lookAtDelta + glm::length(lookAtDelta) * saccade, 1.0f)); + glm::quat between = rotationBetween(front, lookAt); + const float MAX_ANGLE = 30.0f * RADIANS_PER_DEGREE; + float angle = glm::clamp(glm::angle(between), -MAX_ANGLE, MAX_ANGLE); + glm::quat rot = glm::angleAxis(angle, glm::axis(between)); + setJointRotationInConstrainedFrame(index, rot * joint.rotation, DEFAULT_PRIORITY); + } +} diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index fe6bc82f35..2eb9d0e0b3 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -47,14 +47,27 @@ typedef std::shared_ptr AnimationHandlePointer; class Rig; typedef std::shared_ptr RigPointer; - class Rig : public QObject, public std::enable_shared_from_this { public: + struct HeadParameters { + float leanSideways = 0.0f; // degrees + float leanForward = 0.0f; // degrees + float torsoTwist = 0.0f; // degrees + glm::quat localHeadOrientation = glm::quat(); + glm::quat worldHeadOrientation = glm::quat(); + glm::vec3 eyeLookAt = glm::vec3(); // world space + glm::vec3 eyeSaccade = glm::vec3(); // world space + int leanJointIndex = -1; + int neckJointIndex = -1; + int leftEyeJointIndex = -1; + int rightEyeJointIndex = -1; + }; + virtual ~Rig() {} - RigPointer getRigPointer() { return shared_from_this(); } + RigPointer getRigPointer() { return shared_from_this(); } AnimationHandlePointer createAnimationHandle(); void removeAnimationHandle(const AnimationHandlePointer& handle); @@ -129,11 +142,17 @@ public: void updateVisibleJointStates(); virtual void updateJointState(int index, glm::mat4 parentTransform) = 0; - virtual void updateFaceJointState(int index, glm::mat4 parentTransform) = 0; void setEnableRig(bool isEnabled) { _enableRig = isEnabled; } + void updateFromHeadParameters(const HeadParameters& params); + protected: + + void updateLeanJoint(int index, float leanSideways, float leanForward, float torsoTwist); + void updateNeckJoint(int index, const glm::quat& localHeadOrientation, float leanSideways, float leanForward, float torsoTwist); + void updateEyeJoint(int index, const glm::quat& worldHeadOrientation, const glm::vec3& lookAt, const glm::vec3& saccade); + QVector _jointStates; QList _animationHandles; From ac46b2bd1733ba18fe6e1db99a1ee204c7408bdc Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 29 Jul 2015 17:38:23 -0700 Subject: [PATCH 168/242] Keep the resetstage in --- interface/src/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1dc2878008..8f37174c18 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1021,7 +1021,7 @@ void Application::paintGL() { batch.resetStages(); // TODO: Testing the water here, it is maybe not needed at all, // i would like to keep it like that until we merge with DisplayPlugin - // renderArgs._context->render(batch); + renderArgs._context->render(batch); } void Application::runTests() { From b7ecffa0beba9a47969e97f69063ab761a613781 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 29 Jul 2015 17:55:56 -0700 Subject: [PATCH 169/242] treat a "g" in an obj file the same as a "o" --- libraries/fbx/src/OBJReader.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index f16c6ba215..da63b2f47f 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -306,7 +306,8 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi } QByteArray token = tokenizer.getDatum(); //qCDebug(modelformat) << token; - if (token == "g") { + // we don't support separate objects in the same file, so treat "o" the same as "g". + if (token == "g" || token == "o") { if (sawG) { // we've encountered the beginning of the next group. tokenizer.pushBackToken(OBJTokenizer::DATUM_TOKEN); From e32e45ed2b8faefc74c0f884d96eb9e1ee3a8865 Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 29 Jul 2015 18:06:46 -0700 Subject: [PATCH 170/242] make sure the writting mask is on for depth buffer --- interface/src/Application.cpp | 2 -- libraries/gpu/src/gpu/GLBackendOutput.cpp | 12 ++++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 8f37174c18..1c91f9282c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1019,8 +1019,6 @@ void Application::paintGL() { // Back to the default framebuffer; gpu::Batch batch; batch.resetStages(); - // TODO: Testing the water here, it is maybe not needed at all, - // i would like to keep it like that until we merge with DisplayPlugin renderArgs._context->render(batch); } diff --git a/libraries/gpu/src/gpu/GLBackendOutput.cpp b/libraries/gpu/src/gpu/GLBackendOutput.cpp index d57ef57d78..c898b4a843 100755 --- a/libraries/gpu/src/gpu/GLBackendOutput.cpp +++ b/libraries/gpu/src/gpu/GLBackendOutput.cpp @@ -209,10 +209,17 @@ void GLBackend::do_clearFramebuffer(Batch& batch, uint32 paramOffset) { int stencil = batch._params[paramOffset + 1]._int; int useScissor = batch._params[paramOffset + 0]._int; + bool restoreDepthMask = false; GLuint glmask = 0; if (masks & Framebuffer::BUFFER_STENCIL) { glClearStencil(stencil); glmask |= GL_STENCIL_BUFFER_BIT; + + bool cacheDepthMask = _pipeline._stateCache.depthTest.getWriteMask(); + if (!cacheDepthMask) { + restoreDepthMask = true; + glDepthMask(GL_TRUE); + } } if (masks & Framebuffer::BUFFER_DEPTH) { @@ -252,6 +259,11 @@ void GLBackend::do_clearFramebuffer(Batch& batch, uint32 paramOffset) { glDisable(GL_SCISSOR_TEST); } + // REstore write mask + if (restoreDepthMask) { + glDepthMask(GL_FALSE); + } + // Restore the color draw buffers only if a frmaebuffer is bound if (_output._framebuffer && !drawBuffers.empty()) { auto glFramebuffer = syncGPUObject(*_output._framebuffer); From 70d64a7777f90d6ae2afabc20348adfceb00e5e1 Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 29 Jul 2015 18:27:10 -0700 Subject: [PATCH 171/242] Really fixing the depth write mask issue on clear... --- libraries/gpu/src/gpu/GLBackendOutput.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/libraries/gpu/src/gpu/GLBackendOutput.cpp b/libraries/gpu/src/gpu/GLBackendOutput.cpp index c898b4a843..d75d0cf521 100755 --- a/libraries/gpu/src/gpu/GLBackendOutput.cpp +++ b/libraries/gpu/src/gpu/GLBackendOutput.cpp @@ -209,11 +209,18 @@ void GLBackend::do_clearFramebuffer(Batch& batch, uint32 paramOffset) { int stencil = batch._params[paramOffset + 1]._int; int useScissor = batch._params[paramOffset + 0]._int; - bool restoreDepthMask = false; GLuint glmask = 0; if (masks & Framebuffer::BUFFER_STENCIL) { glClearStencil(stencil); glmask |= GL_STENCIL_BUFFER_BIT; + // TODO: we will probably need to also check the write mask of stencil like we do + // for depth buffer, but as would say a famous Fez owner "We'll cross that bridge when we come to it" + } + + bool restoreDepthMask = false; + if (masks & Framebuffer::BUFFER_DEPTH) { + glClearDepth(depth); + glmask |= GL_DEPTH_BUFFER_BIT; bool cacheDepthMask = _pipeline._stateCache.depthTest.getWriteMask(); if (!cacheDepthMask) { @@ -222,11 +229,6 @@ void GLBackend::do_clearFramebuffer(Batch& batch, uint32 paramOffset) { } } - if (masks & Framebuffer::BUFFER_DEPTH) { - glClearDepth(depth); - glmask |= GL_DEPTH_BUFFER_BIT; - } - std::vector drawBuffers; if (masks & Framebuffer::BUFFER_COLORS) { for (unsigned int i = 0; i < Framebuffer::MAX_NUM_RENDER_BUFFERS; i++) { @@ -259,7 +261,7 @@ void GLBackend::do_clearFramebuffer(Batch& batch, uint32 paramOffset) { glDisable(GL_SCISSOR_TEST); } - // REstore write mask + // Restore write mask meaning turn back off if (restoreDepthMask) { glDepthMask(GL_FALSE); } From 291e0e21ae858d09b53d05cbb1fc2dc19c7e306f Mon Sep 17 00:00:00 2001 From: Niraj Venkat Date: Wed, 29 Jul 2015 18:47:27 -0700 Subject: [PATCH 172/242] HBAO final implementation --- .../src/AmbientOcclusionEffect.cpp | 2 +- .../render-utils/src/ambient_occlusion.slf | 183 +++--------------- .../render-utils/src/occlusion_blend.slf | 7 +- 3 files changed, 34 insertions(+), 158 deletions(-) diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index f19fa6e18a..720f765ae4 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -164,7 +164,7 @@ const gpu::PipelinePointer& AmbientOcclusion::getBlendPipeline() { // Blend on transparent state->setBlendFunction(true, - gpu::State::SRC_COLOR, gpu::State::BLEND_OP_ADD, gpu::State::DEST_COLOR); + gpu::State::INV_SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::SRC_ALPHA); // Good to go add the brand new pipeline _blendPipeline.reset(gpu::Pipeline::create(program, state)); diff --git a/libraries/render-utils/src/ambient_occlusion.slf b/libraries/render-utils/src/ambient_occlusion.slf index 96aba989a8..f45fd9b6a0 100644 --- a/libraries/render-utils/src/ambient_occlusion.slf +++ b/libraries/render-utils/src/ambient_occlusion.slf @@ -17,99 +17,8 @@ <@include gpu/Transform.slh@> <$declareStandardTransform()$> -/* -varying vec2 varTexcoord; -uniform sampler2D depthTexture; -uniform sampler2D normalTexture; - -uniform float g_scale; -uniform float g_bias; -uniform float g_sample_rad; -uniform float g_intensity; -uniform float bufferWidth; -uniform float bufferHeight; - -#define SAMPLE_COUNT 4 - -float getRandom(vec2 uv) { - return fract(sin(dot(uv.xy ,vec2(12.9898,78.233))) * 43758.5453); -} - -void main(void) { - vec3 sampleKernel[4] = { vec3(0.2, 0.0, 0.0), - vec3(0.0, 0.2, 0.0), - vec3(0.0, 0.0, 0.2), - vec3(0.2, 0.2, 0.2) }; - - TransformCamera cam = getTransformCamera(); - TransformObject obj = getTransformObject(); - - vec3 eyeDir = vec3(0.0, 0.0, -3.0); - vec3 cameraPositionWorldSpace; - <$transformEyeToWorldDir(cam, eyeDir, cameraPositionWorldSpace)$> - - vec4 depthColor = texture2D(depthTexture, varTexcoord); - - // z in non linear range [0,1] - float depthVal = depthColor.r; - // conversion into NDC [-1,1] - float zNDC = depthVal * 2.0 - 1.0; - float n = 1.0; // the near plane - float f = 30.0; // the far plane - float l = -1.0; // left - float r = 1.0; // right - float b = -1.0; // bottom - float t = 1.0; // top - - // conversion into eye space - float zEye = 2*f*n / (zNDC*(f-n)-(f+n)); - // Converting from pixel coordinates to NDC - float xNDC = gl_FragCoord.x/bufferWidth * 2.0 - 1.0; - float yNDC = gl_FragCoord.y/bufferHeight * 2.0 - 1.0; - // Unprojecting X and Y from NDC to eye space - float xEye = -zEye*(xNDC*(r-l)+(r+l))/(2.0*n); - float yEye = -zEye*(yNDC*(t-b)+(t+b))/(2.0*n); - vec3 currentFragEyeSpace = vec3(xEye, yEye, zEye); - vec3 currentFragWorldSpace; - <$transformEyeToWorldDir(cam, currentFragEyeSpace, currentFragWorldSpace)$> - - vec3 cameraToPositionRay = normalize(currentFragWorldSpace - cameraPositionWorldSpace); - vec3 origin = cameraToPositionRay * depthVal + cameraPositionWorldSpace; - - vec3 normal = normalize(texture2D(normalTexture, varTexcoord).xyz); - //normal = normalize(normal * normalMatrix); - - vec3 rvec = vec3(getRandom(varTexcoord.xy), getRandom(varTexcoord.yx), getRandom(varTexcoord.xx)) * 2.0 - 1.0; - vec3 tangent = normalize(rvec - normal * dot(rvec, normal)); - vec3 bitangent = cross(normal, tangent); - mat3 tbn = mat3(tangent, bitangent, normal); - - float occlusion = 0.0; - - for (int i = 0; i < SAMPLE_COUNT; ++i) { - vec3 samplePos = origin + (tbn * sampleKernel[i]) * g_sample_rad; - vec4 offset = cam._projectionViewUntranslated * vec4(samplePos, 1.0); - - offset.xy = (offset.xy / offset.w) * 0.5 + 0.5; - float depth = length(samplePos - cameraPositionWorldSpace); - - float sampleDepthVal = texture2D(depthTexture, offset.xy).r; - - float rangeDelta = abs(depthVal - sampleDepthVal); - float rangeCheck = smoothstep(0.0, 1.0, g_sample_rad / rangeDelta); - - occlusion += rangeCheck * step(sampleDepthVal, depth); - } - - occlusion = 1.0 - occlusion / float(SAMPLE_COUNT); - occlusion = clamp(pow(occlusion, g_intensity), 0.0, 1.0); - gl_FragColor = vec4(vec3(occlusion), 1.0); -}*/ - -// This is a HBAO-Shader for OpenGL, based upon nvidias directX implementation -// supplied in their SampleSDK available from nvidia.com -// The slides describing the implementation is available at +// Based on NVidia HBAO implementation in D3D11 // http://www.nvidia.co.uk/object/siggraph-2008-HBAO.html varying vec2 varTexcoord; @@ -127,8 +36,6 @@ uniform float bufferHeight; const float PI = 3.14159265; const vec2 FocalLen = vec2(1.0, 1.0); -//const vec2 UVToViewA = vec2(1.0, 1.0); -//const vec2 UVToViewB = vec2(1.0, 1.0); const vec2 LinMAD = vec2(0.1-10.0, 0.1+10.0) / (2.0*0.1*10.0); @@ -141,13 +48,12 @@ const float R = 0.3; const float R2 = 0.3*0.3; const float NegInvR2 = - 1.0 / (0.3*0.3); const float TanBias = tan(30.0 * PI / 180.0); -const float MaxRadiusPixels = 100.0; +const float MaxRadiusPixels = 50.0; const int NumDirections = 6; const int NumSamples = 4; -float ViewSpaceZFromDepth(float d) -{ +float ViewSpaceZFromDepth(float d){ // [0,1] -> [-1,1] clip space d = d * 2.0 - 1.0; @@ -155,80 +61,62 @@ float ViewSpaceZFromDepth(float d) return -1.0 / (LinMAD.x * d + LinMAD.y); } -vec3 UVToViewSpace(vec2 uv, float z) -{ +vec3 UVToViewSpace(vec2 uv, float z){ //uv = UVToViewA * uv + UVToViewB; return vec3(uv * z, z); } -vec3 GetViewPos(vec2 uv) -{ +vec3 GetViewPos(vec2 uv){ float z = ViewSpaceZFromDepth(texture2D(depthTexture, uv).r); return UVToViewSpace(uv, z); } -vec3 GetViewPosPoint(ivec2 uv) -{ +vec3 GetViewPosPoint(ivec2 uv){ vec2 coord = vec2(gl_FragCoord.xy) + uv; //float z = texelFetch(texture0, coord, 0).r; float z = texture2D(depthTexture, uv).r; return UVToViewSpace(uv, z); } -float TanToSin(float x) -{ +float TanToSin(float x){ return x * inversesqrt(x*x + 1.0); } -float InvLength(vec2 V) -{ +float InvLength(vec2 V){ return inversesqrt(dot(V,V)); } -float Tangent(vec3 V) -{ +float Tangent(vec3 V){ return V.z * InvLength(V.xy); } -float BiasedTangent(vec3 V) -{ +float BiasedTangent(vec3 V){ return V.z * InvLength(V.xy) + TanBias; } -float Tangent(vec3 P, vec3 S) -{ +float Tangent(vec3 P, vec3 S){ return -(P.z - S.z) * InvLength(S.xy - P.xy); } -float Length2(vec3 V) -{ +float Length2(vec3 V){ return dot(V,V); } -vec3 MinDiff(vec3 P, vec3 Pr, vec3 Pl) -{ +vec3 MinDiff(vec3 P, vec3 Pr, vec3 Pl){ vec3 V1 = Pr - P; vec3 V2 = P - Pl; return (Length2(V1) < Length2(V2)) ? V1 : V2; } -vec2 SnapUVOffset(vec2 uv) -{ +vec2 SnapUVOffset(vec2 uv){ return round(uv * AORes) * InvAORes; } -float Falloff(float d2) -{ +float Falloff(float d2){ return d2 * NegInvR2 + 1.0f; } -float HorizonOcclusion( vec2 deltaUV, - vec3 P, - vec3 dPdu, - vec3 dPdv, - float randstep, - float numSamples) -{ +float HorizonOcclusion( vec2 deltaUV, vec3 P, vec3 dPdu, vec3 dPdv, float randstep, float numSamples){ float ao = 0; // Offset the first coord with some noise @@ -247,8 +135,7 @@ float HorizonOcclusion( vec2 deltaUV, vec3 S; // Sample to find the maximum angle - for(float s = 1; s <= numSamples; ++s) - { + for(float s = 1; s <= numSamples; ++s){ uv += deltaUV; S = GetViewPos(uv); tanS = Tangent(P, S); @@ -265,18 +152,14 @@ float HorizonOcclusion( vec2 deltaUV, sinH = sinS; } } - return ao; } -vec2 RotateDirections(vec2 Dir, vec2 CosSin) -{ - return vec2(Dir.x*CosSin.x - Dir.y*CosSin.y, - Dir.x*CosSin.y + Dir.y*CosSin.x); +vec2 RotateDirections(vec2 Dir, vec2 CosSin){ + return vec2(Dir.x*CosSin.x - Dir.y*CosSin.y, Dir.x*CosSin.y + Dir.y*CosSin.x); } -void ComputeSteps(inout vec2 stepSizeUv, inout float numSteps, float rayRadiusPix, float rand) -{ +void ComputeSteps(inout vec2 stepSizeUv, inout float numSteps, float rayRadiusPix, float rand){ // Avoid oversampling if numSteps is greater than the kernel radius in pixels numSteps = min(NumSamples, rayRadiusPix); @@ -297,28 +180,27 @@ void ComputeSteps(inout vec2 stepSizeUv, inout float numSteps, float rayRadiusPi stepSizeUv = stepSizePix * InvAORes; } -float getRandom(vec2 uv) { +float getRandom(vec2 uv){ return fract(sin(dot(uv.xy ,vec2(12.9898,78.233))) * 43758.5453); } -void main(void) -{ +void main(void){ float numDirections = NumDirections; vec3 P, Pr, Pl, Pt, Pb; - P = GetViewPos(varTexcoord); + P = GetViewPos(varTexcoord); // Sample neighboring pixels - Pr = GetViewPos(varTexcoord + vec2( InvAORes.x, 0)); - Pl = GetViewPos(varTexcoord + vec2(-InvAORes.x, 0)); - Pt = GetViewPos(varTexcoord + vec2( 0, InvAORes.y)); - Pb = GetViewPos(varTexcoord + vec2( 0,-InvAORes.y)); + Pr = GetViewPos(varTexcoord + vec2( InvAORes.x, 0)); + Pl = GetViewPos(varTexcoord + vec2(-InvAORes.x, 0)); + Pt = GetViewPos(varTexcoord + vec2( 0, InvAORes.y)); + Pb = GetViewPos(varTexcoord + vec2( 0,-InvAORes.y)); - // Calculate tangent basis vectors using the minimu difference + // Calculate tangent basis vectors using the minimum difference vec3 dPdu = MinDiff(P, Pr, Pl); vec3 dPdv = MinDiff(P, Pt, Pb) * (AORes.y * InvAORes.x); - // Get the random samples from the noise texture + // Get the random samples from the noise function vec3 random = vec3(getRandom(varTexcoord.xy), getRandom(varTexcoord.yx), getRandom(varTexcoord.xx)); // Calculate the projected size of the hemisphere @@ -328,8 +210,7 @@ void main(void) float ao = 1.0; // Make sure the radius of the evaluated hemisphere is more than a pixel - if(rayRadiusPix > 1.0) - { + if(rayRadiusPix > 1.0){ ao = 0.0; float numSteps; vec2 stepSizeUV; @@ -340,8 +221,7 @@ void main(void) float alpha = 2.0 * PI / numDirections; // Calculate the horizon occlusion of each direction - for(float d = 0; d < numDirections; ++d) - { + for(float d = 0; d < numDirections; ++d){ float theta = alpha * d; // Apply noise to the direction @@ -361,6 +241,5 @@ void main(void) ao = 1.0 - ao / numDirections * AOStrength; } - //out_frag0 = vec2(ao, 30.0 * P.z); gl_FragColor = vec4(vec3(ao), 1.0); } \ No newline at end of file diff --git a/libraries/render-utils/src/occlusion_blend.slf b/libraries/render-utils/src/occlusion_blend.slf index 965d806759..cdab624b95 100644 --- a/libraries/render-utils/src/occlusion_blend.slf +++ b/libraries/render-utils/src/occlusion_blend.slf @@ -21,9 +21,6 @@ uniform sampler2D blurredOcclusionTexture; void main(void) { vec4 occlusionColor = texture2D(blurredOcclusionTexture, varTexcoord); - if(occlusionColor.r > 0.8 && occlusionColor.r <= 1.0) { - gl_FragColor = vec4(vec3(0.0), 0.0); - } else { - gl_FragColor = vec4(vec3(occlusionColor.r), 1.0); - } + gl_FragColor = vec4(vec3(0.0), occlusionColor.r); + } From dc34c025bd1b6413ed5306f4d4f5cff4ce0f686b Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 29 Jul 2015 18:54:05 -0700 Subject: [PATCH 173/242] whitespace --- libraries/animation/src/Rig.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 2eb9d0e0b3..70f0a59a21 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -67,7 +67,7 @@ public: virtual ~Rig() {} - RigPointer getRigPointer() { return shared_from_this(); } + RigPointer getRigPointer() { return shared_from_this(); } AnimationHandlePointer createAnimationHandle(); void removeAnimationHandle(const AnimationHandlePointer& handle); From f943dc8492ed768977e7cbd96e9700b6f2e82650 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 29 Jul 2015 19:34:05 -0700 Subject: [PATCH 174/242] Use single file robot model as fallback for full avatar models Fixes having only a skeleton with no head if an invalid full model file is configured. --- .../resources/meshes/defaultAvatar_full.fst | 64 ++++++++++++++++++ .../defaultAvatar_full/defaultAvatar_full.fbx | Bin 0 -> 977504 bytes .../defaultAvatar_full/textures/visor.png | Bin 0 -> 4665 bytes interface/src/avatar/Avatar.cpp | 8 ++- interface/src/avatar/Avatar.h | 1 + interface/src/avatar/MyAvatar.h | 2 +- 6 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 interface/resources/meshes/defaultAvatar_full.fst create mode 100644 interface/resources/meshes/defaultAvatar_full/defaultAvatar_full.fbx create mode 100644 interface/resources/meshes/defaultAvatar_full/textures/visor.png diff --git a/interface/resources/meshes/defaultAvatar_full.fst b/interface/resources/meshes/defaultAvatar_full.fst new file mode 100644 index 0000000000..eba175e771 --- /dev/null +++ b/interface/resources/meshes/defaultAvatar_full.fst @@ -0,0 +1,64 @@ +name = defaultAvatar_full +type = body+head +scale = 1 +filename = defaultAvatar_full/defaultAvatar_full.fbx +texdir = defaultAvatar_full/textures +joint = jointNeck = Head +joint = jointLeftHand = LeftHand +joint = jointRoot = Hips +joint = jointHead = HeadTop_End +joint = jointRightHand = RightHand +joint = jointLean = Spine +freeJoint = LeftArm +freeJoint = LeftForeArm +freeJoint = RightArm +freeJoint = RightForeArm +jointIndex = LeftHand = 35 +jointIndex = Reye = 3 +jointIndex = Hips = 10 +jointIndex = LeftHandIndex1 = 36 +jointIndex = LeftHandIndex2 = 37 +jointIndex = LeftHandIndex3 = 38 +jointIndex = LeftHandIndex4 = 39 +jointIndex = LeftShoulder = 32 +jointIndex = RightLeg = 12 +jointIndex = Grp_blendshapes = 0 +jointIndex = Leye = 4 +jointIndex = headphone = 8 +jointIndex = RightForeArm = 26 +jointIndex = Spine = 21 +jointIndex = LeftFoot = 18 +jointIndex = RightToeBase = 14 +jointIndex = face = 1 +jointIndex = LeftToe_End = 20 +jointIndex = Spine1 = 22 +jointIndex = body = 9 +jointIndex = Spine2 = 23 +jointIndex = RightUpLeg = 11 +jointIndex = top1 = 7 +jointIndex = Neck = 40 +jointIndex = HeadTop_End = 42 +jointIndex = RightShoulder = 24 +jointIndex = RightArm = 25 +jointIndex = Head = 41 +jointIndex = LeftLeg = 17 +jointIndex = LeftForeArm = 34 +jointIndex = hair = 6 +jointIndex = RightHand = 27 +jointIndex = LeftToeBase = 19 +jointIndex = LeftUpLeg = 16 +jointIndex = mouth = 2 +jointIndex = RightFoot = 13 +jointIndex = LeftArm = 33 +jointIndex = shield = 5 +jointIndex = RightHandIndex1 = 28 +jointIndex = RightHandIndex2 = 29 +jointIndex = RightToe_End = 15 +jointIndex = RightHandIndex3 = 30 +jointIndex = RightHandIndex4 = 31 +ry = 0 +rz = 0 +tx = 0 +ty = 0 +tz = 0 +rx = 0 diff --git a/interface/resources/meshes/defaultAvatar_full/defaultAvatar_full.fbx b/interface/resources/meshes/defaultAvatar_full/defaultAvatar_full.fbx new file mode 100644 index 0000000000000000000000000000000000000000..71bc127c414abee528ef53ac122a377c0bf0a135 GIT binary patch literal 977504 zcmd3O2{=^k8}~?|lqE?eOIlP4MV7HlB9gUiiHyM*Q<%xj*qMr=6zxiw79omCA~8s4 zrAR2tAO@q1eVFaLjyd&C|I(}f|GU2Hd#>x%%sJ=&-RpDT_w$@*UJFEs7a{>>{eFzq54sbmRK!ZclH~@*5 zzKixl+Tjs?*jhen;UBPq5J)%Qjk(T9M1Xm_Fa*NlyE@Mblz;R}3)2>f#gaHsg^HiJ(nkB5>H4VQtbjfzq9=RN6ssAA&|Kc2xJXUPQd0i z0caH3AK^Rw#Tes<#Sr|xcqRnw*b8d5gGaG~k0RU%i}gi&PGMHYlI(i1~fo^D?|u_2qM9FNU}2^fm!JfV8A5)1cF)d zz<{ay-7w)M2s{$VcyKc-`)OUiU*J)m2m1Rs+x!w`fAuwV>NHRMpkhj|u%0 z8fhz(3gRdw7sQsO>zcl`}!jE-6$&6W zg@C{@_u4A~fj~CrZ=yX`eD&r*Rf%f$CMKN-J_l&L@TZDB9i-}8wry6|-n>=!N036* z)zx2uL|XjSZ_BKzpP~l=?dxT*Reg)5`WAH^RUr9+LyTAc?2vdo+8>2ugG_!ZWmn-61zA2I+j zLycMjfk12nFkS>tWPqYAG607~dg9T6NN*4hOxQq`1tTcVs|lH-cnfZ~`u}^G9H9{7nN8GcyFNv1}1fWsQGSo?bj) z5&m{~L;!x;cox%_fW$rh0id(o>-H!B_^=NI*!qtr{sF*XXQY?7-2|JgY$5#y<@$)f@4zA+&n z0O^nCBL!O4-=;Axm&*n8|NZa;TmX*w-`h-_i`YNwsCmX1ffRtrF{CG!1Y zOGrm51mX^X@FE0=;pz2>3Lo-MsklMxCldgnV}~IGcp`0h%LZVMV*Xh{OvB2H^Gu+} zGl3K^jiy7_$RF)zhevoG`a>a~j%UOnq@ubykevP*=Hfgxf!r*z7lXlDW4w@-|JpbawR|@~o0kbdAaW8s z83C0S@GLaFwgU>?&qYxJd=fzBbJ|ZsaPiD&fGMY2@M(Lx{T^*xk_P|;p#-?P3b9A} zVSRxK0Ql0>Blw3?`hds4@ly=6N$~us!t>|!S`3Nv3_wrs8O)Hr0HBqUJVLNG0`tcb z{z6C_!%VB&7vk+mHqio46 zyljaGK?azB@$erplLrF)tCy!i(t)}Hj-R|-v6$zqjXZNtZ?7QHC?EWP7GFD{*D?YN zp7Zu$kZu)^Zn_aC;CZ#k1A_tAusC1x6q2*AMj|cd!5eA-KaA>F+gDu?4hTx`d}7*c$=qcP|D%Jy(r9 zJqdmU-|5ePf^Y;O!0HFI{?mQYZ0WE5ECP@Ke~7&!1dG7ord4sU#Q39t|9Hg!5dO#f zndQ)3$_rkg1LRM5K$8RHXCpj50PR7*BZ2kH_w7nB-vJS@38V*BG{0sjAk&z71lt`# z`Xccde+6Lu1&*IW_mcGgRj>VKbtn4pq;yDNOM&N!pD%JXdVd$Jj)Gy0EY zGtTAfe<1rq+G+eF*^EMpDa@=JV&;Tn0+OxgB@Q6A&Ha6l0chSTdWMD~9Hin)Q(zta zbBw)L;Ki63j~mcQfpq^vh^TE(iT$ zhYy%uOwP(R>Hi8dD~-PXE6l8v(p<|k48Kx5!~B&}VgCv{tL*g6gu}WAhS}ZdL3`SA1pP1I+hsBAtU_v?c?bl^*?v~ zxgZr-f`M>#!1CJAKhmix^Nh#88R&xv`rV=c*d@V(kArV@fU_UgNE8BiD`txf@B~g6 z`5>@JI|3Gq3Bb>UQuH$lu)FsCFwpniDsz7?WbmKg($GMZ)jYog+oh%fNMz`YYwMNk zdAjluSlXEEzy$wJPGAWWy#A+>0_@^hdipBz7pvc014w_`qUBo*{a6*%fY^L*Isxn- zl?vD%`su=~P?a|DBsS&Q1nA!Yknc6>KfnKi`MV0`v4LkcaJvcU(j3t^vNVONE#&l<1U>Q66YfV?0bOYr(L?tjXYQUU)l(t0^xuF60n_Kv+me|3H~1%WlI&F zfd7d&yDs^6F?~{n7b)QTB4EqFHURAhoT>fCUt=USO>x1Cj#0?yq~jK<)rG z*9p^y9&LRv{-_1OyDo726iq#wc%}u9ivVE>*ahv6n%--k;T0et&NQyizm=1_;9+i*ODo6j(@r9%s4~V-djNj{hnocc}AZ0#{!^ z#`*`+GlWmJnZgnRNm1vS9$Xv(3r`a?4(p5XL;{DU|NZUf_X+>&haONozCBoeXg=}& z#dKy{crpS86|kK?rNwIw`QiQ-NiT2V84{eEz&qDp8O&bZ9#CGs)MJN5dJ=pQ0e|rG z%yWI+R-Sl#spmT$pHMUTm<~gO^x!LHz{hqzXn!=Y3BEHJi}43KU!Vh>2|{!G)2%!g zfaMlQ{(F($zQB<(jQ<~$nmIbypitnePGCO#gE%X-zGMH(plI_dAdutu`zT=J+#VT> zCj=nfz)Htg0D&V`^&lm19Ou^p2vh)5k6_?TEeKNuEaJfNQ~jf8@Epjuj}4Ll$9o)b zzdd;e04Zql2*JV$WO9Q}>p;U0Gf&V9TX>Fc& z!Mp$*$^Cu+k~dj@97XwEKmi`J0TIBS8kp1$j&?}isqgQ<`T=Ga?+;*U@FzRRK}x=j zat9ppdz<{bpaNi?fK+@F1)xG>fp+6Jfe2=Z*^Fm8yxav?fj@TvrT)heK4U^aAQCup zu@-!qFO6M*Kp^?xWnib`*E9x9AMhC851>q7hXfoyC9-QeyzL#}r*;Ci)AM7dUwO=O zNa*r3{N0hqpEVQ&pXAfd{y&-6gU1};TX&lvy)glP z$bf`jae$rspT;!2ZnA;$ z<`Ca%)Z7n&LfRn@6M)mJGezYEkPd7p0iXOt2Oe0N@%S;Ie?5o+HY&7yLjgo|EOjjNCuYl=>S9E zeSTWX0-0ssUZnSrUl#)Q`34g5{mNweK?|mKKuSKR*!m(u?663r z*R;xi?7$(An?G{~j0DC4VCCPP#Q#1OK7o|{p$HuE0u~rRD=}@k-y8ywFyQIRmx_V? z(Y*wJf8a-(Go)1*NX54yY>C0ln1%7+<9r(nMxLHX9PUq&{Ws%Y7|<>9hA{77H%{G< zcfcO7%%YzTJ6cM-bAUh)5Xf|&4H5Y1=5z)C4g~^NR~zzj43I141I?VNF9L;|e&qY9 z$M0;XN7~Fh&vx1KPWA)6{hxRy{w2?4uo?9%8*-Q(Pf^(4>3Mo)o=3`NsVDI-c>+Ho z0#e3r{JDLCFgra*{*ottW}fac7iY&4wf*mG+dDH)^{x2X@%;RkJY9`uwCy|bS?XzH z^mlr`o0%te=PY?v{3TD59W&~AR&JI&wRilTo`o~>RJWTY&+NbCxyg7&Jt?#0sbKtf zdS0KICsy>*?Be#qU-DdHGNYdKfm!ARVUxepGj?X4WxBKUXV_ozWX;S|LS>dYe(*1O zVt3AL+j+C(>ACaoZ2M(qp6>Kn@@)J|p8HH^)bohhEd6O=`geN1nVIK($Sir5{w2@t zW;5!ErOtA$Xqf$-o{whc`EKqk_00H7p6hqbsOLM*EcKM#^>=zE&&>1sl3D7R@RvL# z%xBb-E;37=g64mxXY|ZG-34aJGw3gQGG^v^c6^pOf%ca?eRt1nTd7&<3E%y9wx!I> zbNg)lS^t+j?JZ{1Q;{;uKHkjY@AQ1}2cCMfwC&Tsm2qahl9}$%7m_I@Y?AzqmXYsZ!dx=q+uq|}gxh$VTdm{;n zJ)ztC9BtFW1OzthO%sXrU4P2`*2ze$;7eaXiI^n3M$3e&JHWeqQxsUpk(IulK3nvyqH(GfzA5XQtbycXU&6CoA(^pP+8+_wgP38HriWe9 z9)zaML<>#zd&CXfMw?SqLymH5Xi1JenG0P{b2cUP#SMBKyy9q6OgnksL8d*nF`b=x z$e~_HhCF7{^sscC`T-%OndJQP(ZEn!s)+0p+C4nfx8dxeE4kzPHqjBpfD7y@>p&%8 z?D*T|Hqi$TaNLFbKHbYbrvN>lVnkYaxXkl&nfP7K2AgGWIkn%mqh2m$7;&FO)1h*z z%Uep{%SWE=bCTe+9)ARV-c9$QM@{TR(k*$xfG#tUV+R_qCaK6z!=~v#GwbwC>~bqO;A?qiboe&*dkdh0Y6M47`sjw zH1Cet+>Tpb*n9rUr%~j$#17#=eZc_)60<5koNB@_dY-bH6H~#d3Wv2Wg55t(5+_=`jU^cyt467-)`D9iZGWgqc1tW^D3OX zbvzbVKoQO#6~SK05N3q#QDdws#6{6J?>rLMfO%KcG^B#%P@}Tny&Tr-Hs&lZt2`a5 zyih)Tk6pr!~VlBfc z5ZZI{z6!VKnsLm%D_iGAQeqNAl?GnhNxvkm*0wWb*@^4aCe52H6DCC|9NT1YMEJH0 z*HxP(&twpWYcJ-Cl;WpOJb-U|y&##e4i>vLuZ|`mBd07$_FT*G80jD`A{q3G5?_sm zxR)-QG!~M3W~7p(QQq$xO--8T`XTrXL#=vFqv$m+t?+zmsAzq^vGWUYi=0)&(Yuf7 zFbttVs)HKY{7w69;WMsmxZ|mhGTHQntd+%dl$~6pD$cg}SXTjOF85?Ztkd-kMEgTr zg(w<{CsnyR`KBw|SdIkJd8Zhj-8A-t2fCv1mhW9tHxt5;4E))cLVkQil|! zs1jbVsJ(rXRuAY)Y2>0xxI%Ehb>3fuQhPBY~9L)Jv zI=6VkA21avUBsDoO5@o`yYWp67{PtMBZ}rJdk!{$et6-)*GDasoAbI?Wg9k1Az+1H z_QAi|^>6$+{r8!)Vi4dDCI~6F9 z<2HD~I*;Cd=#T`w)Y0i#t#*q$tIA~*lGCUrq*~A(;f|FKTRPP3U^7SN0LkK(adIvF zNcQ2zqs)1Dp{Te5AGod5OH6>J&GQte8=>789r4I_?uM+~S616gJjCPYB$jyAu8L3e zlnin{D2NRlmG8_9klWFMtaGJsUYu!PZm423wnm2`hBcVFb4Vz0=VHmAA%P)>35_k# z=MI$^iJ-{-L-Z&GjXkb=;nn0@;;Hj;N(M((kcnei>i51~lRWH&u9t?SGL^!yyc zHe}4Fcb^bgGU0cTy7htduu<|&_I{y9HhpeZlJF9&Ol8E!f>e=e$q=octyAS~&ary6 zPg|rA@M2xLLIy#zQ&s3RBtJb{fT|~1FekD8Ng~^whN~U)Uk)2dB$~JeTMg&6B~BEVH{_rw z#L3GxQ|;jw9bB!1ca$I8rD@+sdAW* z(&&>w&8zijzgSYYr^U=KZd)OT0vR_lMCK|#BSWt-@4iRC9TZ`2T>DSn&o+E@WU4RG zFfM%LHAl>KiNUEtQPQnc#&I-hLBN?Ph3C#ES@SusY`WT#q6%rpJnQUmEn~xy4PzH7VbtdZgv>I(bh2! z`35(Da5En{ZGk^=hvDtX?FFm9-5T*A7kx&RvCo9rtk7Riik>`mRrnnwVqwnML~^y)p&t2^jX-Gy}KGB{qaLFc4xEI;VB(jLj`LT zN!?W%S9167pv6mPUEH0AHP9k&pVY@`Fb;M;7WF0%n;JiNl1~wG9d$c8Rud>!9zN#T z4oR$3P;|Q)wy@Hvo|Bn={}}Xxq}2w}STIzX9@sFvtR>-E%;c+$urQ^8JdEM?l!c+1 zX^b{vz0|5rqoxd_ggenAQOgKL+2zP*q~(t$izlR_;kg|r?+?nL1-LZIVT!NFSJ90@ zJ*S~8SJ(BTt~vXW`%~q5(ny+cPTe^}S;wWc9lD&mgk_CG zV@^*t0fjJM(gFZEl9|;kq6ic~X%= z-W_^nl6V+ZyP73w{)L#WRvaTu$fP#fe7!oz#3Vk`jd|NLuca^Up^PV<@uVwTK`2yd zz-+Skl@H~vbK_?368&d`TGaTLq)c+nfm0{B9m=1jI^tmd=ehR`2c6Z9r}(=%tiqVd z_0Nlh+C<+=ns7cQ_W63qlvsL0wac2g>4om60?bj59pYkZ1T$z;k;*kXNdM`&4+xiVU@PYWTBAifh`wSto+P^;Je8Wpk zv2l{CWiZM7ofC|FI-&)}-GJquIxtw#UwL(qRv<#{sx^k^4+tCTJrxPRL=_&c{qXgj zcv6H=vf4vdO?%ZTTQh_F(~Nmv4I4asO(`lOs0%OTx#F)$Y!5rRNHqrf2`YFBU;VZ! zatp=a89d1{nC!HdCV`Tp&XK`5b3YtPc}dA?Sw`VJ$-7@qjf&;&*655g(9G?wzLD$Q zEvzb&vYK@(dyCSC_lGnD#n-toQL48X@|&%uc4h>h$nb85z+3pa5&euz~X)I4Zb3iObQR7|^XZN)Mp~l+rb$YM&an2^`9#Jc?4tZKP zmC?VjH|~K-S>org&=WFu?+M8fO)SIwrd*^95T9J&J=^ZNUWT-gR_WKHL}jU4?ioIi z44e{w62lqP5WVWie!MbRy?K~zzmk#YSeiL%Ny6xg%NhD<3{6OUd;NCaTPm~~v&>06 zEqI8k`h10c)A%y%xiiC8H@`8PfMqAdh+WftqoEH&EU(e!oYQ%T+feDWjXS^b@YIwJ zWvkTJu1{Q%@Jsers@Rizoo?N73l*Yj8-A6c+_i?*QuI9~4(%WMWK79XomahaHO9Fi zR|Lbnf(nkz!_gB8$-;%3-izqC6p4&T$0#pOJ;Ug%Z$8-*21TsFIz}^C7e;q3ta<9Y zhO=P!TI>b^HsxI~g|m}Zt9$82#R8e@BUOu@M#jJvxt7>2FC>yrqVUe;`=4{ELU_hu zT7lG{^cE5=RQw1H_uMH+Oi(=arJ&qlavmzUV{BVrZs8n5i?Nm1_gM~-BTpEdMXm~c z!}g@gjCMKet-&$p%9^7DQakR5pK9Amd+k(l^fl8f!A&-1DoyJ*VSQO*W@!Xo81pjc&^noVo&G%dMMinr-PDc;INiD{ z9}A`izW6oeHii*bL>NwK;#n)Vq;{OF?CL8D<_c1#>Mlgrs+Td`cj_QxxQ1E_FMxc?I|JiqvHeT~oBXzB(*3#ynqE;B2 z8E_1+4v!?TB_EYJ*DpUO*-8{{w10HO{RTJ4Ua%$#8XW}R^N!O0QR6wxs!M2aqDGuW z?SJw4x{(u|{aSYMLe)`?vYqu)WtDlykG@`enOM3WYk)H~EZ@OC7j#2SLAizc#8y0? z3~_CE1eN(HNZtHRpi#!h)gV6*(W{SxaK7lB2u@d%fAhkBzndonJ3fT19c2sLk(9}Q ztcZ1RUxJO$Rqj9UKQ6<{tu2}7YVXRu_fq9O>(D7tqFiB%>$VcH!{eQy6pr#^vG5Ul z)hH%xP)IeP{U+@(?#X-=lW&rO7RZjOVL$stjIZ7 z$q9quqYVd4hTht*EQHoeawZbGcgTs>*((=&mMa}Q&Rt=b;+3mCW(j+tc{3IrvO|pi zB*O5uwwsk^VLMuDl+Z&??V}0kURSf}nNu@0A)K>d$*QnUF;6!S8yLmI8rGCsO%`<( zyA@%3=AiWm@~vLKty9&$?c=f);w0y5k2RX_^aKu>G@)({ z%6vrJQpNT%W7fAH6Tm1hbH%I|;J%)KkD4@zx2;IEh`C2f89u7EF2$WoUnzvj>s@&p zHu&Y_eU)QRGhv;250&W)1QL;LHE9*K&f(UKw+fa+9uJabT@ynVj>oPXN)=&l@|@GG zQL<#91Brt;31X-pI;uOLsGY-1d0A72druCOVVuJ_Ya-o`&;1+0|cO9l}2LQ@}oX@hwOy@+}Ix?aN#&abcy! z?G#g%b6V}<6E#P2;cqDOM<%nzC~=w( z%Gf6xCCr?^E$S79OPdiA(Uos^v^M7~8O|h7IN$E3!G1U+*A>-I`nV36DrChBT2&Tu5;b#t?L{~r<~a$77u6N& z7l@Oyrk3=fLV-qNrmsKA=c^*qx0?xN=q+8n91`?tCYqZn;6og;e+po5&N~keI7iWIl&TK{=Hv zWrQqAx;Xc?68-~Dc)i;e7qr{_?k#HD&fxduJf`PeX%`JYdF3EvX{R$UBer1-)bqzO;VQnH?+_nR*w7iB^6_ zKHW zM<;`GU!6Za=i zO>{O&$i$5tIH2DpqwRmOw#YDYJ|~D(6=FvpTVI4ppJ>KaCUh?*tk2MMC^yVeTPh4U zNWCPR98n-v2#?Q^**CRQCni>C`2)4rkk`vzVLpBZWt5bUiJySjzWA`!@ zwWQ;?3`IXT?u{_XDR@yZHo~w*ne{bD@9N;ZOTFPL!z5NK!>VAVa3sB6gx)O7 zBI70+D(?lACEn3kOlFxqtnPpDDVZJ{A6klCAh&JUGih*dQM^R90mE#`GC@~-`bY-1 zahGc&W1Ej#C~;%m@{$!J3q01xm+uNCZ+IAul5J{clVjOrqtiL3&gQl6_Bd@2Clt7rAtz1KaJK~eT8fVA?-?t42J3vQS~hl0 z`A&|T2R*|@?crg^bmwJiuf@}CIeO$57E@cd9e|6<8B^O*E@F(1-+d?EZ)nsgSsADu z?RMu)8GCX5R*|J2vzsn0TBw>=KY5h9SC+ccaozmFyqLr=jqDDM3|v+LYHCjkg|mZV zZRVHKY@2s>;_Y5a=w9!E*PPP@yNEApqp2Iil{S1isrFg>1A@l5xtA&|)ROKy|Q;KmG&vf~fGaH$q=$3>;=A(tMhnICE2KAd8}ic9u;qjOiTJHd_F`n3q* z6sdW08@+u2P|Hi0z4|OuG5Wk)BU+{Q8!k<{Dx_0C+zj0!&L%w+L=h!&#tVd0mrCZ` zBd`B-j&W&MdvgWKPX6Kbc>^6|Pd!`-M8&?-I)sl+cUHW|&E1nyBVGRBP;g}R<;wcv zA~9k3+AlD#;WG*8)KBDwAr*_{YzJfJ${R|3B2UisztOLo?|s%_W5(ze|dx6z&W$b)Y#~weT(4Ea=Iz~X|Bc-2iOnCCo8>& zOZ&$cGs@>YG*i7b;$n+YZ!?y9^=V{9L&0@Nvru@B?%UVIYyI~^$5-UwqNLMS$JZ00 zcTI)m4JAB^9M4W0Hfa(+L5^5H`7W9@fO$Z8+K()*&e?6_mu;NNpw8*;NZD#u-fq8K zhuGF8Os*52tl(S~Zz(k&6sL>FpftN;H*pt}s|_;_Jrbv`b-1qIj}5M9*3c)8Usp;; z*88?{@{3SYog?KtEoF<%8(WJ7hY0Tl%Nq2?BP|-!Q#*znU=W#YuZ-Znp-T&2vGlP7 zs{D9FNPMTt?oEOtnwGT{=VGi&nt)!CoFRqufk67VqOCGuan5>Z-TPhU(UqrrSwFEnh@- zrEfg*l+pMo={VxfJ1pa}P+3fI&%y!OYI zYPM=SvUe{|^)$1Ndd-kb<8J7Faxi#JOnLL(y2RraVM+r+Ht8qCvbQV_f4@Z=J2{mc zEEidm!=95|vO7e2jq3Au?K?-ML`p3t;us%-@0tx^q$pbzTB|*&F?BdM>p>?5l4=Zp zLku?9a8X>#%|ET;a>{MCMWt7pNO89+g`;voUovv##oH4Lj$!)Pi`bfzW|&Q8w4x6} z^aBx>4cepIzpQ7+(!QIGSM&~rn!Y~82!@Mw2wlRV44Vrz0D{h2=y^?!tlz#re0SRl~%f&DGIj} zp9qN_OPETjp_WUz#LIGH8()$JR6dp3vp*HeBM2|J2#q+4#fXJ&A;Wh&bM-NxNz?UH}$mxq=bd?hF}jAlA_EItJhe%tL7@Ss zG+?i{3od`~>(#+HPxcLkv}-31Pj1YD=_^O23?yjJZ`YCtC-yn__LADsxfO%7LAXFp zfBdesmN2*>Mk9~=?g$^VfVpM}tFZ*hrB`Z{%b)LlCn<_EhG#sS#HlG@CuxXb7r$qd z8Yj~(TwEG{NkhIX>wvL(j?PnIopy8Tz6S;KdtQ6LFa99GEM|Pu&aCY$;5;XJlhl+y zu7Tmjj>ILWRgfoF5)(6sa-y)2iQW=XtpL?;84pyO*T<{XF);@n);_n`mb8OKs()rp zrXx>bKI@e^yA^%2^~<}S^Xdr(BBLUDG`IF0vu=vDf|BF*Amj3NLx!VS%R!sS`_NbY z^c=apy^J%RH*U$;Luy_ryji_(pEBo#)wsUP+~Bj+hPhEA!e@>!N%nJcN}UB=t<$OZ z9tImwI_nI2=Z-}lX`i!`nfQWm{m6U7sbSmFOCpA(*8BI%8}tsp6FT_`;gT zwxjU}FUb_{Zb@#Z3{+k!V;}P>b-OZGF4bgkRWHjcLG@FTEID{$>k#fNyWC0RXpFYE zPfL(X*okU;y{258)+&}(gRtY>xjos!Uj%X*chMahmWRBZ_;94*U1Gt#ns?O*@u!ob z&Fuv$3}087Hx}JRyT|buQP)QF&hfhpvBKVF;lkaCa*-D*V{21;ZJ_2MFT`)xmCH|> zz$rH;vyL4*KEme6J{;-lFJp_J@^n4itBN&zu(3nh+r5R-;VFk_7_^qi)7>qYCX(YJ z8<`VJs$~Ub*rI1s?9z>^iCJr~rR~Pg>RAh2N0lNBii{-7pM^f{TSIrR_oUFeQm(6| z+LsY94=<<{3+e|4Dr1+vE6i#Xe)qmWgqnd?us29Vko9vm4NN3FI&PTlc5VP}GU$?J ztp*XltR0=w-|w?Rn=FUrR3uWeZ3e>9ZpG%VBPr`~gT=$gc4O+6GdJM9TDDNOF8V6^ zaE&lLHS)$KZ-m=vTT0x(=Wmp7X??TclTXUnNe?oWIj>a9TG=b#ZxoxDpJJ_kvR}IN zsT`E0RyU;fZe(6+h)8QYGX?wP!>|p?95p~0BPp|kpK?F8A1P8Mt|CXi{j_}L8qTTZ zIvFp!S`iug&09>Pp;Kk3vdMdmM`-huKPn$D4330iC*;#Zi#(wf;+M~feNI{wes#>E zm*HsEaGRDO1-clBfIkP$`2a8shlQZYm3t&N;mhdh z97d$OVf~cFMoF^nE!326jYDtm$@&|sKb>Poe)Xs1mFJhCG+rN7tIjVkvLnzSGTlCI z0g z!_;#!jR5w0ubJdwSB;jQ%?Rrzs2cu#XQ zLx|QlnaKFEfs(cN@CO&eL+TAjou1B-p^@jxhYzp79>`mQP4(V58g)58wlt}K4Q)!J z|3v)PImr&|O`wa?>&bMdd6Z$}6^Ytu(3c7y=gKGFI(&6PzfUt`BB@FLAv~YF^RRAG zw9Y}M+l3^<)p70!mRa4TBV0E2a6?+ekkABbk#Zz!*E5UalVd7lP)c~$GRjTcRUZ^S z<}SuQIjE1-+U>WE=A=4wM3lRkQa-t7U+K+C_G2i^wOww3t6Z3FR9{@5w7K$-G({=h zG?3C*dvs8FkDT`zQ>vy8X*ba(E&U>dwCL1JPb~3f_vccL^C~gjS~_r?0JPsy%qE)c z{;FXm)VH}n?QBX!-yqV9YZ-KY$G+m^6*@HQF}usb`uD^RZ@WIH`RYkd&M9r8cg)q? zPe}I1YO*%Lb3JQmvder!lEt80pS?pbXA+tv>)TS1^DG=zYw&z6Ltn2OwJIy@?w+=R z9^5!>KjopnYVGLQ)P|6EWDBamkh(xH7QcD;BWX|kaGIF_h1LJ`g$?)3K}sm3`^M?k zSc2r6FLIiqUE@In`q-yjspEYKcjoEepFlTPbLUJfXO~35@1`ePg96FY_jXT8c@EyHl8_%L zPO7JOcdol{b~=Og1!e8wO1{9UiH-0c8Go~gK343oED>cT8gYPVV|c48!B4swAFLQ8_3V zOGIwVu{%bzunW(7A0Cx4?(SMGp0zjP!ezBK(y7ChtBOdQ;QGW5r?cCx+AvCJk5mKP zp5ZJV6~(caNFP@-*PZKms&aHd!JXi?>={0@B0&CF`)FaXO>`?|G%+^UEl;$t{R-=@ zmbkH;L}LMjI&S!cd|g6qyn0S2ad+dj@>FH1NAE>@*&*)>vz4D2e%`+=S4>vdU6oN@ zemPZyS=_XX-t7Jmt|cet8j|>u^Z9dGqUd@ep1nhtz%clDU8JWQwoNSDty`jJt=4YJ z!;jl3zMaD#l+s2*Caza>Zix4AIi6BQ8tBRXqCG(_pTJ3)alVR{O)h&yqSOXmzxD8a z#Qp1H4aSkdSib*(ZucgT~&Z8U8k-)SZzZWtTmxoWW4P-c{W`}Q!TUNvh)B%j;K(%Csup$!f#Y$*`(NT zX@5;&dBYfP$%I3tf#f(_vQ)Ybq0+Nt6~{p~(PH!UTv3iOOQ<$j>B|sHxh(PXi%2q! z@J!9|1iKg^+xz14l3YsfvL>lF7SD@W^B;3u_eF)=@motFDJmM0woH(R5)8+nm0Q|< zdh?PLgoMNwLj{~*rv+7C+f+n;h0}H&eiWj*0=dI*WVoO#F~Tn#r<^8vBVN@lnUPm# z95WUy+y#xif3{O)CAR@}y<+1h1{o{Ym32e#2$TDctR^3ZT{xbb41s0d#VhDA)~IHP zRXebI&JsVJkLOkkKe92LSE8|D6JKr~B=?N6>Yxx?m*Jqte z%Zi}|@#okc?>+7YIUZa?|1zcS539ix!72}YOB>mzp4=~}Vm^Vcpk96y34;noq@;V{ zWw2&OBoTo*Czn%Dj zIm@_uEq(M$%6n{9eycjuS>9g#f)g8G(OOvajdBXQ(kz`QHvU4Oy|biweNLNPcw18! z_OdFB(a9}KvBpNFiZb$dOAueK$}yH(JF<$~9G`ep*ualHc^lHdrd|6KqsVPE;AM8@stW|j5yQ8$Y;QDGNK}a6ox1C9@Y@RcmP|@A9Q?tEjXBjHP+t|$V zQ_J?4fXLi^lwxksRg)&0s>I@?#ijb9;ZKml*s6#1A^C3HC52hGN|(GlIFrs6bp=r0 z`&E+~%L}@o$LmkRW1b#v4fE-Qq$so7g7RXqEneYYyv5mD?dahx9j`h& zhY`sxE5G5tT(9jQ#t3h#-$9jRMdhxqT@5{2$$n92UETb0<9jBiSg&#?d+*k&rq^s%k*m}oy1XzVVou%G7DdN$Xko@vj~h|XTkrIc)Ci+2o&)^!*Q-!!D>MaPr| zEtE^~#;%a-&kT&FeHz#8dh=*hKFnoBBk^cXdvnKWl)8T-+VAZI>n=X!rV16QG%(V> zK&k!M=`n+P6zZ|GB;uCirxCY2ltDepVX5?%&BS_gbptXDH5J--usGql#e1ffN396O zqdYXp2^v~!2EQ3$^j0;VN#aS#E#) zm{Q_pNY1_Zta_aikr1(na1(YFA3h?S{lfZ5mRgBIz+){eH!GhQKq!iDvprA05^NJ)G;E^Vq!y`S z#@M~WsZ~|nHQ#4}Yss!ap{KO%W~HIIXSCXL^_rv**_@a0I5ZX}-YM-Hwx}?;Kzcy( zZ68~L=;BEUo#^m|vQjgBheq*mr%m*8)Xt*~CvO$K`W(KCIH(a@wMeUZqJTy|5kB&? zE^I})+0Glak4a~wZjc8hYhoS?s8&mi7t}DfSO$9V>2|7zEa2D>}7tTjid_-1!lcZnwsvA1=JD4p#DYAnEBiVRupkOIvj) zk$sAeF{IbYqe0~UGpHl4n_-le*FA1WBhQxTVDtUvVzK81qPAM~viFVe_B)-e z_L+@;CUQDECKpnZ(9G1Rs8WJO#KoN&H%zD`9A8X{8=tm)9OFE{wL21Nv&r&J=Y11O3xSm@m@fhvtF!#J z1<1Y%$L$)kdxHH!8i1zx3?;($jUZtmGJ$)(5UydQ*%%tn;wiTopWvU4=IzxP$>w_C z3EO9iZ`EH5I+rZn=HelGlL!b8}UWlExzyrb%jp1MH=#Sd&c&X z{RSLxpTDjcfezg@OoXh75&?c*cF#hKwBItmBOSUX9Gpz4j}>-L+-+o`cN?0bmreOu z2E-`-lKnt{)`46nOh9@!=;HJD>fz#mpvE=tj^|h0$!y5_^pL{l?!7`QgwfXqNG-Iy z=MDGlg8spVu|3~cD%T?FJjEqH{yoG(K3m3IZ9FUZFnz}_+C1v{C+ANE8D>r{W<>OB zz_}H(kwr^gH}`Dq#KQ4fr>H|O7n^4jd8rT9t|DI*IrkX}bEE*Cqn3ZEYh-VQ3F#8k zm8Ws5DP<2rLhY%2-5CmJ?&2x(SI}*)@fnx$W%5Yka?n^#lG-7mEA)5tlivVpQ19NL zWW<{5zoNIJUjG^8EjwkKBw0}%#<7Ue``py5#=Hx?*vJ^D$=SBHWuy5n@b`mijp|+N z6+s`p0hqZ<-#>G*|5y<#rAzElR{G2@d@j;?t!<|6Se+syrB*SvW%@PkIS6ko3=ceQ zb{ot0SD()tU2kS`H=*rNz|J~^2Wv8=#&1_pZpmxfyh8O&nSIuyT~K$_mp2oZ0!$Pg z7@cH8*O>d?o6ZH5KZ~J>S8ymW)P~yL?NF)e_cwn+qp~UV2SGNsZlbx|c(y7c8S?=m zbC0dkmBA?SYw>du&~(w!+Peg@1$NQ+VQOsrxrL*b=6Nf7*2+Q+=9)3b1yvD?`5SB5 z>zxu&rtCexPs31IM=z|?XWip`TL!Tb)^yxyW4QlZQfEynI2KG)tcA3gBf zf5@R>OI)-iG5J~DT}!g65;pF`e*NI2us*aR{tPe1i&qe_9h#&#`^}>=r2ns3qUq82 zuo~uiRbraWmP|L-N7F>6++0z-W+*H3G~@4NNAsiX*TS%&R(UH_!*=fDN+FHP$s^yZ zoCqiA7TW*=f;|A@Yh6j)BrWfsxAIZN_{se(UVEo7OYDwHx)LQJL8GHyWAy360^{C< zxVs~5_80G{l_JbU_e@O0)M-K9B_pLzy)#)O2~AX_JAws|=G2E(>6`sRiXw@GR!nsRF5Ay~_q(ARa7{9c!`k~0ra3!KMI@&jP*pEF9Zqog* zwdrFW@4_legZk0evJ_Nc)DzP5BaSLQC`p2nIiIlldGsJ}Bd=I^Vs6|l$FA1TT=~Ac zQ?ffSeWFC4cVE4Epd3I^2ULr|V4BA5XlM1N`|1@*6OW+zpJ}u+PIuk-KTuw9~=n z_|h!KQbErynr8io?zP{Y64Ke^q~fYq7L|?#HP4z_P>rCp61e!YEN$UfQQLbn4#F{f zEb-GND5YoEIG9Wr-t#=n+}3b|>=bt-yw9sRk|e^`&I}@B`i6YU7?d!*duZpnk9Pj| zhc912`y_6E+xnX@(G%PT>yMr>IDpXJs6VnID=zk5P~~goioJu&fF=JNMnL@-43k6%PBcU37h~^JB0`kL@)_@5J4tPO0>f=Dm*|(fOy~T!Nc^?-$ zpS^(!@gq&Gn_!9F3Pam~dxsg)CyLqD4OD(xzuGic?DhsBm27ZYlM&&-Kd#(!6Uwn7 z9Halj&8WQ-`@SJR&w@U47YZb3)g6^ZGnV8AX-mFmm&0G>K(y{bad*epocpH^7n-Nf z6rnT^lp~J&UpXroBQNY;1Xw|xIMYIufe(+FzYD9)y(HdyC;M*D10yM&^Ktv zP!*Nc1ZDm~YYGgij(5(}9x@SnZ+Nf1Y#4_H#wEMp=7HLw7fmELZyFReE^dK(@<)6VWMI9Iq`BV*42xV|F5SW|WPLrNvpt7kMa$!d^4_fhq% zp(h`%%cOMw(jG75FKVZoeC9}Q-tL37S8M0tXUSQb>BkL$Iw$R`Sq=0r(6N~~%_KHc z3i~BK;$n^jL`!2+Wygwyb36@p;i3p|M;O#Y*#ezJgt=K?grBF=&c_vZ`s`L%%RTZ6 z&Q3etv0@7r=g$+16ZedqgTd|Ozb38J+zcV0gBbk^vFGw?^PpD-bFU6+hu%~$rY3dp zC;CrbtBC58M6%UJRe(r<6*$CH^yV1t-P2;C`fKVMFuN?+YFd+Ie` z(+;;X;?Bld%l$thOgcw;Zh5;On=!JZJ>s@Lntu4gv1}HU%~stg^7Pg}1U}&G$Go$U zjDA9n=9x(yTqpR`{QBxHDMTe*?v-H|-qbC}a>==JG2Gn}gg>r2eqCY12 z)sbqB$lneik)?c>JGIo>_~lT1)_;5f#CF!3mP%1v*%LEnLr&Pfn6>NH{L@xF??&d%Occ;?P-$Np|0Wy{TL>~TOYHGJ&E&W@QFt3s#hrrj$7de3u=^>s-C~Ukp+;G}2cM#V z=#MFxA5``9~BZPDOhYoyB)ayeqISHgWELgk&i==;q(Ia_^H_9~ad0?PRAaZCE)a|ie!&b;S7=zdW1tFaIn$};=3OP0`e}d zGLvu^eH52-Pctv3W4|f8G`Vp721o%#-lHaOYOj zDja?y)fue6%`8+O6{b#+Fi|jTLTNak(noG5wzH>Fdx83HkpJ~vnkgpWR-;{22Uhzi z%!ijS>q5o*jWdog=FLmFtvO5MAg6RMM_qTXE^MscpJHJ5Ow-YBQ*42sy(?e$hX2zM zIO=w%bc%fV$!1Re)H{G_68i^cedl~)R&7WW$i$CyBYu##Jp^eF%FgOGT88`v zjR=b1?3$fO{^wnTjv>SepO&{dkZIhwk0c8v$4N4?OEBR>3J#zl3DwYC4x@fdnaekW6&dV1qLwx5gn zOLB|)!#o)WLnvpUD4FWl25KxM8*$=JMHd-luba1^+UDQ*G!H)uJ7uc2(rhx1L+LVC z5Wah*!UW@ROLM8e$yib@(B>rH{q98<4B6+3{*Pi-Lw_#aRNbSi-yPNCx`aMR(W@4q zsDA9>)tsrxB=Ea$P3lFciFOy$zT@JRxuOiFueT}m7(;5Xw?QmBCW?uV3G#x0=sAt~ zV<-!1MK@Xbucdj0nEYB0ksvY&0DtHb`yHage)dQN>xn7^4DV6|ao zv&8Ap`wOc%S>toMr<3+>_cLbe_NK3-gUqv{c@KfLNc#JMY7%t7I9 znSu9Q_jl`{1Lt76z5^u<=qmxf2luz|X0BGOhJ-J^NXuV&5ozUeP0wxVT-2N;TDf?@ zZy@{gc`hzrzdD@vV5O2sjRR9rNUPjX(H98Q%ds?)ak^5nsektj_pp;ob$acIo@KIQ zd|P*+N9-XllQ>-7Oj^s(i0v$=WoW8Quxqvx70Ag>uVoIH^C2$M=fslhn_864DFxF( zn_1u<(em9*^X_uw2q?8&iDUem%yj|n1%BMwyQWI~IrV;$x4!w_h4TpA z@S*&b9nqN0Uwg;IQ6oV42%;g&zzX1tzf0OaoCHv0u&*DIS4?3R*O?6s-4Isz(*ad~cN>g%)#(9OgFO0-RCLkY@g=abInuT<+kcNInInz6fM;-`E2+XZhlpPt1Y z3V60$QSqT64iY{FEYeSh8-4U~EY;jJn?Cg$(4d@|O3O%T_XH0%+=gkkwq0qzjRJ1G z174)`0%}b0^dUmR=3Wx#ZzVpySrCQLUH- z+jN_o=Ie4JzU99<2AVAK#I3UnlfbV2I7Tf2@L7-b{OI-hRjV}Oh0K1*r#5)7@aS9y ziSkMR#=O~0Luh$r{IQS=K%twas5M4kllxW1U!gD6e=7SYs>7d|s9JRaN<+==jLw=% z&DWnmEu8c+AH`|tapj)quIk3|=t~lW-Jyahytctv7>PJM_h z?Hc|T=x!y@uYe>|HE}`@P{JzgO?GOeq<@Nv?LDN=6n#;OdsDlg#MbW+(9Qh@HAGdz zcyvrqs}elFto9PSE#@ck5Y1)=&J;X&rvJEwBn+r~))5W$bDYZ#w6iYeFS!1U6K*G5 zfUr-$r&p~m)}NtG=%$0`ZZ4mBGt|)cU(Iy<&qrx5>3uTYUQ^MCx-?+#QAVo%ghh5 zf?yhyzP!3LPGAz4AzM?Gp}1SaQzHaBNyLHM`iIO{|5Ax#Oehl+2g;b9%ruzG^YoZ# zo`xn%Ub{rD5rX4DsZ7tv86wWdj^O@|tk2I5x+l3MzyR&0n28+;@8&AjANW*nD)YGROTU^N{Ke$uf)29w4)!tY&mBCL@A(3e|^*8EhR)@kh&8bqpm zVQx65OxJ0dc!Z&ZMG7A(eukgb`C|P&x9HhOYP{KT3#(G)IAYY!H1~(pPYROtdhAvA9{YVeI`wkShYRQ zSI82qeH;Ni`nDvg4J=58Ep(K|1yd;gSrPdw z1yiMTj`Zt)(_XV??>N=MDnnjvbXFTUF3@C%c-H49ybA10fVf^DlME}Q{hwTJqB%9u zkPPuc$EP?wyqx<~p$MT)JQ^i*w| z$R&c-IO`HYh?!5BB{KC9^k@`HGl>@|BFvp}Kt!A&3X&m~#l_JZ$B_%tyKZsLG#9~% zL~zr1buE5=Ncyk?8A^eDzgko3JxKNl;GolMi49(LD>*~*GbJvp(U$l))W3xu5`5pFeT zK~&W&M|F#O5z(st+Fd1wFO4I#_ZwG3cka!vv)zu((%Z7`kJn0`x)7Zc1KextZc*C? z$Lh)RxC=88a{hDoXnJ z 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Ξ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

YPeEUmT*E^5VB|a@&ID%fJ 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 lzij1Up}lEBvgm9QREsoCWvUfIKs* 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�M`Zf-zuer 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%0A0BX!f;H3zW+h)8^R{> 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

Zm+>=LbR0;@cy)x+v)VjwOmG* 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ⅅ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 z%0D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 l*59AXzf!%!0DzMd5^#-+Y&$& attachmentData) { diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 253c94b0b3..fca6b51922 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -170,6 +170,7 @@ public: Q_INVOKABLE glm::vec3 getAngularVelocity() const { return _angularVelocity; } Q_INVOKABLE glm::vec3 getAngularAcceleration() const { return _angularAcceleration; } + virtual bool getUseFullAvatar() const { return false; } /// Scales a world space position vector relative to the avatar position and scale /// \param vector position to be scaled. Will store the result diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index b18b8df121..a27710bb80 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -129,7 +129,7 @@ public: Q_INVOKABLE void useBodyURL(const QUrl& bodyURL, const QString& modelName = QString()); Q_INVOKABLE void useHeadAndBodyURLs(const QUrl& headURL, const QUrl& bodyURL, const QString& headName = QString(), const QString& bodyName = QString()); - Q_INVOKABLE bool getUseFullAvatar() const { return _useFullAvatar; } + Q_INVOKABLE virtual bool getUseFullAvatar() const { return _useFullAvatar; } Q_INVOKABLE const QUrl& getFullAvatarURLFromPreferences() const { return _fullAvatarURLFromPreferences; } Q_INVOKABLE const QUrl& getHeadURLFromPreferences() const { return _headURLFromPreferences; } Q_INVOKABLE const QUrl& getBodyURLFromPreferences() const { return _skeletonURLFromPreferences; } From 46b48171550455ef465f7d471f529fbe02bf19eb Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 29 Jul 2015 20:08:58 -0700 Subject: [PATCH 175/242] Add DDE option to couple eyelids Use the most "open" value for both. --- interface/src/Application.cpp | 1 + interface/src/Menu.cpp | 2 ++ interface/src/Menu.h | 1 + interface/src/devices/DdeFaceTracker.cpp | 7 +++++++ 4 files changed, 11 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3696d5c9c4..8c8cbafafd 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2004,6 +2004,7 @@ void Application::setActiveFaceTracker() { #ifdef HAVE_DDE bool isUsingDDE = Menu::getInstance()->isOptionChecked(MenuOption::UseCamera); Menu::getInstance()->getActionForOption(MenuOption::BinaryEyelidControl)->setVisible(isUsingDDE); + Menu::getInstance()->getActionForOption(MenuOption::CoupleEyelids)->setVisible(isUsingDDE); Menu::getInstance()->getActionForOption(MenuOption::UseAudioForMouth)->setVisible(isUsingDDE); Menu::getInstance()->getActionForOption(MenuOption::VelocityFilter)->setVisible(isUsingDDE); Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setVisible(isUsingDDE); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 1cbe127857..2d4fafe605 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -421,6 +421,8 @@ Menu::Menu() { faceTrackingMenu->addSeparator(); QAction* binaryEyelidControl = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::BinaryEyelidControl, 0, true); binaryEyelidControl->setVisible(true); // DDE face tracking is on by default + QAction* coupleEyelids = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::CoupleEyelids, 0, true); + coupleEyelids->setVisible(true); // DDE face tracking is on by default QAction* useAudioForMouth = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::UseAudioForMouth, 0, true); useAudioForMouth->setVisible(true); // DDE face tracking is on by default QAction* ddeFiltering = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::VelocityFilter, 0, true); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 62a1ae3b0f..bd6c982d70 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -165,6 +165,7 @@ namespace MenuOption { const QString ControlWithSpeech = "Control With Speech"; const QString CopyAddress = "Copy Address to Clipboard"; const QString CopyPath = "Copy Path to Clipboard"; + const QString CoupleEyelids = "Couple Eyelids"; const QString DebugAmbientOcclusion = "Debug Ambient Occlusion"; const QString DecreaseAvatarSize = "Decrease Avatar Size"; const QString DeleteBookmark = "Delete Bookmark..."; diff --git a/interface/src/devices/DdeFaceTracker.cpp b/interface/src/devices/DdeFaceTracker.cpp index c9170bd413..396539c3cf 100644 --- a/interface/src/devices/DdeFaceTracker.cpp +++ b/interface/src/devices/DdeFaceTracker.cpp @@ -564,6 +564,13 @@ void DdeFaceTracker::decodePacket(const QByteArray& buffer) { eyeCoefficients[1] = _filteredEyeBlinks[1]; } + // Couple eyelid values if configured - use the most "open" value for both + if (Menu::getInstance()->isOptionChecked(MenuOption::CoupleEyelids)) { + float eyeCoefficient = std::min(eyeCoefficients[0], eyeCoefficients[1]); + eyeCoefficients[0] = eyeCoefficient; + eyeCoefficients[1] = eyeCoefficient; + } + // Use EyeBlink values to control both EyeBlink and EyeOpen if (eyeCoefficients[0] > 0) { _coefficients[_leftBlinkIndex] = eyeCoefficients[0]; From a9556660c4ee2995eb5f96039c7489a0bf276d8b Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 29 Jul 2015 20:53:24 -0700 Subject: [PATCH 176/242] fix linux build --- libraries/entities/src/ParticleEffectEntityItem.cpp | 6 +++--- libraries/gpu/src/gpu/Batch.cpp | 2 ++ libraries/gpu/src/gpu/GLBackendShader.cpp | 3 +-- libraries/gpu/src/gpu/GLBackendState.cpp | 10 +++++----- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index 4dfc9dd436..dc5bbb85ed 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -155,9 +155,9 @@ void ParticleEffectEntityItem::computeAndUpdateDimensions() { float yMin = std::min(yApex, yEnd); // times 2 because dimensions are diameters not radii. - glm::vec3 dims(2.0f * std::max(fabs(xMin), fabs(xMax)), - 2.0f * std::max(fabs(yMin), fabs(yMax)), - 2.0f * std::max(fabs(zMin), fabs(zMax))); + glm::vec3 dims(2.0f * std::max(fabsf(xMin), fabsf(xMax)), + 2.0f * std::max(fabsf(yMin), fabsf(yMax)), + 2.0f * std::max(fabsf(zMin), fabsf(zMax))); EntityItem::setDimensions(dims); } diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index a10f5b3de7..0347536808 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -8,6 +8,8 @@ // 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 "Batch.h" #if defined(NSIGHT_FOUND) diff --git a/libraries/gpu/src/gpu/GLBackendShader.cpp b/libraries/gpu/src/gpu/GLBackendShader.cpp index dccd035cb4..5fa0e277e5 100755 --- a/libraries/gpu/src/gpu/GLBackendShader.cpp +++ b/libraries/gpu/src/gpu/GLBackendShader.cpp @@ -639,14 +639,13 @@ int makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& slotBindin GLchar name[NAME_LENGTH]; GLint length = 0; GLint size = 0; - GLenum type = 0; GLint binding = -1; glGetActiveUniformBlockiv(glprogram, i, GL_UNIFORM_BLOCK_NAME_LENGTH, &length); glGetActiveUniformBlockName(glprogram, i, NAME_LENGTH, &length, name); glGetActiveUniformBlockiv(glprogram, i, GL_UNIFORM_BLOCK_BINDING, &binding); glGetActiveUniformBlockiv(glprogram, i, GL_UNIFORM_BLOCK_DATA_SIZE, &size); - + GLuint blockIndex = glGetUniformBlockIndex(glprogram, name); // CHeck if there is a requested binding for this block diff --git a/libraries/gpu/src/gpu/GLBackendState.cpp b/libraries/gpu/src/gpu/GLBackendState.cpp index e9dcd3aad3..22c61b2365 100644 --- a/libraries/gpu/src/gpu/GLBackendState.cpp +++ b/libraries/gpu/src/gpu/GLBackendState.cpp @@ -482,15 +482,15 @@ void GLBackend::syncPipelineStateCache() { State::Data state; glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); - - // Point size is always on - glHint(GL_POINT_SMOOTH_HINT, GL_NICEST); - glEnable(GL_PROGRAM_POINT_SIZE_EXT); + + // Point size is always on + glHint(GL_POINT_SMOOTH_HINT, GL_NICEST); + glEnable(GL_PROGRAM_POINT_SIZE_EXT); glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); getCurrentGLState(state); State::Signature signature = State::evalSignature(state); - + _pipeline._stateCache = state; _pipeline._stateSignatureCache = signature; } From fa6d6a51230de1c91b8d2bf0ff29ddc5fe757603 Mon Sep 17 00:00:00 2001 From: Marcel Verhagen Date: Thu, 30 Jul 2015 15:08:02 +0200 Subject: [PATCH 177/242] Added the & references to the const variables --- libraries/fbx/src/FBXReader.cpp | 8 ++++---- libraries/fbx/src/FBXReader.h | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 69482a8c81..8b83acdca4 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1455,7 +1455,7 @@ void buildModelMesh(ExtractedMesh& extracted) { } #endif // USE_MODEL_MESH -QByteArray fileOnUrl(const QByteArray filenameString,const QString url) { +QByteArray fileOnUrl(const QByteArray& filenameString, const QString& url) { QString path = QFileInfo(url).path(); QByteArray filename = filenameString; QFileInfo checkFile(path + "/" + filename.replace('\\', '/')); @@ -1469,7 +1469,7 @@ QByteArray fileOnUrl(const QByteArray filenameString,const QString url) { return filename; } -FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, QString url, bool loadLightmaps, float lightmapLevel) { +FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { QHash meshes; QHash modelIDsToNames; QHash meshIDsToMeshIndices; @@ -2722,12 +2722,12 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping, return geometry; } -FBXGeometry readFBX(const QByteArray& model, const QVariantHash& mapping, const QString url, bool loadLightmaps, float lightmapLevel) { +FBXGeometry readFBX(const QByteArray& model, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { QBuffer buffer(const_cast(&model)); buffer.open(QIODevice::ReadOnly); return readFBX(&buffer, mapping, url, loadLightmaps, lightmapLevel); } -FBXGeometry readFBX(QIODevice* device, const QVariantHash& mapping, const QString url, bool loadLightmaps, float lightmapLevel) { +FBXGeometry readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { return extractFBXGeometry(parseFBX(device), mapping, url, loadLightmaps, lightmapLevel); } diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index cf86c304ac..776b78b3bb 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -268,10 +268,10 @@ Q_DECLARE_METATYPE(FBXGeometry) /// Reads FBX geometry from the supplied model and mapping data. /// \exception QString if an error occurs in parsing -FBXGeometry readFBX(const QByteArray& model, const QVariantHash& mapping, const QString url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); +FBXGeometry readFBX(const QByteArray& model, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); /// Reads FBX geometry from the supplied model and mapping data. /// \exception QString if an error occurs in parsing -FBXGeometry readFBX(QIODevice* device, const QVariantHash& mapping, const QString url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); +FBXGeometry readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); #endif // hifi_FBXReader_h From 4bb415fd0d6ffe8b5e936298bfa0d9359549ed67 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 30 Jul 2015 09:29:59 -0700 Subject: [PATCH 178/242] Fix for torso twist. Off by one error, 0 is a valid parent bone index. --- libraries/animation/src/Rig.cpp | 25 ++++++++++++++++++++++--- libraries/animation/src/Rig.h | 2 ++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 2ff6faa868..15f3dd65ec 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -16,6 +16,25 @@ #include "AnimationLogging.h" #include "Rig.h" +void Rig::HeadParameters::dump() const { + qCDebug(animation, "HeadParameters ="); + qCDebug(animation, " leanSideways = %0.5f", leanSideways); + qCDebug(animation, " leanForward = %0.5f", leanForward); + qCDebug(animation, " torsoTwist = %0.5f", torsoTwist); + glm::vec3 axis = glm::axis(localHeadOrientation); + float theta = glm::angle(localHeadOrientation); + qCDebug(animation, " localHeadOrientation axis = (%.5f, %.5f, %.5f), theta = %0.5f", axis.x, axis.y, axis.z, theta); + axis = glm::axis(worldHeadOrientation); + theta = glm::angle(worldHeadOrientation); + qCDebug(animation, " worldHeadOrientation axis = (%.5f, %.5f, %.5f), theta = %0.5f", axis.x, axis.y, axis.z, theta); + qCDebug(animation, " eyeLookAt = (%.5f, %.5f, %.5f)", eyeLookAt.x, eyeLookAt.y, eyeLookAt.z); + qCDebug(animation, " eyeSaccade = (%.5f, %.5f, %.5f)", eyeSaccade.x, eyeSaccade.y, eyeSaccade.z); + qCDebug(animation, " leanJointIndex = %.d", leanJointIndex); + qCDebug(animation, " neckJointIndex = %.d", neckJointIndex); + qCDebug(animation, " leftEyeJointIndex = %.d", leftEyeJointIndex); + qCDebug(animation, " rightEyeJointIndex = %.d", rightEyeJointIndex); +} + void insertSorted(QList& handles, const AnimationHandlePointer& handle) { for (QList::iterator it = handles.begin(); it != handles.end(); it++) { if (handle->getPriority() > (*it)->getPriority()) { @@ -674,7 +693,7 @@ void Rig::updateFromHeadParameters(const HeadParameters& params) { } void Rig::updateLeanJoint(int index, float leanSideways, float leanForward, float torsoTwist) { - if (index > 0 && _jointStates[index].getParentIndex() > 0) { + if (index >= 0 && _jointStates[index].getParentIndex() >= 0) { auto& parentState = _jointStates[_jointStates[index].getParentIndex()]; // get the rotation axes in joint space and use them to adjust the rotation @@ -691,7 +710,7 @@ void Rig::updateLeanJoint(int index, float leanSideways, float leanForward, floa } void Rig::updateNeckJoint(int index, const glm::quat& localHeadOrientation, float leanSideways, float leanForward, float torsoTwist) { - if (index > 0 && _jointStates[index].getParentIndex() > 0) { + if (index >= 0 && _jointStates[index].getParentIndex() >= 0) { auto& parentState = _jointStates[_jointStates[index].getParentIndex()]; auto joint = _jointStates[index].getFBXJoint(); @@ -712,7 +731,7 @@ void Rig::updateNeckJoint(int index, const glm::quat& localHeadOrientation, floa } void Rig::updateEyeJoint(int index, const glm::quat& worldHeadOrientation, const glm::vec3& lookAt, const glm::vec3& saccade) { - if ( index > 0 && _jointStates[index].getParentIndex() > 0) { + if (index >= 0 && _jointStates[index].getParentIndex() >= 0) { auto& parentState = _jointStates[_jointStates[index].getParentIndex()]; auto joint = _jointStates[index].getFBXJoint(); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 70f0a59a21..2af61bfded 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -63,6 +63,8 @@ public: int neckJointIndex = -1; int leftEyeJointIndex = -1; int rightEyeJointIndex = -1; + + void dump() const; }; virtual ~Rig() {} From fc80e427b9ceef1f61acf4f6005ef1a688091f94 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 30 Jul 2015 11:08:45 -0700 Subject: [PATCH 179/242] remove names from audio-mixer jitter calc comments --- assignment-client/src/audio/AudioMixer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index b68332210b..04cbb0b325 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -927,9 +927,9 @@ void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) { const QString USE_STDEV_FOR_DESIRED_CALC_JSON_KEY = "use_stdev_for_desired_calc"; _streamSettings._useStDevForJitterCalc = audioBufferGroupObject[USE_STDEV_FOR_DESIRED_CALC_JSON_KEY].toBool(); if (_streamSettings._useStDevForJitterCalc) { - qDebug() << "Using Philip's stdev method for jitter calc if dynamic jitter buffers enabled"; + qDebug() << "Using stdev method for jitter calc if dynamic jitter buffers enabled"; } else { - qDebug() << "Using Fred's max-gap method for jitter calc if dynamic jitter buffers enabled"; + qDebug() << "Using max-gap method for jitter calc if dynamic jitter buffers enabled"; } const QString WINDOW_STARVE_THRESHOLD_JSON_KEY = "window_starve_threshold"; From 314434e0689098d567f76e2cb2c92b586e9c5dc3 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 30 Jul 2015 12:27:28 -0700 Subject: [PATCH 180/242] Re-added neck and eye procedural animation to FaceModel. This is only used in the case of split head/body avatars. --- interface/src/avatar/FaceModel.cpp | 61 +++++++++++++++++++++++++++++- interface/src/avatar/FaceModel.h | 4 ++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/FaceModel.cpp b/interface/src/avatar/FaceModel.cpp index d5d4da8665..f909489dc9 100644 --- a/interface/src/avatar/FaceModel.cpp +++ b/interface/src/avatar/FaceModel.cpp @@ -25,6 +25,7 @@ FaceModel::FaceModel(Head* owningHead, RigPointer rig) : void FaceModel::simulate(float deltaTime, bool fullUpdate) { updateGeometry(); + Avatar* owningAvatar = static_cast(_owningHead->_owningAvatar); glm::vec3 neckPosition; if (!owningAvatar->getSkeletonModel().getNeckPosition(neckPosition)) { @@ -37,19 +38,75 @@ void FaceModel::simulate(float deltaTime, bool fullUpdate) { } setRotation(neckParentRotation); setScale(glm::vec3(1.0f, 1.0f, 1.0f) * _owningHead->getScale()); - + setPupilDilation(_owningHead->getPupilDilation()); setBlendshapeCoefficients(_owningHead->getBlendshapeCoefficients()); - + // FIXME - this is very expensive, we shouldn't do it if we don't have to //invalidCalculatedMeshBoxes(); if (isActive()) { setOffset(-_geometry->getFBXGeometry().neckPivot); + + for (int i = 0; i < _rig->getJointStateCount(); i++) { + updateJointState(i); + } + Model::simulateInternal(deltaTime); } } +void FaceModel::maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, int index) { + // get the rotation axes in joint space and use them to adjust the rotation + glm::mat3 axes = glm::mat3_cast(glm::quat()); + glm::mat3 inverse = glm::mat3(glm::inverse(parentState.getTransform() * + glm::translate(_rig->getJointDefaultTranslationInConstrainedFrame(index)) * + joint.preTransform * glm::mat4_cast(joint.preRotation))); + glm::vec3 pitchYawRoll = safeEulerAngles(_owningHead->getFinalOrientationInLocalFrame()); + glm::vec3 lean = glm::radians(glm::vec3(_owningHead->getFinalLeanForward(), + _owningHead->getTorsoTwist(), + _owningHead->getFinalLeanSideways())); + pitchYawRoll -= lean; + _rig->setJointRotationInConstrainedFrame(index, + glm::angleAxis(-pitchYawRoll.z, glm::normalize(inverse * axes[2])) + * glm::angleAxis(pitchYawRoll.y, glm::normalize(inverse * axes[1])) + * glm::angleAxis(-pitchYawRoll.x, glm::normalize(inverse * axes[0])) + * joint.rotation, DEFAULT_PRIORITY); +} + +void FaceModel::maybeUpdateEyeRotation(Model* model, const JointState& parentState, const FBXJoint& joint, int index) { + // likewise with the eye joints + // NOTE: at the moment we do the math in the world-frame, hence the inverse transform is more complex than usual. + glm::mat4 inverse = glm::inverse(glm::mat4_cast(model->getRotation()) * parentState.getTransform() * + glm::translate(_rig->getJointDefaultTranslationInConstrainedFrame(index)) * + joint.preTransform * glm::mat4_cast(joint.preRotation * joint.rotation)); + glm::vec3 front = glm::vec3(inverse * glm::vec4(_owningHead->getFinalOrientationInWorldFrame() * IDENTITY_FRONT, 0.0f)); + glm::vec3 lookAtDelta = _owningHead->getCorrectedLookAtPosition() - model->getTranslation(); + glm::vec3 lookAt = glm::vec3(inverse * glm::vec4(lookAtDelta + glm::length(lookAtDelta) * _owningHead->getSaccade(), 1.0f)); + glm::quat between = rotationBetween(front, lookAt); + const float MAX_ANGLE = 30.0f * RADIANS_PER_DEGREE; + _rig->setJointRotationInConstrainedFrame(index, glm::angleAxis(glm::clamp(glm::angle(between), + -MAX_ANGLE, MAX_ANGLE), glm::axis(between)) * + joint.rotation, DEFAULT_PRIORITY); +} + +void FaceModel::updateJointState(int index) { + const JointState& state = _rig->getJointState(index); + const FBXJoint& joint = state.getFBXJoint(); + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + + // guard against out-of-bounds access to _jointStates + if (joint.parentIndex != -1 && joint.parentIndex >= 0 && joint.parentIndex < _rig->getJointStateCount()) { + const JointState& parentState = _rig->getJointState(joint.parentIndex); + if (index == geometry.neckJointIndex) { + maybeUpdateNeckRotation(parentState, joint, index); + + } else if (index == geometry.leftEyeJointIndex || index == geometry.rightEyeJointIndex) { + maybeUpdateEyeRotation(this, parentState, joint, index); + } + } +} + bool FaceModel::getEyePositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const { if (!isActive()) { return false; diff --git a/interface/src/avatar/FaceModel.h b/interface/src/avatar/FaceModel.h index 5a19a8ea29..40f502f2d3 100644 --- a/interface/src/avatar/FaceModel.h +++ b/interface/src/avatar/FaceModel.h @@ -26,6 +26,10 @@ public: virtual void simulate(float deltaTime, bool fullUpdate = true); + void maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, int index); + void maybeUpdateEyeRotation(Model* model, const JointState& parentState, const FBXJoint& joint, int index); + void updateJointState(int index); + /// Retrieve the positions of up to two eye meshes. /// \return whether or not both eye meshes were found bool getEyePositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const; From 85f06639a5a67117be3c7ff644f4ffc771d97123 Mon Sep 17 00:00:00 2001 From: Niels Nesse Date: Thu, 30 Jul 2015 12:50:57 -0700 Subject: [PATCH 181/242] Remove redundant code in getOrCreateChildElementAt() --- libraries/octree/src/OctreeElement.cpp | 45 +------------------------- 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/libraries/octree/src/OctreeElement.cpp b/libraries/octree/src/OctreeElement.cpp index e11a11fc8e..d166b886b8 100644 --- a/libraries/octree/src/OctreeElement.cpp +++ b/libraries/octree/src/OctreeElement.cpp @@ -640,50 +640,7 @@ OctreeElement* OctreeElement::getOrCreateChildElementAt(float x, float y, float // otherwise, we need to find which of our children we should recurse glm::vec3 ourCenter = _cube.calcCenter(); - int childIndex = CHILD_UNKNOWN; - // left half - if (x > ourCenter.x) { - if (y > ourCenter.y) { - // top left - if (z > ourCenter.z) { - // top left far - childIndex = CHILD_TOP_LEFT_FAR; - } else { - // top left near - childIndex = CHILD_TOP_LEFT_NEAR; - } - } else { - // bottom left - if (z > ourCenter.z) { - // bottom left far - childIndex = CHILD_BOTTOM_LEFT_FAR; - } else { - // bottom left near - childIndex = CHILD_BOTTOM_LEFT_NEAR; - } - } - } else { - // right half - if (y > ourCenter.y) { - // top right - if (z > ourCenter.z) { - // top right far - childIndex = CHILD_TOP_RIGHT_FAR; - } else { - // top right near - childIndex = CHILD_TOP_RIGHT_NEAR; - } - } else { - // bottom right - if (z > ourCenter.z) { - // bottom right far - childIndex = CHILD_BOTTOM_RIGHT_FAR; - } else { - // bottom right near - childIndex = CHILD_BOTTOM_RIGHT_NEAR; - } - } - } + int childIndex = getMyChildContainingPoint(glm::vec3(x, y, z)); // Now, check if we have a child at that location child = getChildAtIndex(childIndex); From 7a10b31dd94f999aa1ca571a6886c0e2c5beed22 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 30 Jul 2015 15:04:27 -0700 Subject: [PATCH 182/242] Clear translation on root joint. Also, delete/rename all instances of updateJointState except for the one in Rig and derived classes. --- interface/src/avatar/FaceModel.cpp | 4 ++-- interface/src/avatar/FaceModel.h | 2 +- interface/src/avatar/SkeletonModel.cpp | 33 -------------------------- interface/src/avatar/SkeletonModel.h | 13 ---------- libraries/animation/src/AvatarRig.cpp | 1 + libraries/render-utils/src/Model.cpp | 8 +------ libraries/render-utils/src/Model.h | 3 --- 7 files changed, 5 insertions(+), 59 deletions(-) diff --git a/interface/src/avatar/FaceModel.cpp b/interface/src/avatar/FaceModel.cpp index f909489dc9..066144a425 100644 --- a/interface/src/avatar/FaceModel.cpp +++ b/interface/src/avatar/FaceModel.cpp @@ -49,7 +49,7 @@ void FaceModel::simulate(float deltaTime, bool fullUpdate) { setOffset(-_geometry->getFBXGeometry().neckPivot); for (int i = 0; i < _rig->getJointStateCount(); i++) { - updateJointState(i); + maybeUpdateNeckAndEyeRotation(i); } Model::simulateInternal(deltaTime); @@ -90,7 +90,7 @@ void FaceModel::maybeUpdateEyeRotation(Model* model, const JointState& parentSta joint.rotation, DEFAULT_PRIORITY); } -void FaceModel::updateJointState(int index) { +void FaceModel::maybeUpdateNeckAndEyeRotation(int index) { const JointState& state = _rig->getJointState(index); const FBXJoint& joint = state.getFBXJoint(); const FBXGeometry& geometry = _geometry->getFBXGeometry(); diff --git a/interface/src/avatar/FaceModel.h b/interface/src/avatar/FaceModel.h index 40f502f2d3..54d685c73c 100644 --- a/interface/src/avatar/FaceModel.h +++ b/interface/src/avatar/FaceModel.h @@ -28,7 +28,7 @@ public: void maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, int index); void maybeUpdateEyeRotation(Model* model, const JointState& parentState, const FBXJoint& joint, int index); - void updateJointState(int index); + void maybeUpdateNeckAndEyeRotation(int index); /// Retrieve the positions of up to two eye meshes. /// \return whether or not both eye meshes were found diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 3830360c0e..1b3298c75d 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -83,29 +83,6 @@ void SkeletonModel::initJointStates(QVector states) { const float PALM_PRIORITY = DEFAULT_PRIORITY; const float LEAN_PRIORITY = DEFAULT_PRIORITY; - -void SkeletonModel::updateClusterMatrices() { - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - glm::mat4 modelToWorld = glm::mat4_cast(_rotation); - for (int i = 0; i < _meshStates.size(); i++) { - MeshState& state = _meshStates[i]; - const FBXMesh& mesh = geometry.meshes.at(i); - if (_showTrueJointTransforms) { - for (int j = 0; j < mesh.clusters.size(); j++) { - const FBXCluster& cluster = mesh.clusters.at(j); - state.clusterMatrices[j] = - modelToWorld * _rig->getJointTransform(cluster.jointIndex) * cluster.inverseBindMatrix; - } - } else { - for (int j = 0; j < mesh.clusters.size(); j++) { - const FBXCluster& cluster = mesh.clusters.at(j); - state.clusterMatrices[j] = - modelToWorld * _rig->getJointVisibleTransform(cluster.jointIndex) * cluster.inverseBindMatrix; - } - } - } -} - void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { _rig->computeMotionAnimationState(deltaTime, _owningAvatar->getPosition(), _owningAvatar->getVelocity(), _owningAvatar->getOrientation()); Model::updateRig(deltaTime, parentTransform); @@ -264,16 +241,6 @@ void SkeletonModel::applyPalmData(int jointIndex, PalmData& palm) { } } -void SkeletonModel::updateJointState(int index) { - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - - _rig->updateJointState(index, parentTransform); - - if (index == _geometry->getFBXGeometry().rootJointIndex) { - _rig->clearJointTransformTranslation(index); - } -} void SkeletonModel::renderJointConstraints(gpu::Batch& batch, int jointIndex) { if (jointIndex == -1 || jointIndex >= _rig->getJointStateCount()) { return; diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index a089c7b6ef..ecc5c80118 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -117,19 +117,6 @@ protected: void applyHandPosition(int jointIndex, const glm::vec3& position); void applyPalmData(int jointIndex, PalmData& palm); - - /// Updates the state of the joint at the specified index. - virtual void updateJointState(int index); - - void maybeUpdateLeanRotation(const JointState& parentState, int index); - void maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, int index); - void maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, int index); - - void updateClusterMatrices(); - void cauterizeHead(); - void initHeadBones(); - void invalidateHeadBones(); - private: void renderJointConstraints(gpu::Batch& batch, int jointIndex); diff --git a/libraries/animation/src/AvatarRig.cpp b/libraries/animation/src/AvatarRig.cpp index 3b48b0814f..919ea43e7d 100644 --- a/libraries/animation/src/AvatarRig.cpp +++ b/libraries/animation/src/AvatarRig.cpp @@ -23,6 +23,7 @@ void AvatarRig::updateJointState(int index, glm::mat4 parentTransform) { int parentIndex = joint.parentIndex; if (parentIndex == -1) { state.computeTransform(parentTransform); + clearJointTransformTranslation(index); } else { // guard against out-of-bounds access to _jointStates if (joint.parentIndex >= 0 && joint.parentIndex < _jointStates.size()) { diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index ec423f7091..72bc818b70 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1375,7 +1375,7 @@ void Model::simulateInternal(float deltaTime) { } } } - + // post the blender if we're not currently waiting for one to finish if (geometry.hasBlendedMeshes() && _blendshapeCoefficients != _blendedBlendshapeCoefficients) { _blendedBlendshapeCoefficients = _blendshapeCoefficients; @@ -1383,12 +1383,6 @@ void Model::simulateInternal(float deltaTime) { } } -void Model::updateJointState(int index) { - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - _rig->updateJointState(index, parentTransform); -} - bool Model::setJointPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, bool useRotation, int lastFreeIndex, bool allIntermediatesFree, const glm::vec3& alignment, float priority) { const FBXGeometry& geometry = _geometry->getFBXGeometry(); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 449cd42a25..09504c7501 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -276,9 +276,6 @@ protected: void simulateInternal(float deltaTime); virtual void updateRig(float deltaTime, glm::mat4 parentTransform); - /// Updates the state of the joint at the specified index. - virtual void updateJointState(int index); - /// \param jointIndex index of joint in model structure /// \param position position of joint in model-frame /// \param rotation rotation of joint in model-frame From 14f4c9c6c04b462f177828717803f52256902887 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Thu, 30 Jul 2015 15:07:36 -0700 Subject: [PATCH 183/242] REmove more of the unnecessary GLBacken .h and GPUCOnfig.h include, The gpu::Context is now completely agnostic of the True Backend --- interface/src/Application.cpp | 5 ++-- interface/src/GLCanvas.h | 1 - libraries/gpu/src/gpu/Context.cpp | 17 ++++++----- libraries/gpu/src/gpu/Context.h | 28 +++++++++++++++---- libraries/gpu/src/gpu/GLBackend.cpp | 18 ++++++++---- libraries/gpu/src/gpu/GLBackend.h | 10 +++++-- .../render-utils/src/FramebufferCache.cpp | 1 - libraries/render/src/render/DrawStatus.cpp | 13 ++++----- libraries/render/src/render/DrawTask.cpp | 2 -- tests/render-utils/src/main.cpp | 4 +-- 10 files changed, 63 insertions(+), 36 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1c91f9282c..047596e40a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -772,8 +772,9 @@ void Application::initializeGL() { } #endif - // Where the gpuContext is created and where the TRUE Backend is created and assigned - _gpuContext = std::make_shared(new gpu::GLBackend()); + // Where the gpuContext is initialized and where the TRUE Backend is created and assigned + gpu::Context::init(); + _gpuContext = std::make_shared(); initDisplay(); qCDebug(interfaceapp, "Initialized Display."); diff --git a/interface/src/GLCanvas.h b/interface/src/GLCanvas.h index 925061edd0..6c53a17e04 100644 --- a/interface/src/GLCanvas.h +++ b/interface/src/GLCanvas.h @@ -13,7 +13,6 @@ #define hifi_GLCanvas_h #include -#include #include #include diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index 6730be33bb..239c460c77 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -10,13 +10,16 @@ // #include "Context.h" -// this include should disappear! as soon as the gpu::Context is in place -#include "GLBackend.h" - using namespace gpu; -Context::Context(Backend* backend) : - _backend(backend) { +Context::CreateBackend Context::_createBackendCallback = nullptr; +Context::MakeProgram Context::_makeProgramCallback = nullptr; +std::once_flag Context::_initialized; + +Context::Context() { + if (_createBackendCallback) { + _backend.reset(_createBackendCallback()); + } } Context::Context(const Context& context) { @@ -26,8 +29,8 @@ Context::~Context() { } bool Context::makeProgram(Shader& shader, const Shader::BindingSet& bindings) { - if (shader.isProgram()) { - return GLBackend::makeProgram(shader, bindings); + if (shader.isProgram() && _makeProgramCallback) { + return _makeProgramCallback(shader, bindings); } return false; } diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index ab7a1d1c11..7158bd1a6d 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -12,6 +12,7 @@ #define hifi_gpu_Context_h #include +#include #include "Batch.h" @@ -26,13 +27,12 @@ namespace gpu { class Backend { public: - virtual~ Backend() {}; + virtual void render(Batch& batch) = 0; virtual void syncCache() = 0; virtual void downloadFramebuffer(const FramebufferPointer& srcFramebuffer, const Vec4i& region, QImage& destImage) = 0; - class TransformObject { public: Mat4 _model; @@ -118,7 +118,21 @@ protected: class Context { public: - Context(Backend* backend); + typedef Backend* (*CreateBackend)(); + typedef bool (*MakeProgram)(Shader& shader, const Shader::BindingSet& bindings); + + + // This one call must happen before any context is created or used (Shader::MakeProgram) in order to setup the Backend and any singleton data needed + template + static void init() { + std::call_once(_initialized, [] { + _createBackendCallback = T::createBackend; + _makeProgramCallback = T::makeProgram; + T::init(); + }); + } + + Context(); ~Context(); void render(Batch& batch); @@ -132,13 +146,17 @@ public: protected: Context(const Context& context); + std::unique_ptr _backend; + // This function can only be called by "static Shader::makeProgram()" // makeProgramShader(...) make a program shader ready to be used in a Batch. // It compiles the sub shaders, link them and defines the Slots and their bindings. // If the shader passed is not a program, nothing happens. - static bool makeProgram(Shader& shader, const Shader::BindingSet& bindings = Shader::BindingSet()); + static bool makeProgram(Shader& shader, const Shader::BindingSet& bindings); - std::unique_ptr _backend; + static CreateBackend _createBackendCallback; + static MakeProgram _makeProgramCallback; + static std::once_flag _initialized; friend class Shader; }; diff --git a/libraries/gpu/src/gpu/GLBackend.cpp b/libraries/gpu/src/gpu/GLBackend.cpp index 198fe4802a..c74a94e03e 100644 --- a/libraries/gpu/src/gpu/GLBackend.cpp +++ b/libraries/gpu/src/gpu/GLBackend.cpp @@ -63,12 +63,7 @@ GLBackend::CommandCall GLBackend::_commandCalls[Batch::NUM_COMMANDS] = (&::gpu::GLBackend::do_glLineWidth), }; -GLBackend::GLBackend() : - _input(), - _transform(), - _pipeline(), - _output() -{ +void GLBackend::init() { static std::once_flag once; std::call_once(once, [] { qCDebug(gpulogging) << "GL Version: " << QString((const char*) glGetString(GL_VERSION)); @@ -108,7 +103,18 @@ GLBackend::GLBackend() : }*/ #endif }); +} +Backend* GLBackend::createBackend() { + return new GLBackend(); +} + +GLBackend::GLBackend() : + _input(), + _transform(), + _pipeline(), + _output() +{ initInput(); initTransform(); } diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index 3686c5138d..e86424ceb7 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -22,10 +22,17 @@ namespace gpu { class GLBackend : public Backend { -public: + + // Context Backend static interface required + friend class Context; + static void init(); + static Backend* createBackend(); + static bool makeProgram(Shader& shader, const Shader::BindingSet& bindings = Shader::BindingSet()); explicit GLBackend(bool syncCache); GLBackend(); +public: + virtual ~GLBackend(); virtual void render(Batch& batch); @@ -47,7 +54,6 @@ public: static void checkGLStackStable(std::function f); - static bool makeProgram(Shader& shader, const Shader::BindingSet& bindings = Shader::BindingSet()); class GLBuffer : public GPUObject { diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 601d99108d..d6ebd001d2 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include "RenderUtilsLogging.h" static QQueue _cachedFramebuffers; diff --git a/libraries/render/src/render/DrawStatus.cpp b/libraries/render/src/render/DrawStatus.cpp index f50f517d7d..cf3616a83a 100644 --- a/libraries/render/src/render/DrawStatus.cpp +++ b/libraries/render/src/render/DrawStatus.cpp @@ -18,10 +18,7 @@ #include #include -#include #include -#include -#include #include "drawItemBounds_vert.h" #include "drawItemBounds_frag.h" @@ -151,17 +148,17 @@ void DrawStatus::run(const SceneContextPointer& sceneContext, const RenderContex const unsigned int VEC3_ADRESS_OFFSET = 3; for (int i = 0; i < nbItems; i++) { - batch._glUniform3fv(_drawItemBoundPosLoc, 1, (const GLfloat*) (itemAABox + i)); - batch._glUniform3fv(_drawItemBoundDimLoc, 1, ((const GLfloat*) (itemAABox + i)) + VEC3_ADRESS_OFFSET); + batch._glUniform3fv(_drawItemBoundPosLoc, 1, (const float*) (itemAABox + i)); + batch._glUniform3fv(_drawItemBoundDimLoc, 1, ((const float*) (itemAABox + i)) + VEC3_ADRESS_OFFSET); batch.draw(gpu::LINES, 24, 0); } batch.setPipeline(getDrawItemStatusPipeline()); for (int i = 0; i < nbItems; i++) { - batch._glUniform3fv(_drawItemStatusPosLoc, 1, (const GLfloat*) (itemAABox + i)); - batch._glUniform3fv(_drawItemStatusDimLoc, 1, ((const GLfloat*) (itemAABox + i)) + VEC3_ADRESS_OFFSET); - batch._glUniform4iv(_drawItemStatusValueLoc, 1, (const GLint*) (itemStatus + i)); + batch._glUniform3fv(_drawItemStatusPosLoc, 1, (const float*) (itemAABox + i)); + batch._glUniform3fv(_drawItemStatusDimLoc, 1, ((const float*) (itemAABox + i)) + VEC3_ADRESS_OFFSET); + batch._glUniform4iv(_drawItemStatusValueLoc, 1, (const int*) (itemStatus + i)); batch.draw(gpu::TRIANGLES, 24, 0); } diff --git a/libraries/render/src/render/DrawTask.cpp b/libraries/render/src/render/DrawTask.cpp index 1a3b68af03..36ff302952 100755 --- a/libraries/render/src/render/DrawTask.cpp +++ b/libraries/render/src/render/DrawTask.cpp @@ -17,9 +17,7 @@ #include #include #include -#include #include -#include using namespace render; diff --git a/tests/render-utils/src/main.cpp b/tests/render-utils/src/main.cpp index 7c02c7e8a7..9abd533650 100644 --- a/tests/render-utils/src/main.cpp +++ b/tests/render-utils/src/main.cpp @@ -94,7 +94,6 @@ class QTestWindow : public QWindow { QSize _size; //TextRenderer* _textRenderer[4]; RateCounter fps; - gpu::ContextPointer _gpuContext; protected: void renderText(); @@ -124,7 +123,8 @@ public: show(); makeCurrent(); - _gpuContext.reset(new gpu::Context(new gpu::GLBackend())); + + gpu::Context::init(); From e52bf2e12de0c1bd6f916a5df7606f3fda2c662f Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 30 Jul 2015 15:51:34 -0700 Subject: [PATCH 184/242] Fix for head cauterization. When rendering rigidly bound mesh clusters were not properly using the cauterization matrix set. --- libraries/render-utils/src/Model.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 72bc818b70..417a4bb16f 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1625,15 +1625,21 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran } if (isSkinned) { - const float* bones = (const float*)state.clusterMatrices.constData(); + const float* bones; if (_cauterizeBones) { bones = (const float*)state.cauterizedClusterMatrices.constData(); + } else { + bones = (const float*)state.clusterMatrices.constData(); } batch._glUniformMatrix4fv(locations->clusterMatrices, state.clusterMatrices.size(), false, bones); _transforms[0] = Transform(); _transforms[0].preTranslate(_translation); } else { - _transforms[0] = Transform(state.clusterMatrices[0]); + if (_cauterizeBones) { + _transforms[0] = Transform(state.cauterizedClusterMatrices[0]); + } else { + _transforms[0] = Transform(state.clusterMatrices[0]); + } _transforms[0].preTranslate(_translation); } batch.setModelTransform(_transforms[0]); From 78a900c8661a8565557af37f0726ddfbed76011a Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 30 Jul 2015 17:01:48 -0700 Subject: [PATCH 185/242] Prototype blend. Just equal weighting for now. --- libraries/animation/src/AnimationHandle.cpp | 4 +++- libraries/animation/src/AnimationHandle.h | 2 ++ libraries/animation/src/JointState.cpp | 5 +++-- libraries/animation/src/JointState.h | 2 +- libraries/animation/src/Rig.cpp | 7 +++++-- libraries/animation/src/Rig.h | 2 +- 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/libraries/animation/src/AnimationHandle.cpp b/libraries/animation/src/AnimationHandle.cpp index e8ac6e0a10..605fb25f1c 100644 --- a/libraries/animation/src/AnimationHandle.cpp +++ b/libraries/animation/src/AnimationHandle.cpp @@ -176,7 +176,9 @@ void AnimationHandle::applyFrame(float frameIndex) { safeMix(floorFrame.rotations.at(i), ceilFrame.rotations.at(i), frameFraction), - _priority); + _priority, + false, + _mix); } } } diff --git a/libraries/animation/src/AnimationHandle.h b/libraries/animation/src/AnimationHandle.h index 5d39682a0a..42e564944e 100644 --- a/libraries/animation/src/AnimationHandle.h +++ b/libraries/animation/src/AnimationHandle.h @@ -63,6 +63,7 @@ public: void setPriority(float priority); float getPriority() const { return _priority; } + void setMix(float mix) { _mix = mix; } void setMaskedJoints(const QStringList& maskedJoints); const QStringList& getMaskedJoints() const { return _maskedJoints; } @@ -119,6 +120,7 @@ private: QString _role; QUrl _url; float _priority; + float _mix; QStringList _maskedJoints; QVector _jointMappings; diff --git a/libraries/animation/src/JointState.cpp b/libraries/animation/src/JointState.cpp index 8f14342e80..3682837719 100644 --- a/libraries/animation/src/JointState.cpp +++ b/libraries/animation/src/JointState.cpp @@ -232,12 +232,13 @@ glm::quat JointState::computeVisibleParentRotation() const { return _visibleRotation * glm::inverse(_fbxJoint->preRotation * _visibleRotationInConstrainedFrame * _fbxJoint->postRotation); } -void JointState::setRotationInConstrainedFrame(glm::quat targetRotation, float priority, bool constrain) { +void JointState::setRotationInConstrainedFrame(glm::quat targetRotation, float priority, bool constrain, float mix) { if (priority >= _animationPriority || _animationPriority == 0.0f) { if (constrain && _constraint) { _constraint->softClamp(targetRotation, _rotationInConstrainedFrame, 0.5f); } - setRotationInConstrainedFrameInternal(targetRotation); + auto rotation = (mix == 1.0f) ? targetRotation : safeMix(getRotationInConstrainedFrame(), targetRotation, mix); + setRotationInConstrainedFrameInternal(rotation); _animationPriority = priority; } } diff --git a/libraries/animation/src/JointState.h b/libraries/animation/src/JointState.h index f15590e5f2..93bf83d2c4 100644 --- a/libraries/animation/src/JointState.h +++ b/libraries/animation/src/JointState.h @@ -84,7 +84,7 @@ public: /// NOTE: the JointState's model-frame transform/rotation are NOT updated! void setRotationInBindFrame(const glm::quat& rotation, float priority, bool constrain = false); - void setRotationInConstrainedFrame(glm::quat targetRotation, float priority, bool constrain = false); + void setRotationInConstrainedFrame(glm::quat targetRotation, float priority, bool constrain = false, float mix = 1.0f); void setVisibleRotationInConstrainedFrame(const glm::quat& targetRotation); const glm::quat& getRotationInConstrainedFrame() const { return _rotationInConstrainedFrame; } const glm::quat& getVisibleRotationInConstrainedFrame() const { return _visibleRotationInConstrainedFrame; } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 15f3dd65ec..8406b61d12 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -390,7 +390,10 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos } void Rig::updateAnimations(float deltaTime, glm::mat4 parentTransform) { + int nAnimationsSoFar = 0; foreach (const AnimationHandlePointer& handle, _runningAnimations) { + handle->setMix(1.0f / ++nAnimationsSoFar); + handle->setPriority(1.0); handle->simulate(deltaTime); } @@ -640,13 +643,13 @@ glm::vec3 Rig::getJointDefaultTranslationInConstrainedFrame(int jointIndex) { return _jointStates[jointIndex].getDefaultTranslationInConstrainedFrame(); } -glm::quat Rig::setJointRotationInConstrainedFrame(int jointIndex, glm::quat targetRotation, float priority, bool constrain) { +glm::quat Rig::setJointRotationInConstrainedFrame(int jointIndex, glm::quat targetRotation, float priority, bool constrain, float mix) { glm::quat endRotation; if (jointIndex == -1 || _jointStates.isEmpty()) { return endRotation; } JointState& state = _jointStates[jointIndex]; - state.setRotationInConstrainedFrame(targetRotation, priority, constrain); + state.setRotationInConstrainedFrame(targetRotation, priority, constrain, mix); endRotation = state.getRotationInConstrainedFrame(); return endRotation; } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 2af61bfded..52db16826a 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -139,7 +139,7 @@ public: glm::quat setJointRotationInBindFrame(int jointIndex, const glm::quat& rotation, float priority, bool constrain = false); glm::vec3 getJointDefaultTranslationInConstrainedFrame(int jointIndex); glm::quat setJointRotationInConstrainedFrame(int jointIndex, glm::quat targetRotation, - float priority, bool constrain = false); + float priority, bool constrain = false, float mix = 1.0f); glm::quat getJointDefaultRotationInParentFrame(int jointIndex); void updateVisibleJointStates(); From fbf21cb089a4579b507dee31110266f183429243 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 30 Jul 2015 18:27:47 -0700 Subject: [PATCH 186/242] FIxed the problem on Mac, by removing all of the gpuConfig includesgit status q :q wq --- libraries/gpu/src/gpu/GLBackend.h | 8 +++---- libraries/gpu/src/gpu/GLBackendShader.cpp | 22 +++++++++++++++++-- .../src/DeferredLightingEffect.cpp | 5 ----- libraries/render-utils/src/Model.cpp | 6 +---- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index e86424ceb7..c0bae76d78 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -27,7 +27,7 @@ class GLBackend : public Backend { friend class Context; static void init(); static Backend* createBackend(); - static bool makeProgram(Shader& shader, const Shader::BindingSet& bindings = Shader::BindingSet()); + static bool makeProgram(Shader& shader, const Shader::BindingSet& bindings); explicit GLBackend(bool syncCache); GLBackend(); @@ -95,9 +95,9 @@ public: #if (GPU_TRANSFORM_PROFILE == GPU_CORE) #else - GLuint _transformObject_model = -1; - GLuint _transformCamera_viewInverse = -1; - GLuint _transformCamera_viewport = -1; + GLint _transformObject_model = -1; + GLint _transformCamera_viewInverse = -1; + GLint _transformCamera_viewport = -1; #endif GLShader(); diff --git a/libraries/gpu/src/gpu/GLBackendShader.cpp b/libraries/gpu/src/gpu/GLBackendShader.cpp index 5fa0e277e5..9c0cf7628d 100755 --- a/libraries/gpu/src/gpu/GLBackendShader.cpp +++ b/libraries/gpu/src/gpu/GLBackendShader.cpp @@ -541,7 +541,12 @@ ElementResource getFormatFromGLUniform(GLenum gltype) { }; -int makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& uniforms, Shader::SlotSet& textures, Shader::SlotSet& samplers) { +int makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, + Shader::SlotSet& uniforms, Shader::SlotSet& textures, Shader::SlotSet& samplers +#if (GPU_FEATURE_PROFILE == GPU_LEGACY) + , Shader::SlotSet& fakeBuffers +#endif +) { GLint uniformsCount = 0; #if (GPU_FEATURE_PROFILE == GPU_LEGACY) @@ -582,6 +587,15 @@ int makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, S } if (elementResource._resource == Resource::BUFFER) { +#if (GPU_FEATURE_PROFILE == GPU_LEGACY) + // if in legacy profile, we fake the uniform buffer with an array + // this is where we detect it assuming it's explicitely assinged a binding + auto requestedBinding = slotBindings.find(std::string(sname)); + if (requestedBinding != slotBindings.end()) { + // found one buffer! + fakeBuffers.insert(Shader::Slot(sname, location, elementResource._element, elementResource._resource)); + } +#endif uniforms.insert(Shader::Slot(sname, location, elementResource._element, elementResource._resource)); } else { // For texture/Sampler, the location is the actual binding value @@ -736,8 +750,12 @@ bool GLBackend::makeProgram(Shader& shader, const Shader::BindingSet& slotBindin Shader::SlotSet uniforms; Shader::SlotSet textures; Shader::SlotSet samplers; +#if (GPU_FEATURE_PROFILE == GPU_CORE) makeUniformSlots(object->_program, slotBindings, uniforms, textures, samplers); - +#else + makeUniformSlots(object->_program, slotBindings, uniforms, textures, samplers, buffers); +#endif + Shader::SlotSet inputs; makeInputSlots(object->_program, slotBindings, inputs); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 7563609026..3fc0a4c46a 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -614,13 +614,8 @@ void DeferredLightingEffect::loadLightProgram(const char* vertSource, const char locations.texcoordMat = program->getUniforms().findLocation("texcoordMat"); locations.coneParam = program->getUniforms().findLocation("coneParam"); -#if (GPU_FEATURE_PROFILE == GPU_CORE) locations.lightBufferUnit = program->getBuffers().findLocation("lightBuffer"); locations.atmosphereBufferUnit = program->getBuffers().findLocation("atmosphereBufferUnit"); -#else - locations.lightBufferUnit = program->getUniforms().findLocation("lightBuffer"); - locations.atmosphereBufferUnit = program->getUniforms().findLocation("atmosphereBufferUnit"); -#endif auto state = std::make_shared(); if (lightVolume) { diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 1e0e04c776..1e4f3f7190 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -187,13 +187,9 @@ void Model::RenderPipelineLib::initLocations(gpu::ShaderPointer& program, Model: locations.specularTextureUnit = program->getTextures().findLocation("specularMap"); locations.emissiveTextureUnit = program->getTextures().findLocation("emissiveMap"); -#if (GPU_FEATURE_PROFILE == GPU_CORE) locations.materialBufferUnit = program->getBuffers().findLocation("materialBuffer"); locations.lightBufferUnit = program->getBuffers().findLocation("lightBuffer"); -#else - locations.materialBufferUnit = program->getUniforms().findLocation("materialBuffer"); - locations.lightBufferUnit = program->getUniforms().findLocation("lightBuffer"); -#endif + locations.clusterMatrices = program->getUniforms().findLocation("clusterMatrices"); locations.clusterIndices = program->getInputs().findLocation("clusterIndices");; From c1f777e18fc397ce799b3cbe72a32f5942bdc477 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 30 Jul 2015 18:39:01 -0700 Subject: [PATCH 187/242] move setHandPosition from SkeletonModel to AvatarRig. Tell Rig::initJointStates joint-indexes for hands and elbows and shoulders --- interface/src/avatar/SkeletonModel.cpp | 83 ++++++-------------------- interface/src/avatar/SkeletonModel.h | 5 -- libraries/animation/src/AvatarRig.cpp | 65 ++++++++++++++++++++ libraries/animation/src/AvatarRig.h | 2 + libraries/animation/src/EntityRig.h | 2 + libraries/animation/src/Rig.cpp | 18 +++++- libraries/animation/src/Rig.h | 24 +++++++- libraries/render-utils/src/Model.cpp | 18 +++++- 8 files changed, 142 insertions(+), 75 deletions(-) diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 1b3298c75d..1d3a1df82a 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -42,7 +42,23 @@ SkeletonModel::~SkeletonModel() { void SkeletonModel::initJointStates(QVector states) { const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - _boundingRadius = _rig->initJointStates(states, parentTransform); + + int rootJointIndex = geometry.rootJointIndex; + int leftHandJointIndex = geometry.leftHandJointIndex; + int leftElbowJointIndex = leftHandJointIndex >= 0 ? geometry.joints.at(leftHandJointIndex).parentIndex : -1; + int leftShoulderJointIndex = leftElbowJointIndex >= 0 ? geometry.joints.at(leftElbowJointIndex).parentIndex : -1; + int rightHandJointIndex = geometry.rightHandJointIndex; + int rightElbowJointIndex = rightHandJointIndex >= 0 ? geometry.joints.at(rightHandJointIndex).parentIndex : -1; + int rightShoulderJointIndex = rightElbowJointIndex >= 0 ? geometry.joints.at(rightElbowJointIndex).parentIndex : -1; + + _boundingRadius = _rig->initJointStates(states, parentTransform, + rootJointIndex, + leftHandJointIndex, + leftElbowJointIndex, + leftShoulderJointIndex, + rightHandJointIndex, + rightElbowJointIndex, + rightShoulderJointIndex); // Determine the default eye position for avatar scale = 1.0 int headJointIndex = _geometry->getFBXGeometry().headJointIndex; @@ -227,7 +243,7 @@ void SkeletonModel::applyPalmData(int jointIndex, PalmData& palm) { palmRotation = rotationBetween(palmRotation * glm::vec3(-sign, 0.0f, 0.0f), fingerDirection) * palmRotation; if (Menu::getInstance()->isOptionChecked(MenuOption::AlternateIK)) { - setHandPosition(jointIndex, palmPosition, palmRotation); + _rig->setHandPosition(jointIndex, palmPosition, palmRotation, extractUniformScale(_scale), PALM_PRIORITY); } else if (Menu::getInstance()->isOptionChecked(MenuOption::AlignForearmsWithWrists)) { float forearmLength = geometry.joints.at(jointIndex).distanceToParent * extractUniformScale(_scale); glm::vec3 forearm = palmRotation * glm::vec3(sign * forearmLength, 0.0f, 0.0f); @@ -332,69 +348,6 @@ void SkeletonModel::renderOrientationDirections(gpu::Batch& batch, int jointInde geometryCache->renderLine(batch, position, pFront, blue, jointLineIDs._front); } - - -void SkeletonModel::setHandPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation) { - // this algorithm is from sample code from sixense - const FBXGeometry& geometry = _geometry->getFBXGeometry(); - int elbowJointIndex = geometry.joints.at(jointIndex).parentIndex; - if (elbowJointIndex == -1) { - return; - } - int shoulderJointIndex = geometry.joints.at(elbowJointIndex).parentIndex; - glm::vec3 shoulderPosition; - if (!getJointPosition(shoulderJointIndex, shoulderPosition)) { - return; - } - // precomputed lengths - float scale = extractUniformScale(_scale); - float upperArmLength = geometry.joints.at(elbowJointIndex).distanceToParent * scale; - float lowerArmLength = geometry.joints.at(jointIndex).distanceToParent * scale; - - // first set wrist position - glm::vec3 wristPosition = position; - - glm::vec3 shoulderToWrist = wristPosition - shoulderPosition; - float distanceToWrist = glm::length(shoulderToWrist); - - // prevent gimbal lock - if (distanceToWrist > upperArmLength + lowerArmLength - EPSILON) { - distanceToWrist = upperArmLength + lowerArmLength - EPSILON; - shoulderToWrist = glm::normalize(shoulderToWrist) * distanceToWrist; - wristPosition = shoulderPosition + shoulderToWrist; - } - - // cosine of angle from upper arm to hand vector - float cosA = (upperArmLength * upperArmLength + distanceToWrist * distanceToWrist - lowerArmLength * lowerArmLength) / - (2 * upperArmLength * distanceToWrist); - float mid = upperArmLength * cosA; - float height = sqrt(upperArmLength * upperArmLength + mid * mid - 2 * upperArmLength * mid * cosA); - - // direction of the elbow - glm::vec3 handNormal = glm::cross(rotation * glm::vec3(0.0f, 1.0f, 0.0f), shoulderToWrist); // elbow rotating with wrist - glm::vec3 relaxedNormal = glm::cross(glm::vec3(0.0f, 1.0f, 0.0f), shoulderToWrist); // elbow pointing straight down - const float NORMAL_WEIGHT = 0.5f; - glm::vec3 finalNormal = glm::mix(relaxedNormal, handNormal, NORMAL_WEIGHT); - - bool rightHand = (jointIndex == geometry.rightHandJointIndex); - if (rightHand ? (finalNormal.y > 0.0f) : (finalNormal.y < 0.0f)) { - finalNormal.y = 0.0f; // dont allow elbows to point inward (y is vertical axis) - } - - glm::vec3 tangent = glm::normalize(glm::cross(shoulderToWrist, finalNormal)); - - // ik solution - glm::vec3 elbowPosition = shoulderPosition + glm::normalize(shoulderToWrist) * mid - tangent * height; - glm::vec3 forwardVector(rightHand ? -1.0f : 1.0f, 0.0f, 0.0f); - glm::quat shoulderRotation = rotationBetween(forwardVector, elbowPosition - shoulderPosition); - - _rig->setJointRotationInBindFrame(shoulderJointIndex, shoulderRotation, PALM_PRIORITY); - _rig->setJointRotationInBindFrame(elbowJointIndex, - rotationBetween(shoulderRotation * forwardVector, wristPosition - elbowPosition) * - shoulderRotation, PALM_PRIORITY); - _rig->setJointRotationInBindFrame(jointIndex, rotation, PALM_PRIORITY); -} - bool SkeletonModel::getLeftHandPosition(glm::vec3& position) const { return getJointPositionInWorldFrame(getLeftHandJointIndex(), position); } diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index ecc5c80118..a13d3c4e75 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -131,11 +131,6 @@ private: QHash _jointOrientationLines; int _triangleFanID; - /// \param jointIndex index of joint in model - /// \param position position of joint in model-frame - /// \param rotation rotation of joint in model-frame - void setHandPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation); - bool getEyeModelPositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const; Avatar* _owningAvatar; diff --git a/libraries/animation/src/AvatarRig.cpp b/libraries/animation/src/AvatarRig.cpp index 919ea43e7d..73ac793c80 100644 --- a/libraries/animation/src/AvatarRig.cpp +++ b/libraries/animation/src/AvatarRig.cpp @@ -32,3 +32,68 @@ void AvatarRig::updateJointState(int index, glm::mat4 parentTransform) { } } } + +void AvatarRig::setHandPosition(int jointIndex, + const glm::vec3& position, const glm::quat& rotation, + float scale, float priority) { + bool rightHand = (jointIndex == _rightHandJointIndex); + + int elbowJointIndex = rightHand ? _rightElbowJointIndex : _leftElbowJointIndex; + int shoulderJointIndex = rightHand ? _rightShoulderJointIndex : _leftShoulderJointIndex; + + // this algorithm is from sample code from sixense + if (elbowJointIndex == -1 || shoulderJointIndex == -1) { + return; + } + + glm::vec3 shoulderPosition; + if (!getJointPosition(shoulderJointIndex, shoulderPosition)) { + return; + } + + // precomputed lengths + float upperArmLength = _jointStates[elbowJointIndex].getFBXJoint().distanceToParent * scale; + float lowerArmLength = _jointStates[jointIndex].getFBXJoint().distanceToParent * scale; + + // first set wrist position + glm::vec3 wristPosition = position; + + glm::vec3 shoulderToWrist = wristPosition - shoulderPosition; + float distanceToWrist = glm::length(shoulderToWrist); + + // prevent gimbal lock + if (distanceToWrist > upperArmLength + lowerArmLength - EPSILON) { + distanceToWrist = upperArmLength + lowerArmLength - EPSILON; + shoulderToWrist = glm::normalize(shoulderToWrist) * distanceToWrist; + wristPosition = shoulderPosition + shoulderToWrist; + } + + // cosine of angle from upper arm to hand vector + float cosA = (upperArmLength * upperArmLength + distanceToWrist * distanceToWrist - lowerArmLength * lowerArmLength) / + (2 * upperArmLength * distanceToWrist); + float mid = upperArmLength * cosA; + float height = sqrt(upperArmLength * upperArmLength + mid * mid - 2 * upperArmLength * mid * cosA); + + // direction of the elbow + glm::vec3 handNormal = glm::cross(rotation * glm::vec3(0.0f, 1.0f, 0.0f), shoulderToWrist); // elbow rotating with wrist + glm::vec3 relaxedNormal = glm::cross(glm::vec3(0.0f, 1.0f, 0.0f), shoulderToWrist); // elbow pointing straight down + const float NORMAL_WEIGHT = 0.5f; + glm::vec3 finalNormal = glm::mix(relaxedNormal, handNormal, NORMAL_WEIGHT); + + if (rightHand ? (finalNormal.y > 0.0f) : (finalNormal.y < 0.0f)) { + finalNormal.y = 0.0f; // dont allow elbows to point inward (y is vertical axis) + } + + glm::vec3 tangent = glm::normalize(glm::cross(shoulderToWrist, finalNormal)); + + // ik solution + glm::vec3 elbowPosition = shoulderPosition + glm::normalize(shoulderToWrist) * mid - tangent * height; + glm::vec3 forwardVector(rightHand ? -1.0f : 1.0f, 0.0f, 0.0f); + glm::quat shoulderRotation = rotationBetween(forwardVector, elbowPosition - shoulderPosition); + + setJointRotationInBindFrame(shoulderJointIndex, shoulderRotation, priority); + setJointRotationInBindFrame(elbowJointIndex, + rotationBetween(shoulderRotation * forwardVector, wristPosition - elbowPosition) * + shoulderRotation, priority); + setJointRotationInBindFrame(jointIndex, rotation, priority); +} diff --git a/libraries/animation/src/AvatarRig.h b/libraries/animation/src/AvatarRig.h index 4a111a535b..5e2153e226 100644 --- a/libraries/animation/src/AvatarRig.h +++ b/libraries/animation/src/AvatarRig.h @@ -22,6 +22,8 @@ class AvatarRig : public Rig { public: ~AvatarRig() {} virtual void updateJointState(int index, glm::mat4 parentTransform); + virtual void setHandPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, + float scale, float priority); }; #endif // hifi_AvatarRig_h diff --git a/libraries/animation/src/EntityRig.h b/libraries/animation/src/EntityRig.h index e8e15a5a28..9b519a7bfe 100644 --- a/libraries/animation/src/EntityRig.h +++ b/libraries/animation/src/EntityRig.h @@ -22,6 +22,8 @@ class EntityRig : public Rig { public: ~EntityRig() {} virtual void updateJointState(int index, glm::mat4 parentTransform); + virtual void setHandPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, + float scale, float priority) {} }; #endif // hifi_EntityRig_h diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 15f3dd65ec..e986d220d7 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -143,8 +143,24 @@ void Rig::deleteAnimations() { _animationHandles.clear(); } -float Rig::initJointStates(QVector states, glm::mat4 parentTransform) { +float Rig::initJointStates(QVector states, glm::mat4 parentTransform, + int rootJointIndex, + int leftHandJointIndex, + int leftElbowJointIndex, + int leftShoulderJointIndex, + int rightHandJointIndex, + int rightElbowJointIndex, + int rightShoulderJointIndex) { _jointStates = states; + + _rootJointIndex = rootJointIndex; + _leftHandJointIndex = leftHandJointIndex; + _leftElbowJointIndex = leftElbowJointIndex; + _leftShoulderJointIndex = leftShoulderJointIndex; + _rightHandJointIndex = rightHandJointIndex; + _rightElbowJointIndex = rightElbowJointIndex; + _rightShoulderJointIndex = rightShoulderJointIndex; + initJointTransforms(parentTransform); int numStates = _jointStates.size(); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 2af61bfded..533717a422 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -42,7 +42,6 @@ class AnimationHandle; typedef std::shared_ptr AnimationHandlePointer; -// typedef QWeakPointer WeakAnimationHandlePointer; class Rig; typedef std::shared_ptr RigPointer; @@ -90,7 +89,14 @@ public: float priority = 1.0f, bool loop = false, bool hold = false, float firstFrame = 0.0f, float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList(), bool startAutomatically = false); - float initJointStates(QVector states, glm::mat4 parentTransform); + float initJointStates(QVector states, glm::mat4 parentTransform, + int rootJointIndex, + int leftHandJointIndex, + int leftElbowJointIndex, + int leftShoulderJointIndex, + int rightHandJointIndex, + int rightElbowJointIndex, + int rightShoulderJointIndex); bool jointStatesEmpty() { return _jointStates.isEmpty(); }; int getJointStateCount() const { return _jointStates.size(); } int indexOfJoint(const QString& jointName) ; @@ -149,6 +155,9 @@ public: void updateFromHeadParameters(const HeadParameters& params); + virtual void setHandPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, + float scale, float priority) = 0; + protected: void updateLeanJoint(int index, float leanSideways, float leanForward, float torsoTwist); @@ -156,6 +165,15 @@ public: void updateEyeJoint(int index, const glm::quat& worldHeadOrientation, const glm::vec3& lookAt, const glm::vec3& saccade); QVector _jointStates; + int _rootJointIndex = -1; + + int _leftHandJointIndex = -1; + int _leftElbowJointIndex = -1; + int _leftShoulderJointIndex = -1; + + int _rightHandJointIndex = -1; + int _rightElbowJointIndex = -1; + int _rightShoulderJointIndex = -1; QList _animationHandles; QList _runningAnimations; @@ -166,6 +184,6 @@ public: bool _isIdle; glm::vec3 _lastFront; glm::vec3 _lastPosition; - }; +}; #endif /* defined(__hifi__Rig__) */ diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 417a4bb16f..d89c40a0be 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -474,7 +474,23 @@ bool Model::updateGeometry() { void Model::initJointStates(QVector states) { const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; - _boundingRadius = _rig->initJointStates(states, parentTransform); + + int rootJointIndex = geometry.rootJointIndex; + int leftHandJointIndex = geometry.leftHandJointIndex; + int leftElbowJointIndex = leftHandJointIndex >= 0 ? geometry.joints.at(leftHandJointIndex).parentIndex : -1; + int leftShoulderJointIndex = leftElbowJointIndex >= 0 ? geometry.joints.at(leftElbowJointIndex).parentIndex : -1; + int rightHandJointIndex = geometry.rightHandJointIndex; + int rightElbowJointIndex = rightHandJointIndex >= 0 ? geometry.joints.at(rightHandJointIndex).parentIndex : -1; + int rightShoulderJointIndex = rightElbowJointIndex >= 0 ? geometry.joints.at(rightElbowJointIndex).parentIndex : -1; + + _boundingRadius = _rig->initJointStates(states, parentTransform, + rootJointIndex, + leftHandJointIndex, + leftElbowJointIndex, + leftShoulderJointIndex, + rightHandJointIndex, + rightElbowJointIndex, + rightShoulderJointIndex); } bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, From 241e767ab6eca0558679f59ba9c37b056346bc0a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 30 Jul 2015 18:44:09 -0700 Subject: [PATCH 188/242] move setHandPosition from SkeletonModel to AvatarRig. Tell Rig::initJointStates joint-indexes for hands and elbows and shoulders --- libraries/animation/src/AvatarRig.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libraries/animation/src/AvatarRig.cpp b/libraries/animation/src/AvatarRig.cpp index 73ac793c80..97fbb82944 100644 --- a/libraries/animation/src/AvatarRig.cpp +++ b/libraries/animation/src/AvatarRig.cpp @@ -20,13 +20,14 @@ void AvatarRig::updateJointState(int index, glm::mat4 parentTransform) { const FBXJoint& joint = state.getFBXJoint(); // compute model transforms - int parentIndex = joint.parentIndex; - if (parentIndex == -1) { + if (index == _rootJointIndex) { + // we always zero-out the translation part of an avatar's root join-transform. state.computeTransform(parentTransform); clearJointTransformTranslation(index); } else { // guard against out-of-bounds access to _jointStates - if (joint.parentIndex >= 0 && joint.parentIndex < _jointStates.size()) { + int parentIndex = joint.parentIndex; + if (parentIndex >= 0 && parentIndex < _jointStates.size()) { const JointState& parentState = _jointStates.at(parentIndex); state.computeTransform(parentState.getTransform(), parentState.getTransformChanged()); } From 0bc94158a8031fa022ae687ea4ea1a8258a82fd7 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 30 Jul 2015 21:09:14 -0700 Subject: [PATCH 189/242] make the height of the tetrahedrons in 'fatten' mode less tall. --- tools/vhacd-util/src/VHACDUtil.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index 2f8175cbb6..02b8e212be 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -125,12 +125,14 @@ void vhacd::VHACDUtil::fattenMeshes(const FBXMesh& mesh, FBXMesh& result, continue; } + // from the middle of the triangle, pull a point down to form a tetrahedron. float dropAmount = 0; dropAmount = glm::max(glm::length(p1 - p0), dropAmount); dropAmount = glm::max(glm::length(p2 - p1), dropAmount); dropAmount = glm::max(glm::length(p0 - p2), dropAmount); + dropAmount *= 0.25f; - glm::vec3 p3 = av - glm::vec3(0, dropAmount, 0); // a point 1 meter below the average of this triangle's points + glm::vec3 p3 = av - glm::vec3(0, dropAmount, 0); int index3 = result.vertices.size(); result.vertices << p3; // add the new point to the result mesh From c8f398024eb21f49959ab2fc92d2e3a4f95a1fbb Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 30 Jul 2015 21:16:07 -0700 Subject: [PATCH 190/242] replace a magic number with a constant --- tools/vhacd-util/src/VHACDUtil.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index 02b8e212be..6743ed6a9f 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -12,6 +12,8 @@ #include #include "VHACDUtil.h" +const float collisionTetrahedronScale = 0.25f; + // FBXReader jumbles the order of the meshes by reading them back out of a hashtable. This will put // them back in the order in which they appeared in the file. @@ -130,7 +132,7 @@ void vhacd::VHACDUtil::fattenMeshes(const FBXMesh& mesh, FBXMesh& result, dropAmount = glm::max(glm::length(p1 - p0), dropAmount); dropAmount = glm::max(glm::length(p2 - p1), dropAmount); dropAmount = glm::max(glm::length(p0 - p2), dropAmount); - dropAmount *= 0.25f; + dropAmount *= collisionTetrahedronScale; glm::vec3 p3 = av - glm::vec3(0, dropAmount, 0); int index3 = result.vertices.size(); From 5bae9843f5ee7fe3220d54bbf343490fa4c6ef5c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 31 Jul 2015 09:34:50 -0700 Subject: [PATCH 191/242] code review --- tools/vhacd-util/src/VHACDUtil.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index 6743ed6a9f..1e7f9df7c1 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -12,7 +12,7 @@ #include #include "VHACDUtil.h" -const float collisionTetrahedronScale = 0.25f; +const float COLLISION_TETRAHEDRON_SCALE = 0.25f; // FBXReader jumbles the order of the meshes by reading them back out of a hashtable. This will put @@ -132,9 +132,9 @@ void vhacd::VHACDUtil::fattenMeshes(const FBXMesh& mesh, FBXMesh& result, dropAmount = glm::max(glm::length(p1 - p0), dropAmount); dropAmount = glm::max(glm::length(p2 - p1), dropAmount); dropAmount = glm::max(glm::length(p0 - p2), dropAmount); - dropAmount *= collisionTetrahedronScale; + dropAmount *= COLLISION_TETRAHEDRON_SCALE; - glm::vec3 p3 = av - glm::vec3(0, dropAmount, 0); + glm::vec3 p3 = av - glm::vec3(0.0f, dropAmount, 0.0f); int index3 = result.vertices.size(); result.vertices << p3; // add the new point to the result mesh From 67760fad79897fa3151bd3b265a484f9c8b121d8 Mon Sep 17 00:00:00 2001 From: bwent Date: Fri, 31 Jul 2015 09:27:25 -0700 Subject: [PATCH 192/242] Add gettable naturalPosition property for model entities --- libraries/entities/src/EntityItemProperties.cpp | 9 ++++++++- libraries/entities/src/EntityItemProperties.h | 6 +++++- libraries/entities/src/EntityScriptingInterface.cpp | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 61253ba6ba..caae0203ae 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -114,7 +114,8 @@ _glowLevelChanged(false), _localRenderAlphaChanged(false), _defaultSettings(true), -_naturalDimensions(1.0f, 1.0f, 1.0f) +_naturalDimensions(1.0f, 1.0f, 1.0f), +_naturalPosition(0.0f, 0.0f, 0.0f) { } @@ -128,6 +129,11 @@ void EntityItemProperties::setSittingPoints(const QVector& sitting } } +void EntityItemProperties::setNaturalPosition(const glm::vec3& min, const glm::vec3& max) { + glm::vec3 radius = (max - min) / 2.0f; + _naturalPosition = max - radius; +} + bool EntityItemProperties::animationSettingsChanged() const { return _animationSettingsChanged; } @@ -378,6 +384,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(dimensions); if (!skipDefaults) { COPY_PROPERTY_TO_QSCRIPTVALUE(naturalDimensions); // gettable, but not settable + COPY_PROPERTY_TO_QSCRIPTVALUE(naturalPosition); } COPY_PROPERTY_TO_QSCRIPTVALUE(rotation); COPY_PROPERTY_TO_QSCRIPTVALUE(velocity); diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 4532ffd67b..94e0f0fac8 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -192,7 +192,10 @@ public: const glm::vec3& getNaturalDimensions() const { return _naturalDimensions; } void setNaturalDimensions(const glm::vec3& value) { _naturalDimensions = value; } - + + const glm::vec3& getNaturalPosition() const { return _naturalPosition; } + void setNaturalPosition(const glm::vec3& min, const glm::vec3& max); + const QStringList& getTextureNames() const { return _textureNames; } void setTextureNames(const QStringList& value) { _textureNames = value; } @@ -232,6 +235,7 @@ private: QVector _sittingPoints; QStringList _textureNames; glm::vec3 _naturalDimensions; + glm::vec3 _naturalPosition; }; Q_DECLARE_METATYPE(EntityItemProperties); diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 36fcc17a71..c40f7edd47 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -118,6 +118,7 @@ EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identit results.setSittingPoints(geometry->sittingPoints); Extents meshExtents = geometry->getUnscaledMeshExtents(); results.setNaturalDimensions(meshExtents.maximum - meshExtents.minimum); + results.setNaturalPosition(meshExtents.minimum, meshExtents.maximum); } } From aab1f708002c5e15d19e7b0371fa05a2faccd6e7 Mon Sep 17 00:00:00 2001 From: bwent Date: Fri, 31 Jul 2015 10:47:05 -0700 Subject: [PATCH 193/242] Renaming vars --- libraries/entities/src/EntityItemProperties.cpp | 6 +++--- libraries/entities/src/EntityItemProperties.h | 2 +- libraries/entities/src/EntityScriptingInterface.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index caae0203ae..ebe4ac6014 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -129,9 +129,9 @@ void EntityItemProperties::setSittingPoints(const QVector& sitting } } -void EntityItemProperties::setNaturalPosition(const glm::vec3& min, const glm::vec3& max) { - glm::vec3 radius = (max - min) / 2.0f; - _naturalPosition = max - radius; +void EntityItemProperties::calculateNaturalPosition(const glm::vec3& min, const glm::vec3& max) { + glm::vec3 halfDimension = (max - min) / 2.0f; + _naturalPosition = max - halfDimension; } bool EntityItemProperties::animationSettingsChanged() const { diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 94e0f0fac8..3ce6040d19 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -194,7 +194,7 @@ public: void setNaturalDimensions(const glm::vec3& value) { _naturalDimensions = value; } const glm::vec3& getNaturalPosition() const { return _naturalPosition; } - void setNaturalPosition(const glm::vec3& min, const glm::vec3& max); + void calculateNaturalPosition(const glm::vec3& min, const glm::vec3& max); const QStringList& getTextureNames() const { return _textureNames; } void setTextureNames(const QStringList& value) { _textureNames = value; } diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index c40f7edd47..12f5ffe190 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -118,7 +118,7 @@ EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identit results.setSittingPoints(geometry->sittingPoints); Extents meshExtents = geometry->getUnscaledMeshExtents(); results.setNaturalDimensions(meshExtents.maximum - meshExtents.minimum); - results.setNaturalPosition(meshExtents.minimum, meshExtents.maximum); + results.calculateNaturalPosition(meshExtents.minimum, meshExtents.maximum); } } From dff6b0a456a96cd7eb37b9069d811ea133c3f0df Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Tue, 30 Jun 2015 14:32:37 -0700 Subject: [PATCH 194/242] Fix isFacingAvatar property on BillboardOverlay. --- .../src/ui/overlays/BillboardOverlay.cpp | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/interface/src/ui/overlays/BillboardOverlay.cpp b/interface/src/ui/overlays/BillboardOverlay.cpp index d26ecc5c67..37e197130b 100644 --- a/interface/src/ui/overlays/BillboardOverlay.cpp +++ b/interface/src/ui/overlays/BillboardOverlay.cpp @@ -20,6 +20,9 @@ #include "Application.h" #include "GeometryUtil.h" +#include + + BillboardOverlay::BillboardOverlay() { _isLoaded = false; } @@ -45,9 +48,28 @@ void BillboardOverlay::render(RenderArgs* args) { glm::quat rotation; if (_isFacingAvatar) { + // LOL, quaternions are hard. // rotate about vertical to face the camera +// glm::vec3 dPos = getPosition() - args->_viewFrustum->getPosition(); +// dPos = glm::normalize(dPos); +// rotation = glm::quat(0, dPos.x, dPos.y, dPos.z); rotation = args->_viewFrustum->getOrientation(); rotation *= glm::angleAxis(glm::pi(), IDENTITY_UP); +// float horizontal = glm::sqrt(dPos.x * dPos.x + dPos.y + dPos.y); +// glm::vec3 zAxis = glm::vec3(0, 0, 1); +// rotation = rotationBetween(zAxis, dPos); +// glm::vec3 euler = safeEulerAngles(rotationBetween(zAxis, dPos)); +// rotation = glm::quat(glm::vec3(euler.x, euler.y, 0)); +// float yaw = (dPos.x == 0.0f && dPos.z == 0.0f) ? 0.0f : glm::atan(dPos.x, dPos.z); +// glm::quat yawQuat = glm::quat(glm::vec3(0, yaw, 0)); +// float pitch = (dPos.y == 0.0f && horizontal == 0.0f) ? 0.0f : glm::atan(dPos.y, horizontal); +// glm::quat pitchQuat = glm::quat(glm::vec3(pitch, 0, 0)); +// glm::mat4x4 matrix = glm::lookAt(args->_viewFrustum->getPosition(), getPosition(), +// glm::vec3(0, 1, 0)); +// rotation = glm::quat_cast(matrix); +// rotation = yawQuat * pitchQuat; +// glm::vec3 pitch = glm::vec3(dPos.x, dPos.y, 0); +// rotation = glm::quat(glm::vec3(pitch, yaw, 0)); rotation *= getRotation(); } else { rotation = getRotation(); From 173a79867ce16f78dfe28b8d8affb06b9cde5909 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Mon, 13 Jul 2015 16:38:16 -0700 Subject: [PATCH 195/242] Extend Overlays API to allow for 3D UI panels. Currently, only BillboardOverlays can be added to a panel, but more types of overlays will be supported in the future. --- examples/example/ui/floatingUIExample.js | 173 ++++++++++++++++++ .../src/ui/overlays/BillboardOverlay.cpp | 50 ++++- interface/src/ui/overlays/BillboardOverlay.h | 5 +- interface/src/ui/overlays/FloatingUIPanel.cpp | 89 +++++++++ interface/src/ui/overlays/FloatingUIPanel.h | 47 +++++ interface/src/ui/overlays/Overlays.cpp | 139 ++++++++++---- interface/src/ui/overlays/Overlays.h | 24 +++ interface/src/ui/overlays/PanelAttachable.h | 44 +++++ 8 files changed, 530 insertions(+), 41 deletions(-) create mode 100644 examples/example/ui/floatingUIExample.js create mode 100644 interface/src/ui/overlays/FloatingUIPanel.cpp create mode 100644 interface/src/ui/overlays/FloatingUIPanel.h create mode 100644 interface/src/ui/overlays/PanelAttachable.h diff --git a/examples/example/ui/floatingUIExample.js b/examples/example/ui/floatingUIExample.js new file mode 100644 index 0000000000..09deca4ec7 --- /dev/null +++ b/examples/example/ui/floatingUIExample.js @@ -0,0 +1,173 @@ +// +// floatingUI.js +// examples/example/ui +// +// Created by Alexander Otavka +// 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 +// + +Script.include(["../../libraries/globals.js"]); + +var BG_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/card-bg.svg"; +var RED_DOT_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/red-dot.svg"; +var BLUE_SQUARE_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/blue-square.svg"; + +var BLANK_ROTATION = { x: 0, y: 0, z: 0, w: 0 }; + +function isBlank(rotation) { + return rotation.x == BLANK_ROTATION.x && + rotation.y == BLANK_ROTATION.y && + rotation.z == BLANK_ROTATION.z && + rotation.w == BLANK_ROTATION.w; +} + +var panel = Overlays.addPanel({ + offsetPosition: { x: 0, y: 0, z: 1 }, +}); + +var panelChildren = []; + +var bg = Overlays.addOverlay("billboard", { + url: BG_IMAGE_URL, + dimensions: { + x: 0.5, + y: 0.5, + }, + isFacingAvatar: false, + visible: true, + alpha: 1.0, + ignoreRayIntersection: false, + attachedPanel: panel, +}); +panelChildren.push(bg); + +var redDot = Overlays.addOverlay("billboard", { + url: RED_DOT_IMAGE_URL, + dimensions: { + x: 0.1, + y: 0.1, + }, + isFacingAvatar: false, + visible: true, + alpha: 1.0, + ignoreRayIntersection: false, + attachedPanel: panel, + offsetPosition: { + x: -0.15, + y: -0.15, + z: -0.001 + } +}); +panelChildren.push(redDot); + +var redDot2 = Overlays.addOverlay("billboard", { + url: RED_DOT_IMAGE_URL, + dimensions: { + x: 0.1, + y: 0.1, + }, + isFacingAvatar: false, + visible: true, + alpha: 1.0, + ignoreRayIntersection: false, + attachedPanel: panel, + offsetPosition: { + x: -0.15, + y: 0, + z: -0.001 + } +}); +panelChildren.push(redDot2); + +var blueSquare = Overlays.addOverlay("billboard", { + url: BLUE_SQUARE_IMAGE_URL, + dimensions: { + x: 0.1, + y: 0.1, + }, + isFacingAvatar: false, + visible: true, + alpha: 1.0, + ignoreRayIntersection: false, + attachedPanel: panel, + offsetPosition: { + x: 0.1, + y: 0, + z: -0.001 + } +}); +panelChildren.push(blueSquare); + +var blueSquare2 = Overlays.addOverlay("billboard", { + url: BLUE_SQUARE_IMAGE_URL, + dimensions: { + x: 0.1, + y: 0.1, + }, + isFacingAvatar: false, + visible: true, + alpha: 1.0, + ignoreRayIntersection: false, + attachedPanel: panel, + offsetPosition: { + x: 0.1, + y: 0.11, + z: -0.001 + } +}); +panelChildren.push(blueSquare2); + +var blueSquare3 = Overlays.addOverlay("billboard", { + url: BLUE_SQUARE_IMAGE_URL, + dimensions: { + x: 0.1, + y: 0.1, + }, + isFacingAvatar: false, + visible: true, + alpha: 1.0, + ignoreRayIntersection: false, + attachedPanel: panel, + offsetPosition: { + x: -0.01, + y: 0.11, + z: -0.001 + } +}); +panelChildren.push(blueSquare3); + +Controller.mousePressEvent.connect(function(event) { + if (event.isRightButton) { + var newOffsetRotation = BLANK_ROTATION; + if (isBlank(Overlays.getPanelProperty(panel, "offsetRotation"))) { + newOffsetRotation = Quat.multiply(MyAvatar.orientation, { x: 0, y: 1, z: 0, w: 0 }); + } + Overlays.editPanel(panel, { + offsetRotation: newOffsetRotation + }); + } else if (event.isLeftButton) { + var pickRay = Camera.computePickRay(event.x, event.y) + var rayPickResult = Overlays.findRayIntersection(pickRay); + print(String(rayPickResult.overlayID)); + if (rayPickResult.intersects) { + for (var i in panelChildren) { + if (panelChildren[i] == rayPickResult.overlayID) { + var oldPos = Overlays.getProperty(rayPickResult.overlayID, "offsetPosition"); + var newPos = { + x: Number(oldPos.x), + y: Number(oldPos.y), + z: Number(oldPos.z) + 0.1 + } + Overlays.editOverlay(rayPickResult.overlayID, { offsetPosition: newPos }); + } + } + } + } +}); + +Script.scriptEnding.connect(function() { + Overlays.deletePanel(panel); +}); \ No newline at end of file diff --git a/interface/src/ui/overlays/BillboardOverlay.cpp b/interface/src/ui/overlays/BillboardOverlay.cpp index 37e197130b..9e1a9a44de 100644 --- a/interface/src/ui/overlays/BillboardOverlay.cpp +++ b/interface/src/ui/overlays/BillboardOverlay.cpp @@ -36,6 +36,13 @@ BillboardOverlay::BillboardOverlay(const BillboardOverlay* billboardOverlay) : { } +void BillboardOverlay::update(float deltatime) { + glm::vec3 newPos = getTranslatedPosition(Application::getInstance()->getAvatarPosition()); + if (newPos != glm::vec3()) { + setPosition(newPos); + } +} + void BillboardOverlay::render(RenderArgs* args) { if (!_texture) { _isLoaded = true; @@ -46,15 +53,17 @@ void BillboardOverlay::render(RenderArgs* args) { return; } + glm::vec3 newPos = getTranslatedPosition(Application::getInstance()->getAvatarPosition()); + if (newPos != glm::vec3()) { + setPosition(newPos); + } + glm::quat rotation; if (_isFacingAvatar) { // LOL, quaternions are hard. - // rotate about vertical to face the camera // glm::vec3 dPos = getPosition() - args->_viewFrustum->getPosition(); // dPos = glm::normalize(dPos); // rotation = glm::quat(0, dPos.x, dPos.y, dPos.z); - rotation = args->_viewFrustum->getOrientation(); - rotation *= glm::angleAxis(glm::pi(), IDENTITY_UP); // float horizontal = glm::sqrt(dPos.x * dPos.x + dPos.y + dPos.y); // glm::vec3 zAxis = glm::vec3(0, 0, 1); // rotation = rotationBetween(zAxis, dPos); @@ -70,9 +79,24 @@ void BillboardOverlay::render(RenderArgs* args) { // rotation = yawQuat * pitchQuat; // glm::vec3 pitch = glm::vec3(dPos.x, dPos.y, 0); // rotation = glm::quat(glm::vec3(pitch, yaw, 0)); + // rotate about vertical to be perpendicular to the camera + rotation = args->_viewFrustum->getOrientation(); + rotation *= glm::angleAxis(glm::pi(), IDENTITY_UP); rotation *= getRotation(); } else { rotation = getRotation(); + if (getAttachedPanel()) { + rotation *= getAttachedPanel()->getOffsetRotation() * + getAttachedPanel()->getFacingRotation(); +// if (getAttachedPanel()->getFacingRotation() != glm::quat(0, 0, 0, 0)) { +// rotation *= getAttachedPanel()->getFacingRotation(); +// } else if (getAttachedPanel()->getOffsetRotation() != glm::quat(0, 0, 0, 0)) { +// rotation *= getAttachedPanel()->getOffsetRotation(); +// } else { +// rotation *= Application::getInstance()->getCamera()->getOrientation() * +// glm::quat(0, 0, 1, 0); +// } + } } float imageWidth = _texture->getWidth(); @@ -114,7 +138,7 @@ void BillboardOverlay::render(RenderArgs* args) { Transform transform = _transform; transform.postScale(glm::vec3(getDimensions(), 1.0f)); transform.setRotation(rotation); - + batch->setModelTransform(transform); batch->setResourceTexture(0, _texture->getGPUTexture()); @@ -171,6 +195,21 @@ void BillboardOverlay::setProperties(const QScriptValue &properties) { if (isFacingAvatarValue.isValid()) { _isFacingAvatar = isFacingAvatarValue.toVariant().toBool(); } + + QScriptValue offsetPosition = properties.property("offsetPosition"); + if (offsetPosition.isValid()) { + QScriptValue x = offsetPosition.property("x"); + QScriptValue y = offsetPosition.property("y"); + QScriptValue z = offsetPosition.property("z"); + + if (x.isValid() && y.isValid() && z.isValid()) { + glm::vec3 newPosition; + newPosition.x = x.toVariant().toFloat(); + newPosition.y = y.toVariant().toFloat(); + newPosition.z = z.toVariant().toFloat(); + setOffsetPosition(newPosition); + } + } } QScriptValue BillboardOverlay::getProperty(const QString& property) { @@ -183,6 +222,9 @@ QScriptValue BillboardOverlay::getProperty(const QString& property) { if (property == "isFacingAvatar") { return _isFacingAvatar; } + if (property == "offsetPosition") { + return vec3toScriptValue(_scriptEngine, getOffsetPosition()); + } return Planar3DOverlay::getProperty(property); } diff --git a/interface/src/ui/overlays/BillboardOverlay.h b/interface/src/ui/overlays/BillboardOverlay.h index 15be0419a9..f7bbfd1817 100644 --- a/interface/src/ui/overlays/BillboardOverlay.h +++ b/interface/src/ui/overlays/BillboardOverlay.h @@ -15,8 +15,9 @@ #include #include "Planar3DOverlay.h" +#include "PanelAttachable.h" -class BillboardOverlay : public Planar3DOverlay { +class BillboardOverlay : public Planar3DOverlay, public PanelAttachable { Q_OBJECT public: BillboardOverlay(); @@ -24,6 +25,8 @@ public: virtual void render(RenderArgs* args); + virtual void update(float deltatime); + // setters void setURL(const QString& url); void setIsFacingAvatar(bool isFacingAvatar) { _isFacingAvatar = isFacingAvatar; } diff --git a/interface/src/ui/overlays/FloatingUIPanel.cpp b/interface/src/ui/overlays/FloatingUIPanel.cpp new file mode 100644 index 0000000000..e655a75f07 --- /dev/null +++ b/interface/src/ui/overlays/FloatingUIPanel.cpp @@ -0,0 +1,89 @@ +// +// FloatingUIPanel.cpp +// interface/src/ui/overlays +// +// Created by Zander Otavka on 7/2/15. +// 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 "FloatingUIPanel.h" + +#include +#include + +#include "Application.h" + + +glm::quat FloatingUIPanel::getOffsetRotation() const { + if (getActualOffsetRotation() == glm::quat(0, 0, 0, 0)) { + return Application::getInstance()->getCamera()->getOrientation() * glm::quat(0, 0, 1, 0); + } + return getActualOffsetRotation(); +} + +QScriptValue FloatingUIPanel::getProperty(const QString &property) { + if (property == "offsetPosition") { + return vec3toScriptValue(_scriptEngine, getOffsetPosition()); + } + if (property == "offsetRotation") { + return quatToScriptValue(_scriptEngine, getActualOffsetRotation()); + } + if (property == "facingRotation") { + return quatToScriptValue(_scriptEngine, getFacingRotation()); + } + + return QScriptValue(); +} + +void FloatingUIPanel::setProperties(const QScriptValue &properties) { + QScriptValue offsetPosition = properties.property("offsetPosition"); + if (offsetPosition.isValid()) { + QScriptValue x = offsetPosition.property("x"); + QScriptValue y = offsetPosition.property("y"); + QScriptValue z = offsetPosition.property("z"); + if (x.isValid() && y.isValid() && z.isValid()) { + glm::vec3 newPosition; + newPosition.x = x.toVariant().toFloat(); + newPosition.y = y.toVariant().toFloat(); + newPosition.z = z.toVariant().toFloat(); + setOffsetPosition(newPosition); + } + } + + QScriptValue offsetRotation = properties.property("offsetRotation"); + if (offsetRotation.isValid()) { + QScriptValue x = offsetRotation.property("x"); + QScriptValue y = offsetRotation.property("y"); + QScriptValue z = offsetRotation.property("z"); + QScriptValue w = offsetRotation.property("w"); + + if (x.isValid() && y.isValid() && z.isValid() && w.isValid()) { + glm::quat newRotation; + newRotation.x = x.toVariant().toFloat(); + newRotation.y = y.toVariant().toFloat(); + newRotation.z = z.toVariant().toFloat(); + newRotation.w = w.toVariant().toFloat(); + setOffsetRotation(newRotation); + } + } + + QScriptValue facingRotation = properties.property("facingRotation"); + if (offsetRotation.isValid()) { + QScriptValue x = facingRotation.property("x"); + QScriptValue y = facingRotation.property("y"); + QScriptValue z = facingRotation.property("z"); + QScriptValue w = facingRotation.property("w"); + + if (x.isValid() && y.isValid() && z.isValid() && w.isValid()) { + glm::quat newRotation; + newRotation.x = x.toVariant().toFloat(); + newRotation.y = y.toVariant().toFloat(); + newRotation.z = z.toVariant().toFloat(); + newRotation.w = w.toVariant().toFloat(); + setFacingRotation(newRotation); + } + } +} diff --git a/interface/src/ui/overlays/FloatingUIPanel.h b/interface/src/ui/overlays/FloatingUIPanel.h new file mode 100644 index 0000000000..7f8d42eb5b --- /dev/null +++ b/interface/src/ui/overlays/FloatingUIPanel.h @@ -0,0 +1,47 @@ +// +// FloatingUIPanel.h +// interface/src/ui/overlays +// +// Created by Zander Otavka on 7/2/15. +// 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_FloatingUIPanel_h +#define hifi_FloatingUIPanel_h + +#include +#include +#include + +class FloatingUIPanel : public QObject { + Q_OBJECT +public: + typedef std::shared_ptr Pointer; + + QList children; + + void init(QScriptEngine* scriptEngine) { _scriptEngine = scriptEngine; } + + glm::vec3 getOffsetPosition() const { return _offsetPosition; } + glm::quat getOffsetRotation() const; + glm::quat getActualOffsetRotation() const { return _offsetRotation; } + glm::quat getFacingRotation() const { return _facingRotation; } + + void setOffsetPosition(glm::vec3 position) { _offsetPosition = position; }; + void setOffsetRotation(glm::quat rotation) { _offsetRotation = rotation; }; + void setFacingRotation(glm::quat rotation) { _facingRotation = rotation; }; + + QScriptValue getProperty(const QString& property); + void setProperties(const QScriptValue& properties); + +private: + glm::vec3 _offsetPosition = glm::vec3(0, 0, 0); + glm::quat _offsetRotation = glm::quat(0, 0, 0, 0); + glm::quat _facingRotation = glm::quat(1, 0, 0, 0); + QScriptEngine* _scriptEngine; +}; + +#endif // hifi_FloatingUIPanel_h diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index ff218db844..fe489847ee 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -48,6 +48,7 @@ Overlays::~Overlays() { } _overlaysHUD.clear(); _overlaysWorld.clear(); + _panels.clear(); } cleanupOverlaysToDelete(); @@ -124,11 +125,36 @@ void Overlays::renderHUD(RenderArgs* renderArgs) { } } +Overlay::Pointer Overlays::getOverlay(unsigned int id) const { + if (_overlaysHUD.contains(id)) { + return _overlaysHUD[id]; + } + if (_overlaysWorld.contains(id)) { + return _overlaysWorld[id]; + } + return nullptr; +} + +void Overlays::setAttachedPanel(Overlay* overlay, unsigned int overlayId, const QScriptValue& property) { + if (PanelAttachable* attachable = dynamic_cast(overlay)) { + if (property.isValid()) { + unsigned int attachedPanelId = property.toVariant().toUInt(); + FloatingUIPanel* panel = nullptr; + if (_panels.contains(attachedPanelId)) { + panel = _panels[attachedPanelId].get(); + panel->children.append(overlayId); + attachable->setAttachedPanel(panel); + } else { + attachable->getAttachedPanel()->children.removeAll(overlayId); + attachable->setAttachedPanel(nullptr); + } + } + } +} + unsigned int Overlays::addOverlay(const QString& type, const QScriptValue& properties) { - unsigned int thisID = 0; Overlay* thisOverlay = NULL; - - bool created = true; + if (type == "image") { thisOverlay = new ImageOverlay(); } else if (type == "text") { @@ -153,16 +179,15 @@ unsigned int Overlays::addOverlay(const QString& type, const QScriptValue& prope thisOverlay = new ModelOverlay(); } else if (type == "billboard") { thisOverlay = new BillboardOverlay(); - } else { - created = false; } - if (created) { + if (thisOverlay) { thisOverlay->setProperties(properties); - thisID = addOverlay(thisOverlay); + unsigned int overlayId = addOverlay(thisOverlay); + setAttachedPanel(thisOverlay, overlayId, properties.property("attachedPanel")); + return overlayId; } - - return thisID; + return 0; } unsigned int Overlays::addOverlay(Overlay* overlay) { @@ -189,17 +214,12 @@ unsigned int Overlays::addOverlay(Overlay* overlay) { } else { _overlaysHUD[thisID] = overlayPointer; } - + return thisID; } unsigned int Overlays::cloneOverlay(unsigned int id) { - Overlay::Pointer thisOverlay = NULL; - if (_overlaysHUD.contains(id)) { - thisOverlay = _overlaysHUD[id]; - } else if (_overlaysWorld.contains(id)) { - thisOverlay = _overlaysWorld[id]; - } + Overlay::Pointer thisOverlay = getOverlay(id); if (thisOverlay) { return addOverlay(thisOverlay->createClone()); @@ -210,14 +230,8 @@ unsigned int Overlays::cloneOverlay(unsigned int id) { bool Overlays::editOverlay(unsigned int id, const QScriptValue& properties) { QWriteLocker lock(&_lock); - Overlay::Pointer thisOverlay; - - if (_overlaysHUD.contains(id)) { - thisOverlay = _overlaysHUD[id]; - } else if (_overlaysWorld.contains(id)) { - thisOverlay = _overlaysWorld[id]; - } + Overlay::Pointer thisOverlay = getOverlay(id); if (thisOverlay) { if (thisOverlay->is3D()) { auto overlay3D = std::static_pointer_cast(thisOverlay); @@ -239,6 +253,8 @@ bool Overlays::editOverlay(unsigned int id, const QScriptValue& properties) { thisOverlay->setProperties(properties); } + setAttachedPanel(thisOverlay.get(), id, properties.property("attachedPanel")); + return true; } return false; @@ -302,15 +318,18 @@ unsigned int Overlays::getOverlayAtPoint(const glm::vec2& point) { OverlayPropertyResult Overlays::getProperty(unsigned int id, const QString& property) { OverlayPropertyResult result; - Overlay::Pointer thisOverlay; + Overlay::Pointer thisOverlay = getOverlay(id); QReadLocker lock(&_lock); - if (_overlaysHUD.contains(id)) { - thisOverlay = _overlaysHUD[id]; - } else if (_overlaysWorld.contains(id)) { - thisOverlay = _overlaysWorld[id]; - } if (thisOverlay) { - result.value = thisOverlay->getProperty(property); + if (property == "attachedPanel") { + if (FloatingUIPanel* panel = dynamic_cast(thisOverlay.get())) { + result.value = _panels.key(FloatingUIPanel::Pointer(panel)); + } else { + result.value = 0; + } + } else { + result.value = thisOverlay->getProperty(property); + } } return result; } @@ -456,12 +475,8 @@ void RayToOverlayIntersectionResultFromScriptValue(const QScriptValue& object, R bool Overlays::isLoaded(unsigned int id) { QReadLocker lock(&_lock); - Overlay::Pointer thisOverlay = NULL; - if (_overlaysHUD.contains(id)) { - thisOverlay = _overlaysHUD[id]; - } else if (_overlaysWorld.contains(id)) { - thisOverlay = _overlaysWorld[id]; - } else { + Overlay::Pointer thisOverlay = getOverlay(id); + if (!thisOverlay) { return false; // not found } return thisOverlay->isLoaded(); @@ -483,3 +498,55 @@ QSizeF Overlays::textSize(unsigned int id, const QString& text) const { } return QSizeF(0.0f, 0.0f); } + +unsigned int Overlays::addPanel(FloatingUIPanel* panel) { + QWriteLocker lock(&_lock); + + FloatingUIPanel::Pointer panelPointer(panel); + unsigned int thisID = _nextOverlayID; + _nextOverlayID++; + _panels[thisID] = panelPointer; + + return thisID; +} + +unsigned int Overlays::addPanel(const QScriptValue& properties) { + FloatingUIPanel* panel = new FloatingUIPanel(); + panel->init(_scriptEngine); + panel->setProperties(properties); + return addPanel(panel); +} + +void Overlays::editPanel(unsigned int panelId, const QScriptValue& properties) { + if (_panels.contains(panelId)) { + _panels[panelId]->setProperties(properties); + } +} + +OverlayPropertyResult Overlays::getPanelProperty(unsigned int panelId, const QString& property) { + OverlayPropertyResult result; + if (_panels.contains(panelId)) { + FloatingUIPanel::Pointer thisPanel = _panels[panelId]; + QReadLocker lock(&_lock); + result.value = thisPanel->getProperty(property); + } + return result; +} + + +void Overlays::deletePanel(unsigned int panelId) { + FloatingUIPanel::Pointer panelToDelete; + + { + QWriteLocker lock(&_lock); + if (_panels.contains(panelId)) { + panelToDelete = _panels.take(panelId); + } else { + return; + } + } + + while (!panelToDelete->children.isEmpty()) { + deleteOverlay(panelToDelete->children.takeLast()); + } +} diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index cd5b0f1d10..2ba2ec8f45 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -16,6 +16,9 @@ #include "Overlay.h" +#include "FloatingUIPanel.h" +#include "PanelAttachable.h" + class PickRay; class OverlayPropertyResult { @@ -90,12 +93,33 @@ public slots: /// overlay; in meters if it is a 3D text overlay QSizeF textSize(unsigned int id, const QString& text) const; + + /// adds a panel that has already been created + unsigned int addPanel(FloatingUIPanel* panel); + + /// creates and adds a panel based on a set of properties + unsigned int addPanel(const QScriptValue& properties); + + /// edit the properties of a panel + void editPanel(unsigned int panelId, const QScriptValue& properties); + + /// get a property of a panel + OverlayPropertyResult getPanelProperty(unsigned int panelId, const QString& property); + + /// deletes a panel and all child overlays + void deletePanel(unsigned int panelId); + private: void cleanupOverlaysToDelete(); + Overlay::Pointer getOverlay(unsigned int id) const; + void setAttachedPanel(Overlay* overlay, unsigned int overlayId, const QScriptValue& property); + QMap _overlaysHUD; QMap _overlaysWorld; + QMap _panels; QList _overlaysToDelete; unsigned int _nextOverlayID; + QReadWriteLock _lock; QReadWriteLock _deleteLock; QScriptEngine* _scriptEngine; diff --git a/interface/src/ui/overlays/PanelAttachable.h b/interface/src/ui/overlays/PanelAttachable.h new file mode 100644 index 0000000000..c364d62fa7 --- /dev/null +++ b/interface/src/ui/overlays/PanelAttachable.h @@ -0,0 +1,44 @@ +// +// PanelAttachable.h +// interface/src/ui/overlays +// +// Created by Zander Otavka on 7/1/15. +// 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_PanelAttachable_h +#define hifi_PanelAttachable_h + +#include "FloatingUIPanel.h" + +#include + +class PanelAttachable { +public: + glm::vec3 getOffsetPosition() const { return _offsetPosition; } + void setOffsetPosition(glm::vec3 position) { _offsetPosition = position; } + + FloatingUIPanel* getAttachedPanel() const { return _attachedPanel; } + void setAttachedPanel(FloatingUIPanel* panel) { _attachedPanel = panel; } + + glm::vec3 getTranslatedPosition(glm::vec3 avatarPosition) { + if (getAttachedPanel()) { + glm::vec3 totalOffsetPosition = + getAttachedPanel()->getFacingRotation() * getOffsetPosition() + + getAttachedPanel()->getOffsetPosition(); + + return getAttachedPanel()->getOffsetRotation() * totalOffsetPosition + + avatarPosition; + } + return glm::vec3(); + } + +private: + FloatingUIPanel* _attachedPanel = nullptr; + glm::vec3 _offsetPosition = glm::vec3(0, 0, 0); +}; + +#endif // hifi_PanelAttachable_h From ed7fc07ab1140c2db7f7fdd22e060e4db27cb595 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Wed, 15 Jul 2015 14:08:09 -0700 Subject: [PATCH 196/242] Fix cloning bug for BillboardOverlay. --- .../src/ui/overlays/BillboardOverlay.cpp | 1 + interface/src/ui/overlays/PanelAttachable.cpp | 33 +++++++++++++++++++ interface/src/ui/overlays/PanelAttachable.h | 17 +++------- interface/src/ui/overlays/Planar3DOverlay.cpp | 9 ++++- interface/src/ui/overlays/Planar3DOverlay.h | 4 +-- 5 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 interface/src/ui/overlays/PanelAttachable.cpp diff --git a/interface/src/ui/overlays/BillboardOverlay.cpp b/interface/src/ui/overlays/BillboardOverlay.cpp index 9e1a9a44de..18d0307743 100644 --- a/interface/src/ui/overlays/BillboardOverlay.cpp +++ b/interface/src/ui/overlays/BillboardOverlay.cpp @@ -29,6 +29,7 @@ BillboardOverlay::BillboardOverlay() { BillboardOverlay::BillboardOverlay(const BillboardOverlay* billboardOverlay) : Planar3DOverlay(billboardOverlay), + PanelAttachable(billboardOverlay), _url(billboardOverlay->_url), _texture(billboardOverlay->_texture), _fromImage(billboardOverlay->_fromImage), diff --git a/interface/src/ui/overlays/PanelAttachable.cpp b/interface/src/ui/overlays/PanelAttachable.cpp new file mode 100644 index 0000000000..172880db97 --- /dev/null +++ b/interface/src/ui/overlays/PanelAttachable.cpp @@ -0,0 +1,33 @@ +// +// PanelAttachable.cpp +// hifi +// +// Created by Zander Otavka on 7/15/15. +// +// + +#include "PanelAttachable.h" + +PanelAttachable::PanelAttachable() : + _attachedPanel(nullptr), + _offsetPosition(glm::vec3()) +{ +} + +PanelAttachable::PanelAttachable(const PanelAttachable* panelAttachable) : + _attachedPanel(panelAttachable->_attachedPanel), + _offsetPosition(panelAttachable->_offsetPosition) +{ +} + +glm::vec3 PanelAttachable::getTranslatedPosition(glm::vec3 avatarPosition) const { + if (getAttachedPanel()) { + glm::vec3 totalOffsetPosition = + getAttachedPanel()->getFacingRotation() * getOffsetPosition() + + getAttachedPanel()->getOffsetPosition(); + + return getAttachedPanel()->getOffsetRotation() * totalOffsetPosition + + avatarPosition; + } + return glm::vec3(); +} diff --git a/interface/src/ui/overlays/PanelAttachable.h b/interface/src/ui/overlays/PanelAttachable.h index c364d62fa7..29b0673157 100644 --- a/interface/src/ui/overlays/PanelAttachable.h +++ b/interface/src/ui/overlays/PanelAttachable.h @@ -18,26 +18,19 @@ class PanelAttachable { public: + PanelAttachable(); + PanelAttachable(const PanelAttachable* panelAttachable); + glm::vec3 getOffsetPosition() const { return _offsetPosition; } void setOffsetPosition(glm::vec3 position) { _offsetPosition = position; } FloatingUIPanel* getAttachedPanel() const { return _attachedPanel; } void setAttachedPanel(FloatingUIPanel* panel) { _attachedPanel = panel; } - glm::vec3 getTranslatedPosition(glm::vec3 avatarPosition) { - if (getAttachedPanel()) { - glm::vec3 totalOffsetPosition = - getAttachedPanel()->getFacingRotation() * getOffsetPosition() + - getAttachedPanel()->getOffsetPosition(); - - return getAttachedPanel()->getOffsetRotation() * totalOffsetPosition + - avatarPosition; - } - return glm::vec3(); - } + glm::vec3 getTranslatedPosition(glm::vec3 avatarPosition) const; private: - FloatingUIPanel* _attachedPanel = nullptr; + FloatingUIPanel* _attachedPanel; glm::vec3 _offsetPosition = glm::vec3(0, 0, 0); }; diff --git a/interface/src/ui/overlays/Planar3DOverlay.cpp b/interface/src/ui/overlays/Planar3DOverlay.cpp index 0ca092ba23..354991b596 100644 --- a/interface/src/ui/overlays/Planar3DOverlay.cpp +++ b/interface/src/ui/overlays/Planar3DOverlay.cpp @@ -14,8 +14,15 @@ #include #include +Planar3DOverlay::Planar3DOverlay() : + Base3DOverlay(), + _dimensions{1.0f, 1.0f} +{ +} + Planar3DOverlay::Planar3DOverlay(const Planar3DOverlay* planar3DOverlay) : - Base3DOverlay(planar3DOverlay) + Base3DOverlay(planar3DOverlay), + _dimensions(planar3DOverlay->_dimensions) { } diff --git a/interface/src/ui/overlays/Planar3DOverlay.h b/interface/src/ui/overlays/Planar3DOverlay.h index fe8c513efd..08a7121e91 100644 --- a/interface/src/ui/overlays/Planar3DOverlay.h +++ b/interface/src/ui/overlays/Planar3DOverlay.h @@ -17,7 +17,7 @@ class Planar3DOverlay : public Base3DOverlay { Q_OBJECT public: - Planar3DOverlay() {} + Planar3DOverlay(); Planar3DOverlay(const Planar3DOverlay* planar3DOverlay); AABox getBounds() const; @@ -32,7 +32,7 @@ public: virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face); protected: - glm::vec2 _dimensions{1.0f, 1.0f}; + glm::vec2 _dimensions; }; From 377a1a54aeaa8e9343325b7b589b056d6a8d242d Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Wed, 15 Jul 2015 15:32:14 -0700 Subject: [PATCH 197/242] Add abstraction layer for Overlays.h. `examples/libraries/overlayUtils.js` allows you to manage overlays in an object oriented manner. Instead of: var billboard = Overlays.addOverlay("billboard", { visible: false }); ... Overlays.editOverlay(billboard, { visible: true }); ... Overlays.deleteOverlay(billboard); You can now do: var billboard = new BillboardOverlay({ visible: false }); ... billboard.visible = true; ... billboard.destroy(); --- examples/example/ui/floatingUIExample.js | 98 ++++----- examples/libraries/overlayUtils.js | 257 ++++++++++++++++++++++- interface/src/ui/overlays/Overlays.h | 6 + 3 files changed, 298 insertions(+), 63 deletions(-) diff --git a/examples/example/ui/floatingUIExample.js b/examples/example/ui/floatingUIExample.js index 09deca4ec7..3b555efce3 100644 --- a/examples/example/ui/floatingUIExample.js +++ b/examples/example/ui/floatingUIExample.js @@ -9,7 +9,10 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -Script.include(["../../libraries/globals.js"]); +Script.include([ + "../../libraries/globals.js", + "../../libraries/overlayUtils.js", +]); var BG_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/card-bg.svg"; var RED_DOT_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/red-dot.svg"; @@ -24,13 +27,11 @@ function isBlank(rotation) { rotation.w == BLANK_ROTATION.w; } -var panel = Overlays.addPanel({ +var panel = new FloatingUIPanel({ offsetPosition: { x: 0, y: 0, z: 1 }, }); -var panelChildren = []; - -var bg = Overlays.addOverlay("billboard", { +var bg = panel.addChild(new BillboardOverlay({ url: BG_IMAGE_URL, dimensions: { x: 0.5, @@ -40,11 +41,9 @@ var bg = Overlays.addOverlay("billboard", { visible: true, alpha: 1.0, ignoreRayIntersection: false, - attachedPanel: panel, -}); -panelChildren.push(bg); +})); -var redDot = Overlays.addOverlay("billboard", { +var redDot = panel.addChild(new BillboardOverlay({ url: RED_DOT_IMAGE_URL, dimensions: { x: 0.1, @@ -54,16 +53,14 @@ var redDot = Overlays.addOverlay("billboard", { visible: true, alpha: 1.0, ignoreRayIntersection: false, - attachedPanel: panel, offsetPosition: { x: -0.15, y: -0.15, z: -0.001 } -}); -panelChildren.push(redDot); +})); -var redDot2 = Overlays.addOverlay("billboard", { +var redDot2 = panel.addChild(new BillboardOverlay({ url: RED_DOT_IMAGE_URL, dimensions: { x: 0.1, @@ -73,16 +70,14 @@ var redDot2 = Overlays.addOverlay("billboard", { visible: true, alpha: 1.0, ignoreRayIntersection: false, - attachedPanel: panel, offsetPosition: { x: -0.15, y: 0, z: -0.001 } -}); -panelChildren.push(redDot2); +})); -var blueSquare = Overlays.addOverlay("billboard", { +var blueSquare = panel.addChild(new BillboardOverlay({ url: BLUE_SQUARE_IMAGE_URL, dimensions: { x: 0.1, @@ -92,16 +87,14 @@ var blueSquare = Overlays.addOverlay("billboard", { visible: true, alpha: 1.0, ignoreRayIntersection: false, - attachedPanel: panel, offsetPosition: { x: 0.1, y: 0, z: -0.001 } -}); -panelChildren.push(blueSquare); +})); -var blueSquare2 = Overlays.addOverlay("billboard", { +var blueSquare2 = panel.addChild(new BillboardOverlay({ url: BLUE_SQUARE_IMAGE_URL, dimensions: { x: 0.1, @@ -111,63 +104,46 @@ var blueSquare2 = Overlays.addOverlay("billboard", { visible: true, alpha: 1.0, ignoreRayIntersection: false, - attachedPanel: panel, offsetPosition: { x: 0.1, y: 0.11, z: -0.001 } -}); -panelChildren.push(blueSquare2); +})); -var blueSquare3 = Overlays.addOverlay("billboard", { - url: BLUE_SQUARE_IMAGE_URL, - dimensions: { - x: 0.1, - y: 0.1, - }, - isFacingAvatar: false, - visible: true, - alpha: 1.0, - ignoreRayIntersection: false, - attachedPanel: panel, - offsetPosition: { - x: -0.01, - y: 0.11, - z: -0.001 - } -}); -panelChildren.push(blueSquare3); +var blueSquare3 = blueSquare2.clone(); +blueSquare3.offsetPosition = { + x: -0.01, + y: 0.11, + z: -0.001 +}; +blueSquare3.ignoreRayIntersection = false; Controller.mousePressEvent.connect(function(event) { if (event.isRightButton) { - var newOffsetRotation = BLANK_ROTATION; - if (isBlank(Overlays.getPanelProperty(panel, "offsetRotation"))) { + var newOffsetRotation; + print(JSON.stringify(panel.offsetRotation)) + if (isBlank(panel.offsetRotation)) { newOffsetRotation = Quat.multiply(MyAvatar.orientation, { x: 0, y: 1, z: 0, w: 0 }); + } else { + newOffsetRotation = BLANK_ROTATION; } - Overlays.editPanel(panel, { - offsetRotation: newOffsetRotation - }); + panel.offsetRotation = newOffsetRotation; } else if (event.isLeftButton) { var pickRay = Camera.computePickRay(event.x, event.y) - var rayPickResult = Overlays.findRayIntersection(pickRay); - print(String(rayPickResult.overlayID)); - if (rayPickResult.intersects) { - for (var i in panelChildren) { - if (panelChildren[i] == rayPickResult.overlayID) { - var oldPos = Overlays.getProperty(rayPickResult.overlayID, "offsetPosition"); - var newPos = { - x: Number(oldPos.x), - y: Number(oldPos.y), - z: Number(oldPos.z) + 0.1 - } - Overlays.editOverlay(rayPickResult.overlayID, { offsetPosition: newPos }); - } + var overlay = panel.findRayIntersection(pickRay); + if (overlay) { + var oldPos = overlay.offsetPosition; + var newPos = { + x: Number(oldPos.x), + y: Number(oldPos.y), + z: Number(oldPos.z) + 0.1 } + overlay.offsetPosition = newPos; } } }); Script.scriptEnding.connect(function() { - Overlays.deletePanel(panel); + panel.destroy(); }); \ No newline at end of file diff --git a/examples/libraries/overlayUtils.js b/examples/libraries/overlayUtils.js index 636ea40825..3acab0103b 100644 --- a/examples/libraries/overlayUtils.js +++ b/examples/libraries/overlayUtils.js @@ -1,6 +1,39 @@ +// +// overlayUtils.js +// examples/libraries +// +// Modified by Zander Otavka on 7/15/15 +// Copyright 2014 High Fidelity, Inc. +// +// Manage overlays with object oriented goodness, instead of ugly `Overlays.h` methods. +// Instead of: +// +// var billboard = Overlays.addOverlay("billboard", { visible: false }); +// ... +// Overlays.editOverlay(billboard, { visible: true }); +// ... +// Overlays.deleteOverlay(billboard); +// +// You can now do: +// +// var billboard = new BillboardOverlay({ visible: false }); +// ... +// billboard.visible = true; +// ... +// billboard.destroy(); +// +// See more on usage below. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + /** - * OverlayGroup provides a way to create composite overlays and control their - * position relative to a settable rootPosition and rootRotation. + * DEPRECATION WARNING: Will be deprecated soon in favor of FloatingUIPanel. + * + * OverlayGroup provides a way to create composite overlays and control their + * position relative to a settable rootPosition and rootRotation. */ OverlayGroup = function(opts) { var that = {}; @@ -62,3 +95,223 @@ OverlayGroup = function(opts) { return that; }; + + +/** + * Object oriented abstraction layer for overlays. + * + * Usage: + * // Create an overlay + * var billboard = new BillboardOverlay({ + * visible: true, + * isFacingAvatar: true, + * ignoreRayIntersections: false + * }); + * + * // Get a property + * var isVisible = billboard.visible; + * + * // Set a single property + * billboard.position = { x: 1, y: 3, z: 2 }; + * + * // Set multiple properties at the same time + * billboard.setProperties({ + * url: "http://images.com/overlayImage.jpg", + * dimensions: { x: 2, y: 2 } + * }); + * + * // Clone an overlay + * var clonedBillboard = billboard.clone(); + * + * // Remove an overlay from the world + * billboard.destroy(); + * + * // Remember, there is a poor orphaned JavaScript object left behind. You should remove any + * // references to it so you don't accidentally try to modify an overlay that isn't there. + * billboard = undefined; + */ +(function() { + var ABSTRACT = null; + + function generateOverlayClass(superclass, type, properties) { + var that; + if (type == ABSTRACT) { + that = function(type, params) { + superclass.apply(this, [type, params]); + }; + } else { + that = function(params) { + superclass.apply(this, [type, params]); + }; + } + + that.prototype = new superclass(); + that.prototype.constructor = that; + + properties.forEach(function(prop) { + Object.defineProperty(that.prototype, prop, { + get: function() { + return Overlays.getProperty(this._id, prop); + }, + set: function(newValue) { + var keyValuePair = {}; + keyValuePair[prop] = newValue; + this.setProperties(keyValuePair); + }, + configurable: true + }); + }); + + return that; + } + + + // Supports multiple inheritance of properties. Just `concat` them onto the end of the + // properties list. + var PANEL_ATTACHABLE_FIELDS = ["attachedPanel"]; + + // TODO: finish exposing all overlay classes. + + var Overlay = (function() { + var BaseOverlay = (function() { + var that = function(type, params) { + Object.apply(this, []); + if (type && params) { + this._type = type; + this._id = Overlays.addOverlay(type, params); + } else { + this._type = ""; + this._id = 0; + } + this._attachedPanelPointer = null; + }; + + that.prototype = new Object(); + that.prototype.constructor = that; + + Object.defineProperty(that.prototype, "overlayType", { + get: function() { + return this._type; + } + }); + + that.prototype.setProperties = function(properties) { + Overlays.editOverlay(this._id, properties); + }; + + that.prototype.clone = function() { + var clone = new this.constructor(); + clone._type = this._type; + clone._id = Overlays.cloneOverlay(this._id); + if (this._attachedPanelPointer) { + this._attachedPanelPointer.addChild(clone); + } + return clone; + }; + + that.prototype.destroy = function() { + Overlays.deleteOverlay(this._id); + }; + + return that; + }()); + + return generateOverlayClass(BaseOverlay, ABSTRACT, [ + "alpha", "glowLevel", "pulseMax", "pulseMin", "pulsePeriod", "glowLevelPulse", + "alphaPulse", "colorPulse", "visible", "anchor" + ]); + }()); + + var Base3DOverlay = generateOverlayClass(Overlay, ABSTRACT, [ + "position", "lineWidth", "rotation", "isSolid", "isFilled", "isWire", "isDashedLine", + "ignoreRayIntersection", "drawInFront", "drawOnHUD" + ]); + + var Planar3DOverlay = generateOverlayClass(Base3DOverlay, ABSTRACT, [ + "dimensions" + ]); + + BillboardOverlay = generateOverlayClass(Planar3DOverlay, "billboard", [ + "url", "subImage", "isFacingAvatar", "offsetPosition" + ].concat(PANEL_ATTACHABLE_FIELDS)); +}()); + + +/** + * Object oriented abstraction layer for panels. + */ +FloatingUIPanel = (function() { + var that = function(params) { + this._id = Overlays.addPanel(params); + this._children = []; + }; + + var FIELDS = ["offsetPosition", "offsetRotation", "facingRotation"]; + FIELDS.forEach(function(prop) { + Object.defineProperty(that.prototype, prop, { + get: function() { + return Overlays.getPanelProperty(this._id, prop); + }, + set: function(newValue) { + var keyValuePair = {}; + keyValuePair[prop] = newValue; + this.setProperties(keyValuePair); + }, + configurable: false + }); + }); + + Object.defineProperty(that.prototype, "children", { + get: function() { + return this._children.slice(); + } + }) + + that.prototype.addChild = function(overlay) { + overlay.attachedPanel = this._id; + overlay._attachedPanelPointer = this; + this._children.push(overlay); + return overlay; + }; + + that.prototype.removeChild = function(overlay) { + var i = this._children.indexOf(overlay); + if (i >= 0) { + overlay.attachedPanel = 0; + overlay._attachedPanelPointer = null; + this._children.splice(i, 1); + } + }; + + that.prototype.setVisible = function(visible) { + for (var i in this._children) { + this._children[i].visible = visible; + } + }; + + that.prototype.setProperties = function(properties) { + Overlays.editPanel(this._id, properties); + }; + + that.prototype.destroy = function() { + Overlays.deletePanel(this._id); + var i = _panels.indexOf(this); + if (i >= 0) { + _panels.splice(i, 1); + } + }; + + that.prototype.findRayIntersection = function(pickRay) { + var rayPickResult = Overlays.findRayIntersection(pickRay); + if (rayPickResult.intersects) { + for (var i in this._children) { + if (this._children[i]._id == rayPickResult.overlayID) { + return this._children[i]; + } + } + } + return null; + }; + + return that; +}()); diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index 2ba2ec8f45..80da2e6db1 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -2,8 +2,14 @@ // Overlays.h // interface/src/ui/overlays // +// Modified by Zander Otavka on 7/15/15 // Copyright 2014 High Fidelity, Inc. // +// Exposes methods for managing `Overlay`s and `FloatingUIPanel`s to scripts. +// +// YOU SHOULD NOT USE `Overlays` DIRECTLY, unless you like pain and deprecation. Instead, use the +// object oriented abstraction layer found in `examples/libraries/overlayUtils.js`. +// // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // From c4cb6fba744b2eea540918fb755f91ddc0c162bf Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 17 Jul 2015 14:29:53 -0700 Subject: [PATCH 198/242] Improve BillboardOverlay::findRayIntersection. --- .../src/ui/overlays/BillboardOverlay.cpp | 94 ++++++++----------- interface/src/ui/overlays/BillboardOverlay.h | 1 + interface/src/ui/overlays/PanelAttachable.cpp | 17 ++++ interface/src/ui/overlays/PanelAttachable.h | 1 + 4 files changed, 60 insertions(+), 53 deletions(-) diff --git a/interface/src/ui/overlays/BillboardOverlay.cpp b/interface/src/ui/overlays/BillboardOverlay.cpp index 18d0307743..be8fab6362 100644 --- a/interface/src/ui/overlays/BillboardOverlay.cpp +++ b/interface/src/ui/overlays/BillboardOverlay.cpp @@ -38,9 +38,11 @@ BillboardOverlay::BillboardOverlay(const BillboardOverlay* billboardOverlay) : } void BillboardOverlay::update(float deltatime) { - glm::vec3 newPos = getTranslatedPosition(Application::getInstance()->getAvatarPosition()); - if (newPos != glm::vec3()) { - setPosition(newPos); + if (getVisible()) { + glm::vec3 newPos = getTranslatedPosition(Application::getInstance()->getAvatarPosition()); + if (newPos != glm::vec3()) { + setPosition(newPos); + } } } @@ -59,46 +61,7 @@ void BillboardOverlay::render(RenderArgs* args) { setPosition(newPos); } - glm::quat rotation; - if (_isFacingAvatar) { - // LOL, quaternions are hard. -// glm::vec3 dPos = getPosition() - args->_viewFrustum->getPosition(); -// dPos = glm::normalize(dPos); -// rotation = glm::quat(0, dPos.x, dPos.y, dPos.z); -// float horizontal = glm::sqrt(dPos.x * dPos.x + dPos.y + dPos.y); -// glm::vec3 zAxis = glm::vec3(0, 0, 1); -// rotation = rotationBetween(zAxis, dPos); -// glm::vec3 euler = safeEulerAngles(rotationBetween(zAxis, dPos)); -// rotation = glm::quat(glm::vec3(euler.x, euler.y, 0)); -// float yaw = (dPos.x == 0.0f && dPos.z == 0.0f) ? 0.0f : glm::atan(dPos.x, dPos.z); -// glm::quat yawQuat = glm::quat(glm::vec3(0, yaw, 0)); -// float pitch = (dPos.y == 0.0f && horizontal == 0.0f) ? 0.0f : glm::atan(dPos.y, horizontal); -// glm::quat pitchQuat = glm::quat(glm::vec3(pitch, 0, 0)); -// glm::mat4x4 matrix = glm::lookAt(args->_viewFrustum->getPosition(), getPosition(), -// glm::vec3(0, 1, 0)); -// rotation = glm::quat_cast(matrix); -// rotation = yawQuat * pitchQuat; -// glm::vec3 pitch = glm::vec3(dPos.x, dPos.y, 0); -// rotation = glm::quat(glm::vec3(pitch, yaw, 0)); - // rotate about vertical to be perpendicular to the camera - rotation = args->_viewFrustum->getOrientation(); - rotation *= glm::angleAxis(glm::pi(), IDENTITY_UP); - rotation *= getRotation(); - } else { - rotation = getRotation(); - if (getAttachedPanel()) { - rotation *= getAttachedPanel()->getOffsetRotation() * - getAttachedPanel()->getFacingRotation(); -// if (getAttachedPanel()->getFacingRotation() != glm::quat(0, 0, 0, 0)) { -// rotation *= getAttachedPanel()->getFacingRotation(); -// } else if (getAttachedPanel()->getOffsetRotation() != glm::quat(0, 0, 0, 0)) { -// rotation *= getAttachedPanel()->getOffsetRotation(); -// } else { -// rotation *= Application::getInstance()->getCamera()->getOrientation() * -// glm::quat(0, 0, 1, 0); -// } - } - } + glm::quat rotation = calculateRotation(args->_viewFrustum->getOrientation()); float imageWidth = _texture->getWidth(); float imageHeight = _texture->getHeight(); @@ -143,8 +106,10 @@ void BillboardOverlay::render(RenderArgs* args) { batch->setModelTransform(transform); batch->setResourceTexture(0, _texture->getGPUTexture()); - DependencyManager::get()->renderQuad(*batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, - glm::vec4(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha)); + DependencyManager::get()->renderQuad( + *batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, + glm::vec4(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha) + ); batch->setResourceTexture(0, args->_whiteTexture); // restore default white color after me } @@ -239,17 +204,40 @@ void BillboardOverlay::setBillboardURL(const QString& url) { _isLoaded = false; } +glm::quat BillboardOverlay::calculateRotation(glm::quat cameraOrientation) const { + if (_isFacingAvatar) { + // LOL, quaternions are hard. + // glm::vec3 dPos = getPosition() - args->_viewFrustum->getPosition(); + // dPos = glm::normalize(dPos); + // rotation = glm::quat(0, dPos.x, dPos.y, dPos.z); + // float horizontal = glm::sqrt(dPos.x * dPos.x + dPos.y + dPos.y); + // glm::vec3 zAxis = glm::vec3(0, 0, 1); + // rotation = rotationBetween(zAxis, dPos); + // glm::vec3 euler = safeEulerAngles(rotationBetween(zAxis, dPos)); + // rotation = glm::quat(glm::vec3(euler.x, euler.y, 0)); + // float yaw = (dPos.x == 0.0f && dPos.z == 0.0f) ? 0.0f : glm::atan(dPos.x, dPos.z); + // glm::quat yawQuat = glm::quat(glm::vec3(0, yaw, 0)); + // float pitch = (dPos.y == 0.0f && horizontal == 0.0f) ? 0.0f : glm::atan(dPos.y, horizontal); + // glm::quat pitchQuat = glm::quat(glm::vec3(pitch, 0, 0)); + // glm::mat4x4 matrix = glm::lookAt(args->_viewFrustum->getPosition(), getPosition(), + // glm::vec3(0, 1, 0)); + // rotation = glm::quat_cast(matrix); + // rotation = yawQuat * pitchQuat; + // glm::vec3 pitch = glm::vec3(dPos.x, dPos.y, 0); + // rotation = glm::quat(glm::vec3(pitch, yaw, 0)); + // rotate about vertical to be perpendicular to the camera + glm::quat rotation = cameraOrientation; + rotation *= glm::angleAxis(glm::pi(), IDENTITY_UP); + return rotation * getRotation(); + } + return getTranslatedRotation(getRotation()); +} + bool BillboardOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face) { - if (_texture && _texture->isLoaded()) { - glm::quat rotation = getRotation(); - if (_isFacingAvatar) { - // rotate about vertical to face the camera - rotation = Application::getInstance()->getCamera()->getRotation(); - rotation *= glm::angleAxis(glm::pi(), glm::vec3(0.0f, 1.0f, 0.0f)); - } - + glm::quat rotation = + calculateRotation(Application::getInstance()->getCamera()->getRotation()); // Produce the dimensions of the billboard based on the image's aspect ratio and the overlay's scale. bool isNull = _fromImage.isNull(); float width = isNull ? _texture->getWidth() : _fromImage.width(); diff --git a/interface/src/ui/overlays/BillboardOverlay.h b/interface/src/ui/overlays/BillboardOverlay.h index f7bbfd1817..73e9db4c1b 100644 --- a/interface/src/ui/overlays/BillboardOverlay.h +++ b/interface/src/ui/overlays/BillboardOverlay.h @@ -41,6 +41,7 @@ public: private: void setBillboardURL(const QString& url); + glm::quat calculateRotation(glm::quat cameraOrientation) const; QString _url; NetworkTexturePointer _texture; diff --git a/interface/src/ui/overlays/PanelAttachable.cpp b/interface/src/ui/overlays/PanelAttachable.cpp index 172880db97..1beea6b74e 100644 --- a/interface/src/ui/overlays/PanelAttachable.cpp +++ b/interface/src/ui/overlays/PanelAttachable.cpp @@ -31,3 +31,20 @@ glm::vec3 PanelAttachable::getTranslatedPosition(glm::vec3 avatarPosition) const } return glm::vec3(); } + +glm::quat PanelAttachable::getTranslatedRotation(glm::quat offsetRotation) const { + glm::quat rotation = offsetRotation; + if (getAttachedPanel()) { + rotation *= getAttachedPanel()->getOffsetRotation() * + getAttachedPanel()->getFacingRotation(); + // if (getAttachedPanel()->getFacingRotation() != glm::quat(0, 0, 0, 0)) { + // rotation *= getAttachedPanel()->getFacingRotation(); + // } else if (getAttachedPanel()->getOffsetRotation() != glm::quat(0, 0, 0, 0)) { + // rotation *= getAttachedPanel()->getOffsetRotation(); + // } else { + // rotation *= Application::getInstance()->getCamera()->getOrientation() * + // glm::quat(0, 0, 1, 0); + // } + } + return rotation; +} diff --git a/interface/src/ui/overlays/PanelAttachable.h b/interface/src/ui/overlays/PanelAttachable.h index 29b0673157..ab8b8a4276 100644 --- a/interface/src/ui/overlays/PanelAttachable.h +++ b/interface/src/ui/overlays/PanelAttachable.h @@ -28,6 +28,7 @@ public: void setAttachedPanel(FloatingUIPanel* panel) { _attachedPanel = panel; } glm::vec3 getTranslatedPosition(glm::vec3 avatarPosition) const; + glm::quat getTranslatedRotation(glm::quat offsetRotation) const; private: FloatingUIPanel* _attachedPanel; From c77a91eb63f0b5ab7fff08b201b2fb15aac1edf7 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 17 Jul 2015 14:31:38 -0700 Subject: [PATCH 199/242] Expose address bar toggling to scripts. --- interface/src/Application.cpp | 1 + interface/src/Application.h | 4 ++++ .../DialogsManagerScriptingInterface.cpp | 18 +++++++++++++++ .../DialogsManagerScriptingInterface.h | 23 +++++++++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 interface/src/scripting/DialogsManagerScriptingInterface.cpp create mode 100644 interface/src/scripting/DialogsManagerScriptingInterface.h diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 047596e40a..995c49b891 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3758,6 +3758,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("AnimationCache", DependencyManager::get().data()); scriptEngine->registerGlobalObject("SoundCache", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Account", AccountScriptingInterface::getInstance()); + scriptEngine->registerGlobalObject("DialogsManager", _dialogsManagerScriptingInterface); scriptEngine->registerGlobalObject("GlobalServices", GlobalServicesScriptingInterface::getInstance()); qScriptRegisterMetaType(scriptEngine, DownloadInfoResultToScriptValue, DownloadInfoResultFromScriptValue); diff --git a/interface/src/Application.h b/interface/src/Application.h index d1886862d2..56c126462a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -49,6 +49,7 @@ #include "avatar/MyAvatar.h" #include "devices/SixenseManager.h" #include "scripting/ControllerScriptingInterface.h" +#include "scripting/DialogsManagerScriptingInterface.h" #include "scripting/WebWindowClass.h" #include "ui/AudioStatsDialog.h" #include "ui/BandwidthDialog.h" @@ -69,6 +70,7 @@ #include "UndoStackScriptingInterface.h" #include "gpu/Context.h" + #include "render/Engine.h" class QGLWidget; @@ -643,6 +645,8 @@ private: ApplicationOverlay _applicationOverlay; ApplicationCompositor _compositor; int _numFramesSinceLastResize = 0; + + DialogsManagerScriptingInterface* _dialogsManagerScriptingInterface = new DialogsManagerScriptingInterface(); }; #endif // hifi_Application_h diff --git a/interface/src/scripting/DialogsManagerScriptingInterface.cpp b/interface/src/scripting/DialogsManagerScriptingInterface.cpp new file mode 100644 index 0000000000..abbedb456e --- /dev/null +++ b/interface/src/scripting/DialogsManagerScriptingInterface.cpp @@ -0,0 +1,18 @@ +// +// DialogsManagerScriptingInterface.cpp +// interface/src/scripting +// +// Created by Zander Otavka on 7/17/15. +// 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 "DialogsManagerScriptingInterface.h" + +#include "ui/DialogsManager.h" + +void DialogsManagerScriptingInterface::toggleAddressBar() { + QMetaObject::invokeMethod(DependencyManager::get().data(), "toggleAddressBar", Qt::QueuedConnection); +} diff --git a/interface/src/scripting/DialogsManagerScriptingInterface.h b/interface/src/scripting/DialogsManagerScriptingInterface.h new file mode 100644 index 0000000000..9a844f79de --- /dev/null +++ b/interface/src/scripting/DialogsManagerScriptingInterface.h @@ -0,0 +1,23 @@ +// +// DialogsManagerScriptingInterface.h +// interface/src/scripting +// +// Created by Zander Otavka on 7/17/15. +// 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_DialogsManagerScriptInterface_h +#define hifi_DialogsManagerScriptInterface_h + +#include + +class DialogsManagerScriptingInterface : public QObject { + Q_OBJECT +public slots: + void toggleAddressBar(); +}; + +#endif From 052415534990607d46aa5ab0af11b9919f8d9d3b Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 17 Jul 2015 15:35:11 -0700 Subject: [PATCH 200/242] Expose face tracking to the script engine. --- interface/src/Application.cpp | 2 ++ interface/src/devices/FaceTracker.h | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 995c49b891..acdfd8cfc9 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3763,6 +3763,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("GlobalServices", GlobalServicesScriptingInterface::getInstance()); qScriptRegisterMetaType(scriptEngine, DownloadInfoResultToScriptValue, DownloadInfoResultFromScriptValue); + scriptEngine->registerGlobalObject("FaceTracker", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("AvatarManager", DependencyManager::get().data()); qScriptRegisterMetaType(scriptEngine, joystickToScriptValue, joystickFromScriptValue); diff --git a/interface/src/devices/FaceTracker.h b/interface/src/devices/FaceTracker.h index 193262d121..7126d19ca8 100644 --- a/interface/src/devices/FaceTracker.h +++ b/interface/src/devices/FaceTracker.h @@ -47,7 +47,6 @@ public: bool isMuted() const { return _isMuted; } void setIsMuted(bool isMuted) { _isMuted = isMuted; } - void toggleMute(); static float getEyeDeflection() { return _eyeDeflection.get(); } static void setEyeDeflection(float eyeDeflection); @@ -57,6 +56,8 @@ signals: public slots: virtual void setEnabled(bool enabled) = 0; + void toggleMute(); + bool getMuted() { return _isMuted; } protected: virtual ~FaceTracker() {}; From 1655dea2c3409b2ff4793639de2531213098a3c9 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Mon, 20 Jul 2015 11:44:03 -0700 Subject: [PATCH 201/242] Fix BillboardOverlay first person render bug. --- interface/src/ui/overlays/BillboardOverlay.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/interface/src/ui/overlays/BillboardOverlay.cpp b/interface/src/ui/overlays/BillboardOverlay.cpp index be8fab6362..c5e73a73e2 100644 --- a/interface/src/ui/overlays/BillboardOverlay.cpp +++ b/interface/src/ui/overlays/BillboardOverlay.cpp @@ -20,6 +20,8 @@ #include "Application.h" #include "GeometryUtil.h" +#include "DeferredLightingEffect.h" + #include @@ -100,12 +102,13 @@ void BillboardOverlay::render(RenderArgs* args) { if (batch) { Transform transform = _transform; - transform.postScale(glm::vec3(getDimensions(), 1.0f)); transform.setRotation(rotation); + transform.postScale(glm::vec3(getDimensions(), 1.0f)); batch->setModelTransform(transform); batch->setResourceTexture(0, _texture->getGPUTexture()); + DependencyManager::get()->bindSimpleProgram(*batch, true, true, true); DependencyManager::get()->renderQuad( *batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, glm::vec4(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha) From e5b870121534d8d6236278ce8abe04abcda15d25 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Mon, 20 Jul 2015 11:45:15 -0700 Subject: [PATCH 202/242] Add toggle event for address bar. --- .../scripting/DialogsManagerScriptingInterface.cpp | 12 ++++++++++-- .../src/scripting/DialogsManagerScriptingInterface.h | 6 ++++++ interface/src/ui/DialogsManager.cpp | 1 + interface/src/ui/DialogsManager.h | 3 +++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/interface/src/scripting/DialogsManagerScriptingInterface.cpp b/interface/src/scripting/DialogsManagerScriptingInterface.cpp index abbedb456e..80a8b4ac7c 100644 --- a/interface/src/scripting/DialogsManagerScriptingInterface.cpp +++ b/interface/src/scripting/DialogsManagerScriptingInterface.cpp @@ -11,8 +11,16 @@ #include "DialogsManagerScriptingInterface.h" +#include + #include "ui/DialogsManager.h" -void DialogsManagerScriptingInterface::toggleAddressBar() { - QMetaObject::invokeMethod(DependencyManager::get().data(), "toggleAddressBar", Qt::QueuedConnection); +DialogsManagerScriptingInterface::DialogsManagerScriptingInterface() { + connect(DependencyManager::get().data(), &DialogsManager::addressBarToggled, + this, &DialogsManagerScriptingInterface::addressBarToggled); +} + +void DialogsManagerScriptingInterface::toggleAddressBar() { + QMetaObject::invokeMethod(DependencyManager::get().data(), + "toggleAddressBar", Qt::QueuedConnection); } diff --git a/interface/src/scripting/DialogsManagerScriptingInterface.h b/interface/src/scripting/DialogsManagerScriptingInterface.h index 9a844f79de..ef44e20d61 100644 --- a/interface/src/scripting/DialogsManagerScriptingInterface.h +++ b/interface/src/scripting/DialogsManagerScriptingInterface.h @@ -16,8 +16,14 @@ class DialogsManagerScriptingInterface : public QObject { Q_OBJECT +public: + DialogsManagerScriptingInterface(); + public slots: void toggleAddressBar(); + +signals: + void addressBarToggled(); }; #endif diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index 04f532c59a..ac5e6833fb 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -36,6 +36,7 @@ void DialogsManager::toggleAddressBar() { AddressBarDialog::toggle(); + emit addressBarToggled(); } void DialogsManager::toggleDiskCacheEditor() { diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index b15830e35c..09e0274d86 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -72,6 +72,9 @@ public slots: // Application Update void showUpdateDialog(); +signals: + void addressBarToggled(); + private slots: void toggleToolWindow(); void hmdToolsClosed(); From 1d37df47754f24ce87ce735a5900b157d9fa3e22 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Mon, 20 Jul 2015 17:49:06 -0700 Subject: [PATCH 203/242] Improve and optimize panel transformations. --- .../src/ui/overlays/BillboardOverlay.cpp | 94 +++++++------------ interface/src/ui/overlays/BillboardOverlay.h | 7 +- interface/src/ui/overlays/PanelAttachable.cpp | 33 ++----- interface/src/ui/overlays/PanelAttachable.h | 18 ++-- 4 files changed, 57 insertions(+), 95 deletions(-) diff --git a/interface/src/ui/overlays/BillboardOverlay.cpp b/interface/src/ui/overlays/BillboardOverlay.cpp index c5e73a73e2..d8da58485c 100644 --- a/interface/src/ui/overlays/BillboardOverlay.cpp +++ b/interface/src/ui/overlays/BillboardOverlay.cpp @@ -39,13 +39,21 @@ BillboardOverlay::BillboardOverlay(const BillboardOverlay* billboardOverlay) : { } -void BillboardOverlay::update(float deltatime) { - if (getVisible()) { - glm::vec3 newPos = getTranslatedPosition(Application::getInstance()->getAvatarPosition()); - if (newPos != glm::vec3()) { - setPosition(newPos); +bool BillboardOverlay::setTransforms(Transform *transform) { + PanelAttachable::setTransforms(transform); + if (_isFacingAvatar) { + glm::quat rotation = Application::getInstance()->getCamera()->getOrientation(); + rotation *= glm::angleAxis(glm::pi(), IDENTITY_UP); + setRotation(rotation); + return true; } - } + return false; +// } +// return true; +} + +void BillboardOverlay::update(float deltatime) { + setTransforms(&_transform); } void BillboardOverlay::render(RenderArgs* args) { @@ -58,12 +66,8 @@ void BillboardOverlay::render(RenderArgs* args) { return; } - glm::vec3 newPos = getTranslatedPosition(Application::getInstance()->getAvatarPosition()); - if (newPos != glm::vec3()) { - setPosition(newPos); - } - - glm::quat rotation = calculateRotation(args->_viewFrustum->getOrientation()); + Q_ASSERT(args->_batch); + auto batch = args->_batch; float imageWidth = _texture->getWidth(); float imageHeight = _texture->getHeight(); @@ -98,24 +102,20 @@ void BillboardOverlay::render(RenderArgs* args) { xColor color = getColor(); float alpha = getAlpha(); - auto batch = args->_batch; + setTransforms(&_transform); + Transform transform = _transform; + transform.postScale(glm::vec3(getDimensions(), 1.0f)); - if (batch) { - Transform transform = _transform; - transform.setRotation(rotation); - transform.postScale(glm::vec3(getDimensions(), 1.0f)); - - batch->setModelTransform(transform); - batch->setResourceTexture(0, _texture->getGPUTexture()); - - DependencyManager::get()->bindSimpleProgram(*batch, true, true, true); - DependencyManager::get()->renderQuad( - *batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, - glm::vec4(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha) - ); + batch->setModelTransform(transform); + batch->setResourceTexture(0, _texture->getGPUTexture()); - batch->setResourceTexture(0, args->_whiteTexture); // restore default white color after me - } + DependencyManager::get()->bindSimpleProgram(*batch, true, true, false); + DependencyManager::get()->renderQuad( + *batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, + glm::vec4(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha) + ); + + batch->setResourceTexture(0, args->_whiteTexture); // restore default white color after me } void BillboardOverlay::setProperties(const QScriptValue &properties) { @@ -207,40 +207,12 @@ void BillboardOverlay::setBillboardURL(const QString& url) { _isLoaded = false; } -glm::quat BillboardOverlay::calculateRotation(glm::quat cameraOrientation) const { - if (_isFacingAvatar) { - // LOL, quaternions are hard. - // glm::vec3 dPos = getPosition() - args->_viewFrustum->getPosition(); - // dPos = glm::normalize(dPos); - // rotation = glm::quat(0, dPos.x, dPos.y, dPos.z); - // float horizontal = glm::sqrt(dPos.x * dPos.x + dPos.y + dPos.y); - // glm::vec3 zAxis = glm::vec3(0, 0, 1); - // rotation = rotationBetween(zAxis, dPos); - // glm::vec3 euler = safeEulerAngles(rotationBetween(zAxis, dPos)); - // rotation = glm::quat(glm::vec3(euler.x, euler.y, 0)); - // float yaw = (dPos.x == 0.0f && dPos.z == 0.0f) ? 0.0f : glm::atan(dPos.x, dPos.z); - // glm::quat yawQuat = glm::quat(glm::vec3(0, yaw, 0)); - // float pitch = (dPos.y == 0.0f && horizontal == 0.0f) ? 0.0f : glm::atan(dPos.y, horizontal); - // glm::quat pitchQuat = glm::quat(glm::vec3(pitch, 0, 0)); - // glm::mat4x4 matrix = glm::lookAt(args->_viewFrustum->getPosition(), getPosition(), - // glm::vec3(0, 1, 0)); - // rotation = glm::quat_cast(matrix); - // rotation = yawQuat * pitchQuat; - // glm::vec3 pitch = glm::vec3(dPos.x, dPos.y, 0); - // rotation = glm::quat(glm::vec3(pitch, yaw, 0)); - // rotate about vertical to be perpendicular to the camera - glm::quat rotation = cameraOrientation; - rotation *= glm::angleAxis(glm::pi(), IDENTITY_UP); - return rotation * getRotation(); - } - return getTranslatedRotation(getRotation()); -} - bool BillboardOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face) { + float& distance, BoxFace& face) { if (_texture && _texture->isLoaded()) { - glm::quat rotation = - calculateRotation(Application::getInstance()->getCamera()->getRotation()); + // Make sure position and rotation is updated. + setTransforms(&_transform); + // Produce the dimensions of the billboard based on the image's aspect ratio and the overlay's scale. bool isNull = _fromImage.isNull(); float width = isNull ? _texture->getWidth() : _fromImage.width(); @@ -248,7 +220,7 @@ bool BillboardOverlay::findRayIntersection(const glm::vec3& origin, const glm::v float maxSize = glm::max(width, height); glm::vec2 dimensions = _dimensions * glm::vec2(width / maxSize, height / maxSize); - return findRayRectangleIntersection(origin, direction, rotation, getPosition(), dimensions, distance); + return findRayRectangleIntersection(origin, direction, getRotation(), getPosition(), dimensions, distance); } return false; diff --git a/interface/src/ui/overlays/BillboardOverlay.h b/interface/src/ui/overlays/BillboardOverlay.h index 73e9db4c1b..3f8e18a0ef 100644 --- a/interface/src/ui/overlays/BillboardOverlay.h +++ b/interface/src/ui/overlays/BillboardOverlay.h @@ -39,10 +39,13 @@ public: virtual BillboardOverlay* createClone() const; +protected: + bool setTransforms(Transform* transform); + private: void setBillboardURL(const QString& url); - glm::quat calculateRotation(glm::quat cameraOrientation) const; - +// glm::quat calculateRotation(glm::quat cameraOrientation) const; + QString _url; NetworkTexturePointer _texture; diff --git a/interface/src/ui/overlays/PanelAttachable.cpp b/interface/src/ui/overlays/PanelAttachable.cpp index 1beea6b74e..7e2ce19099 100644 --- a/interface/src/ui/overlays/PanelAttachable.cpp +++ b/interface/src/ui/overlays/PanelAttachable.cpp @@ -20,31 +20,14 @@ PanelAttachable::PanelAttachable(const PanelAttachable* panelAttachable) : { } -glm::vec3 PanelAttachable::getTranslatedPosition(glm::vec3 avatarPosition) const { +bool PanelAttachable::setTransforms(Transform* transform) { + Q_ASSERT(transform != nullptr); if (getAttachedPanel()) { - glm::vec3 totalOffsetPosition = - getAttachedPanel()->getFacingRotation() * getOffsetPosition() + - getAttachedPanel()->getOffsetPosition(); - - return getAttachedPanel()->getOffsetRotation() * totalOffsetPosition + - avatarPosition; + transform->setTranslation(getAttachedPanel()->getAnchorPosition()); + transform->setRotation(getAttachedPanel()->getOffsetRotation()); + transform->postTranslate(getOffsetPosition() + getAttachedPanel()->getOffsetPosition()); + transform->postRotate(getFacingRotation() * getAttachedPanel()->getFacingRotation()); + return true; } - return glm::vec3(); -} - -glm::quat PanelAttachable::getTranslatedRotation(glm::quat offsetRotation) const { - glm::quat rotation = offsetRotation; - if (getAttachedPanel()) { - rotation *= getAttachedPanel()->getOffsetRotation() * - getAttachedPanel()->getFacingRotation(); - // if (getAttachedPanel()->getFacingRotation() != glm::quat(0, 0, 0, 0)) { - // rotation *= getAttachedPanel()->getFacingRotation(); - // } else if (getAttachedPanel()->getOffsetRotation() != glm::quat(0, 0, 0, 0)) { - // rotation *= getAttachedPanel()->getOffsetRotation(); - // } else { - // rotation *= Application::getInstance()->getCamera()->getOrientation() * - // glm::quat(0, 0, 1, 0); - // } - } - return rotation; + return false; } diff --git a/interface/src/ui/overlays/PanelAttachable.h b/interface/src/ui/overlays/PanelAttachable.h index ab8b8a4276..d44504f5d4 100644 --- a/interface/src/ui/overlays/PanelAttachable.h +++ b/interface/src/ui/overlays/PanelAttachable.h @@ -15,24 +15,28 @@ #include "FloatingUIPanel.h" #include +#include class PanelAttachable { public: PanelAttachable(); PanelAttachable(const PanelAttachable* panelAttachable); - glm::vec3 getOffsetPosition() const { return _offsetPosition; } - void setOffsetPosition(glm::vec3 position) { _offsetPosition = position; } - FloatingUIPanel* getAttachedPanel() const { return _attachedPanel; } - void setAttachedPanel(FloatingUIPanel* panel) { _attachedPanel = panel; } + glm::vec3 getOffsetPosition() const { return _offsetPosition; } + glm::quat getFacingRotation() const { return _facingRotation; } - glm::vec3 getTranslatedPosition(glm::vec3 avatarPosition) const; - glm::quat getTranslatedRotation(glm::quat offsetRotation) const; + void setAttachedPanel(FloatingUIPanel* panel) { _attachedPanel = panel; } + void setOffsetPosition(glm::vec3 position) { _offsetPosition = position; } + void setFacingRotation(glm::quat rotation) { _facingRotation = rotation; } + +protected: + bool setTransforms(Transform* transform); private: FloatingUIPanel* _attachedPanel; - glm::vec3 _offsetPosition = glm::vec3(0, 0, 0); + glm::vec3 _offsetPosition; + glm::quat _facingRotation; }; #endif // hifi_PanelAttachable_h From da2afca391054d71328fa8037a1f99706367fb45 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Thu, 23 Jul 2015 08:47:33 -0700 Subject: [PATCH 204/242] Expose overlay type checking to JavaScript. --- .../src/ui/overlays/BillboardOverlay.cpp | 18 +-- interface/src/ui/overlays/BillboardOverlay.h | 6 +- interface/src/ui/overlays/Circle3DOverlay.cpp | 2 + interface/src/ui/overlays/Circle3DOverlay.h | 3 + interface/src/ui/overlays/Cube3DOverlay.cpp | 2 + interface/src/ui/overlays/Cube3DOverlay.h | 3 + interface/src/ui/overlays/FloatingUIPanel.cpp | 153 +++++++++++++++--- interface/src/ui/overlays/FloatingUIPanel.h | 25 ++- interface/src/ui/overlays/Grid3DOverlay.cpp | 3 + interface/src/ui/overlays/Grid3DOverlay.h | 3 + interface/src/ui/overlays/ImageOverlay.cpp | 3 + interface/src/ui/overlays/ImageOverlay.h | 3 + interface/src/ui/overlays/Line3DOverlay.cpp | 3 + interface/src/ui/overlays/Line3DOverlay.h | 3 + .../src/ui/overlays/LocalModelsOverlay.cpp | 3 + .../src/ui/overlays/LocalModelsOverlay.h | 3 + interface/src/ui/overlays/ModelOverlay.cpp | 3 + interface/src/ui/overlays/ModelOverlay.h | 3 + interface/src/ui/overlays/Overlay.cpp | 3 +- interface/src/ui/overlays/Overlay.h | 1 + interface/src/ui/overlays/Overlays.cpp | 95 ++++++----- interface/src/ui/overlays/Overlays.h | 14 +- interface/src/ui/overlays/PanelAttachable.cpp | 12 +- interface/src/ui/overlays/PanelAttachable.h | 10 +- .../src/ui/overlays/Rectangle3DOverlay.cpp | 3 + .../src/ui/overlays/Rectangle3DOverlay.h | 3 + interface/src/ui/overlays/Sphere3DOverlay.cpp | 2 + interface/src/ui/overlays/Sphere3DOverlay.h | 3 + interface/src/ui/overlays/Text3DOverlay.cpp | 2 + interface/src/ui/overlays/Text3DOverlay.h | 3 + interface/src/ui/overlays/TextOverlay.cpp | 2 + interface/src/ui/overlays/TextOverlay.h | 3 + 32 files changed, 294 insertions(+), 104 deletions(-) diff --git a/interface/src/ui/overlays/BillboardOverlay.cpp b/interface/src/ui/overlays/BillboardOverlay.cpp index d8da58485c..3e8333b859 100644 --- a/interface/src/ui/overlays/BillboardOverlay.cpp +++ b/interface/src/ui/overlays/BillboardOverlay.cpp @@ -25,6 +25,8 @@ #include +QString const BillboardOverlay::TYPE = "billboard"; + BillboardOverlay::BillboardOverlay() { _isLoaded = false; } @@ -39,17 +41,13 @@ BillboardOverlay::BillboardOverlay(const BillboardOverlay* billboardOverlay) : { } -bool BillboardOverlay::setTransforms(Transform *transform) { +void BillboardOverlay::setTransforms(Transform *transform) { PanelAttachable::setTransforms(transform); - if (_isFacingAvatar) { - glm::quat rotation = Application::getInstance()->getCamera()->getOrientation(); - rotation *= glm::angleAxis(glm::pi(), IDENTITY_UP); - setRotation(rotation); - return true; - } - return false; -// } -// return true; + if (_isFacingAvatar) { + glm::quat rotation = Application::getInstance()->getCamera()->getOrientation(); + rotation *= glm::angleAxis(glm::pi(), IDENTITY_UP); + setRotation(rotation); + } } void BillboardOverlay::update(float deltatime) { diff --git a/interface/src/ui/overlays/BillboardOverlay.h b/interface/src/ui/overlays/BillboardOverlay.h index 3f8e18a0ef..47be764f2f 100644 --- a/interface/src/ui/overlays/BillboardOverlay.h +++ b/interface/src/ui/overlays/BillboardOverlay.h @@ -20,6 +20,9 @@ class BillboardOverlay : public Planar3DOverlay, public PanelAttachable { Q_OBJECT public: + static QString const TYPE; + virtual QString getType() const { return TYPE; } + BillboardOverlay(); BillboardOverlay(const BillboardOverlay* billboardOverlay); @@ -40,11 +43,10 @@ public: virtual BillboardOverlay* createClone() const; protected: - bool setTransforms(Transform* transform); + void setTransforms(Transform* transform); private: void setBillboardURL(const QString& url); -// glm::quat calculateRotation(glm::quat cameraOrientation) const; QString _url; NetworkTexturePointer _texture; diff --git a/interface/src/ui/overlays/Circle3DOverlay.cpp b/interface/src/ui/overlays/Circle3DOverlay.cpp index 53f1b4ce21..b906f008d6 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.cpp +++ b/interface/src/ui/overlays/Circle3DOverlay.cpp @@ -15,6 +15,8 @@ #include +QString const Circle3DOverlay::TYPE = "circle3d"; + Circle3DOverlay::Circle3DOverlay() : _startAt(0.0f), _endAt(360.0f), diff --git a/interface/src/ui/overlays/Circle3DOverlay.h b/interface/src/ui/overlays/Circle3DOverlay.h index d83703fcd0..5879fe1701 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.h +++ b/interface/src/ui/overlays/Circle3DOverlay.h @@ -18,6 +18,9 @@ class Circle3DOverlay : public Planar3DOverlay { Q_OBJECT public: + static QString const TYPE; + virtual QString getType() const { return TYPE; } + Circle3DOverlay(); Circle3DOverlay(const Circle3DOverlay* circle3DOverlay); diff --git a/interface/src/ui/overlays/Cube3DOverlay.cpp b/interface/src/ui/overlays/Cube3DOverlay.cpp index 200a1a328f..a306c7c86d 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.cpp +++ b/interface/src/ui/overlays/Cube3DOverlay.cpp @@ -19,6 +19,8 @@ #include #include +QString const Cube3DOverlay::TYPE = "cube"; + Cube3DOverlay::Cube3DOverlay(const Cube3DOverlay* cube3DOverlay) : Volume3DOverlay(cube3DOverlay) { diff --git a/interface/src/ui/overlays/Cube3DOverlay.h b/interface/src/ui/overlays/Cube3DOverlay.h index 397ad77a9e..6f9026a091 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.h +++ b/interface/src/ui/overlays/Cube3DOverlay.h @@ -17,6 +17,9 @@ class Cube3DOverlay : public Volume3DOverlay { Q_OBJECT public: + static QString const TYPE; + virtual QString getType() const { return TYPE; } + Cube3DOverlay() {} Cube3DOverlay(const Cube3DOverlay* cube3DOverlay); diff --git a/interface/src/ui/overlays/FloatingUIPanel.cpp b/interface/src/ui/overlays/FloatingUIPanel.cpp index e655a75f07..a862e4fcd4 100644 --- a/interface/src/ui/overlays/FloatingUIPanel.cpp +++ b/interface/src/ui/overlays/FloatingUIPanel.cpp @@ -13,23 +13,58 @@ #include #include +#include +#include "avatar/AvatarManager.h" +#include "avatar/MyAvatar.h" #include "Application.h" +#include "Base3DOverlay.h" -glm::quat FloatingUIPanel::getOffsetRotation() const { - if (getActualOffsetRotation() == glm::quat(0, 0, 0, 0)) { - return Application::getInstance()->getCamera()->getOrientation() * glm::quat(0, 0, 1, 0); - } - return getActualOffsetRotation(); +std::function const FloatingUIPanel::AVATAR_POSITION = []() -> glm::vec3 { + return DependencyManager::get()->getMyAvatar()->getPosition(); +}; + +std::function const FloatingUIPanel::AVATAR_ORIENTATION = []() -> glm::quat { + return DependencyManager::get()->getMyAvatar()->getOrientation() * + glm::angleAxis(glm::pi(), IDENTITY_UP); +}; + +FloatingUIPanel::FloatingUIPanel() : + _anchorPosition(AVATAR_POSITION), + _offsetRotation(AVATAR_ORIENTATION) +{ +} + +glm::vec3 FloatingUIPanel::getPosition() const { + return getOffsetRotation() * getOffsetPosition() + getAnchorPosition(); +} + +glm::quat FloatingUIPanel::getRotation() const { + return getOffsetRotation() * getFacingRotation(); +} + +void FloatingUIPanel::setAnchorPosition(glm::vec3 position) { + setAnchorPosition([position]() -> glm::vec3 { + return position; + }); +} + +void FloatingUIPanel::setOffsetRotation(glm::quat rotation) { + setOffsetRotation([rotation]() -> glm::quat { + return rotation; + }); } QScriptValue FloatingUIPanel::getProperty(const QString &property) { - if (property == "offsetPosition") { - return vec3toScriptValue(_scriptEngine, getOffsetPosition()); + if (property == "anchorPosition") { + return vec3toScriptValue(_scriptEngine, getAnchorPosition()); } if (property == "offsetRotation") { - return quatToScriptValue(_scriptEngine, getActualOffsetRotation()); + return quatToScriptValue(_scriptEngine, getOffsetRotation()); + } + if (property == "offsetPosition") { + return vec3toScriptValue(_scriptEngine, getOffsetPosition()); } if (property == "facingRotation") { return quatToScriptValue(_scriptEngine, getFacingRotation()); @@ -39,6 +74,91 @@ QScriptValue FloatingUIPanel::getProperty(const QString &property) { } void FloatingUIPanel::setProperties(const QScriptValue &properties) { + QScriptValue anchor = properties.property("anchorPosition"); + if (anchor.isValid()) { + QScriptValue type = anchor.property("type"); + QScriptValue value = anchor.property("value"); + + if (type.isValid()) { + QString typeString = type.toVariant().toString(); + if (typeString == "myAvatar") { + setAnchorPosition(AVATAR_POSITION); + } else if (value.isValid()) { + if (typeString == "overlay") { + Overlay::Pointer overlay = Application::getInstance()->getOverlays() + .getOverlay(value.toVariant().toUInt()); + if (overlay->is3D()) { + auto overlay3D = std::static_pointer_cast(overlay); + setAnchorPosition([&overlay3D]() -> glm::vec3 { + return overlay3D->getPosition(); + }); + } + } else if (typeString == "panel") { + FloatingUIPanel::Pointer panel = Application::getInstance()->getOverlays() + .getPanel(value.toVariant().toUInt()); + setAnchorPosition([panel]() -> glm::vec3 { + return panel->getPosition(); + }); + } else if (typeString == "vec3") { + QScriptValue x = value.property("x"); + QScriptValue y = value.property("y"); + QScriptValue z = value.property("z"); + if (x.isValid() && y.isValid() && z.isValid()) { + glm::vec3 newPosition; + newPosition.x = x.toVariant().toFloat(); + newPosition.y = y.toVariant().toFloat(); + newPosition.z = z.toVariant().toFloat(); + setAnchorPosition(newPosition); + } + } + } + } + } + + QScriptValue offsetRotation = properties.property("offsetRotation"); + if (offsetRotation.isValid()) { + QScriptValue type = offsetRotation.property("type"); + QScriptValue value = offsetRotation.property("value"); + + if (type.isValid()) { + QString typeString = type.toVariant().toString(); + if (typeString == "myAvatar") { + setOffsetRotation(AVATAR_ORIENTATION); + } else if (value.isValid()) { + if (typeString == "overlay") { + Overlay::Pointer overlay = Application::getInstance()->getOverlays() + .getOverlay(value.toVariant().toUInt()); + if (overlay->is3D()) { + auto overlay3D = std::static_pointer_cast(overlay); + setOffsetRotation([&overlay3D]() -> glm::quat { + return overlay3D->getRotation(); + }); + } + } else if (typeString == "panel") { + FloatingUIPanel::Pointer panel = Application::getInstance()->getOverlays() + .getPanel(value.toVariant().toUInt()); + setOffsetRotation([panel]() -> glm::quat { + return panel->getRotation(); + }); + } else if (typeString == "quat") { + QScriptValue x = value.property("x"); + QScriptValue y = value.property("y"); + QScriptValue z = value.property("z"); + QScriptValue w = value.property("w"); + + if (x.isValid() && y.isValid() && z.isValid() && w.isValid()) { + glm::quat newRotation; + newRotation.x = x.toVariant().toFloat(); + newRotation.y = y.toVariant().toFloat(); + newRotation.z = z.toVariant().toFloat(); + newRotation.w = w.toVariant().toFloat(); + setOffsetRotation(newRotation); + } + } + } + } + } + QScriptValue offsetPosition = properties.property("offsetPosition"); if (offsetPosition.isValid()) { QScriptValue x = offsetPosition.property("x"); @@ -53,23 +173,6 @@ void FloatingUIPanel::setProperties(const QScriptValue &properties) { } } - QScriptValue offsetRotation = properties.property("offsetRotation"); - if (offsetRotation.isValid()) { - QScriptValue x = offsetRotation.property("x"); - QScriptValue y = offsetRotation.property("y"); - QScriptValue z = offsetRotation.property("z"); - QScriptValue w = offsetRotation.property("w"); - - if (x.isValid() && y.isValid() && z.isValid() && w.isValid()) { - glm::quat newRotation; - newRotation.x = x.toVariant().toFloat(); - newRotation.y = y.toVariant().toFloat(); - newRotation.z = z.toVariant().toFloat(); - newRotation.w = w.toVariant().toFloat(); - setOffsetRotation(newRotation); - } - } - QScriptValue facingRotation = properties.property("facingRotation"); if (offsetRotation.isValid()) { QScriptValue x = facingRotation.property("x"); diff --git a/interface/src/ui/overlays/FloatingUIPanel.h b/interface/src/ui/overlays/FloatingUIPanel.h index 7f8d42eb5b..abc1328928 100644 --- a/interface/src/ui/overlays/FloatingUIPanel.h +++ b/interface/src/ui/overlays/FloatingUIPanel.h @@ -25,22 +25,31 @@ public: void init(QScriptEngine* scriptEngine) { _scriptEngine = scriptEngine; } + glm::vec3 getAnchorPosition() const { return _anchorPosition(); } + glm::quat getOffsetRotation() const { return _offsetRotation(); } glm::vec3 getOffsetPosition() const { return _offsetPosition; } - glm::quat getOffsetRotation() const; - glm::quat getActualOffsetRotation() const { return _offsetRotation; } glm::quat getFacingRotation() const { return _facingRotation; } + glm::vec3 getPosition() const; + glm::quat getRotation() const; - void setOffsetPosition(glm::vec3 position) { _offsetPosition = position; }; - void setOffsetRotation(glm::quat rotation) { _offsetRotation = rotation; }; - void setFacingRotation(glm::quat rotation) { _facingRotation = rotation; }; + void setAnchorPosition(const std::function& func) { _anchorPosition = func; } + void setAnchorPosition(glm::vec3 position); + void setOffsetRotation(const std::function& func) { _offsetRotation = func; } + void setOffsetRotation(glm::quat rotation); + void setOffsetPosition(glm::vec3 position) { _offsetPosition = position; } + void setFacingRotation(glm::quat rotation) { _facingRotation = rotation; } QScriptValue getProperty(const QString& property); void setProperties(const QScriptValue& properties); private: - glm::vec3 _offsetPosition = glm::vec3(0, 0, 0); - glm::quat _offsetRotation = glm::quat(0, 0, 0, 0); - glm::quat _facingRotation = glm::quat(1, 0, 0, 0); + static std::function const AVATAR_POSITION; + static std::function const AVATAR_ORIENTATION; + + std::function _anchorPosition; + std::function _offsetRotation; + glm::vec3 _offsetPosition{0, 0, 0}; + glm::quat _facingRotation{1, 0, 0, 0}; QScriptEngine* _scriptEngine; }; diff --git a/interface/src/ui/overlays/Grid3DOverlay.cpp b/interface/src/ui/overlays/Grid3DOverlay.cpp index 956eae35ff..074ad3d17f 100644 --- a/interface/src/ui/overlays/Grid3DOverlay.cpp +++ b/interface/src/ui/overlays/Grid3DOverlay.cpp @@ -18,6 +18,9 @@ #include #include + +QString const Grid3DOverlay::TYPE = "grid"; + Grid3DOverlay::Grid3DOverlay() : _minorGridWidth(1.0), _majorGridEvery(5) { diff --git a/interface/src/ui/overlays/Grid3DOverlay.h b/interface/src/ui/overlays/Grid3DOverlay.h index 80ec4b84b9..5fb3852905 100644 --- a/interface/src/ui/overlays/Grid3DOverlay.h +++ b/interface/src/ui/overlays/Grid3DOverlay.h @@ -18,6 +18,9 @@ class Grid3DOverlay : public Planar3DOverlay { Q_OBJECT public: + static QString const TYPE; + virtual QString getType() const { return TYPE; } + Grid3DOverlay(); Grid3DOverlay(const Grid3DOverlay* grid3DOverlay); diff --git a/interface/src/ui/overlays/ImageOverlay.cpp b/interface/src/ui/overlays/ImageOverlay.cpp index cad9010531..ed88f50981 100644 --- a/interface/src/ui/overlays/ImageOverlay.cpp +++ b/interface/src/ui/overlays/ImageOverlay.cpp @@ -16,6 +16,9 @@ #include #include + +QString const ImageOverlay::TYPE = "image"; + ImageOverlay::ImageOverlay() : _imageURL(), _renderImage(false), diff --git a/interface/src/ui/overlays/ImageOverlay.h b/interface/src/ui/overlays/ImageOverlay.h index 59d4102933..5698a73732 100644 --- a/interface/src/ui/overlays/ImageOverlay.h +++ b/interface/src/ui/overlays/ImageOverlay.h @@ -24,6 +24,9 @@ class ImageOverlay : public Overlay2D { Q_OBJECT public: + static QString const TYPE; + virtual QString getType() const { return TYPE; } + ImageOverlay(); ImageOverlay(const ImageOverlay* imageOverlay); diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index d026e5f56e..0acd7ecc1e 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -13,6 +13,9 @@ #include #include + +QString const Line3DOverlay::TYPE = "line3d"; + Line3DOverlay::Line3DOverlay() : _geometryCacheID(DependencyManager::get()->allocateID()) { diff --git a/interface/src/ui/overlays/Line3DOverlay.h b/interface/src/ui/overlays/Line3DOverlay.h index 4a4d8f4d90..05812709dc 100644 --- a/interface/src/ui/overlays/Line3DOverlay.h +++ b/interface/src/ui/overlays/Line3DOverlay.h @@ -17,6 +17,9 @@ class Line3DOverlay : public Base3DOverlay { Q_OBJECT public: + static QString const TYPE; + virtual QString getType() const { return TYPE; } + Line3DOverlay(); Line3DOverlay(const Line3DOverlay* line3DOverlay); ~Line3DOverlay(); diff --git a/interface/src/ui/overlays/LocalModelsOverlay.cpp b/interface/src/ui/overlays/LocalModelsOverlay.cpp index 3f701cbbe6..38e11562da 100644 --- a/interface/src/ui/overlays/LocalModelsOverlay.cpp +++ b/interface/src/ui/overlays/LocalModelsOverlay.cpp @@ -14,6 +14,9 @@ #include #include + +QString const LocalModelsOverlay::TYPE = "localmodels"; + LocalModelsOverlay::LocalModelsOverlay(EntityTreeRenderer* entityTreeRenderer) : Volume3DOverlay(), _entityTreeRenderer(entityTreeRenderer) { diff --git a/interface/src/ui/overlays/LocalModelsOverlay.h b/interface/src/ui/overlays/LocalModelsOverlay.h index c311b2bc1b..011f3dfb11 100644 --- a/interface/src/ui/overlays/LocalModelsOverlay.h +++ b/interface/src/ui/overlays/LocalModelsOverlay.h @@ -19,6 +19,9 @@ class EntityTreeRenderer; class LocalModelsOverlay : public Volume3DOverlay { Q_OBJECT public: + static QString const TYPE; + virtual QString getType() const { return TYPE; } + LocalModelsOverlay(EntityTreeRenderer* entityTreeRenderer); LocalModelsOverlay(const LocalModelsOverlay* localModelsOverlay); diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index ed15e57d43..7559d696d5 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -13,6 +13,9 @@ #include "Application.h" + +QString const ModelOverlay::TYPE = "model"; + ModelOverlay::ModelOverlay() : _model(), _modelTextures(QVariantMap()), diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index 1c43f42909..97ecfb642c 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -19,6 +19,9 @@ class ModelOverlay : public Volume3DOverlay { Q_OBJECT public: + static QString const TYPE; + virtual QString getType() const { return TYPE; } + ModelOverlay(); ModelOverlay(const ModelOverlay* modelOverlay); diff --git a/interface/src/ui/overlays/Overlay.cpp b/interface/src/ui/overlays/Overlay.cpp index 17e4ae74e1..7824c0c498 100644 --- a/interface/src/ui/overlays/Overlay.cpp +++ b/interface/src/ui/overlays/Overlay.cpp @@ -110,7 +110,8 @@ void Overlay::setProperties(const QScriptValue& properties) { } if (properties.property("visible").isValid()) { - setVisible(properties.property("visible").toVariant().toBool()); + bool visible = properties.property("visible").toVariant().toBool(); + setVisible(visible); } if (properties.property("anchor").isValid()) { diff --git a/interface/src/ui/overlays/Overlay.h b/interface/src/ui/overlays/Overlay.h index 1698d71fc3..9a19c71db6 100644 --- a/interface/src/ui/overlays/Overlay.h +++ b/interface/src/ui/overlays/Overlay.h @@ -44,6 +44,7 @@ public: virtual void removeFromScene(Overlay::Pointer overlay, std::shared_ptr scene, render::PendingChanges& pendingChanges); // getters + virtual QString getType() const = 0; virtual bool is3D() const = 0; bool isLoaded() { return _isLoaded; } bool getVisible() const { return _visible; } diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index fe489847ee..fb9f64c84e 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -135,13 +135,13 @@ Overlay::Pointer Overlays::getOverlay(unsigned int id) const { return nullptr; } -void Overlays::setAttachedPanel(Overlay* overlay, unsigned int overlayId, const QScriptValue& property) { - if (PanelAttachable* attachable = dynamic_cast(overlay)) { +void Overlays::setAttachedPanel(Overlay::Pointer overlay, unsigned int overlayId, const QScriptValue& property) { + auto attachable = std::dynamic_pointer_cast(overlay); + if (attachable) { if (property.isValid()) { unsigned int attachedPanelId = property.toVariant().toUInt(); - FloatingUIPanel* panel = nullptr; if (_panels.contains(attachedPanelId)) { - panel = _panels[attachedPanelId].get(); + auto panel = _panels[attachedPanelId]; panel->children.append(overlayId); attachable->setAttachedPanel(panel); } else { @@ -153,32 +153,32 @@ void Overlays::setAttachedPanel(Overlay* overlay, unsigned int overlayId, const } unsigned int Overlays::addOverlay(const QString& type, const QScriptValue& properties) { - Overlay* thisOverlay = NULL; + Overlay::Pointer thisOverlay = nullptr; - if (type == "image") { - thisOverlay = new ImageOverlay(); - } else if (type == "text") { - thisOverlay = new TextOverlay(); - } else if (type == "text3d") { - thisOverlay = new Text3DOverlay(); - } else if (type == "cube") { - thisOverlay = new Cube3DOverlay(); - } else if (type == "sphere") { - thisOverlay = new Sphere3DOverlay(); - } else if (type == "circle3d") { - thisOverlay = new Circle3DOverlay(); - } else if (type == "rectangle3d") { - thisOverlay = new Rectangle3DOverlay(); - } else if (type == "line3d") { - thisOverlay = new Line3DOverlay(); - } else if (type == "grid") { - thisOverlay = new Grid3DOverlay(); - } else if (type == "localmodels") { - thisOverlay = new LocalModelsOverlay(Application::getInstance()->getEntityClipboardRenderer()); - } else if (type == "model") { - thisOverlay = new ModelOverlay(); - } else if (type == "billboard") { - thisOverlay = new BillboardOverlay(); + if (type == ImageOverlay::TYPE) { + thisOverlay = std::make_shared(); + } else if (type == TextOverlay::TYPE) { + thisOverlay = std::make_shared(); + } else if (type == Text3DOverlay::TYPE) { + thisOverlay = std::make_shared(); + } else if (type == Cube3DOverlay::TYPE) { + thisOverlay = std::make_shared(); + } else if (type == Sphere3DOverlay::TYPE) { + thisOverlay = std::make_shared(); + } else if (type == Circle3DOverlay::TYPE) { + thisOverlay = std::make_shared(); + } else if (type == Rectangle3DOverlay::TYPE) { + thisOverlay = std::make_shared(); + } else if (type == Line3DOverlay::TYPE) { + thisOverlay = std::make_shared(); + } else if (type == Grid3DOverlay::TYPE) { + thisOverlay = std::make_shared(); + } else if (type == LocalModelsOverlay::TYPE) { + thisOverlay = std::make_shared(Application::getInstance()->getEntityClipboardRenderer()); + } else if (type == ModelOverlay::TYPE) { + thisOverlay = std::make_shared(); + } else if (type == BillboardOverlay::TYPE) { + thisOverlay = std::make_shared(); } if (thisOverlay) { @@ -190,29 +190,28 @@ unsigned int Overlays::addOverlay(const QString& type, const QScriptValue& prope return 0; } -unsigned int Overlays::addOverlay(Overlay* overlay) { - Overlay::Pointer overlayPointer(overlay); +unsigned int Overlays::addOverlay(Overlay::Pointer overlay) { overlay->init(_scriptEngine); QWriteLocker lock(&_lock); unsigned int thisID = _nextOverlayID; _nextOverlayID++; if (overlay->is3D()) { - Base3DOverlay* overlay3D = static_cast(overlay); + auto overlay3D = std::static_pointer_cast(overlay); if (overlay3D->getDrawOnHUD()) { - _overlaysHUD[thisID] = overlayPointer; + _overlaysHUD[thisID] = overlay; } else { - _overlaysWorld[thisID] = overlayPointer; + _overlaysWorld[thisID] = overlay; render::ScenePointer scene = Application::getInstance()->getMain3DScene(); render::PendingChanges pendingChanges; - overlayPointer->addToScene(overlayPointer, scene, pendingChanges); + overlay->addToScene(overlay, scene, pendingChanges); scene->enqueuePendingChanges(pendingChanges); } } else { - _overlaysHUD[thisID] = overlayPointer; + _overlaysHUD[thisID] = overlay; } return thisID; @@ -222,7 +221,7 @@ unsigned int Overlays::cloneOverlay(unsigned int id) { Overlay::Pointer thisOverlay = getOverlay(id); if (thisOverlay) { - return addOverlay(thisOverlay->createClone()); + return addOverlay(Overlay::Pointer(thisOverlay->createClone())); } return 0; // Not found @@ -253,7 +252,7 @@ bool Overlays::editOverlay(unsigned int id, const QScriptValue& properties) { thisOverlay->setProperties(properties); } - setAttachedPanel(thisOverlay.get(), id, properties.property("attachedPanel")); + setAttachedPanel(thisOverlay, id, properties.property("attachedPanel")); return true; } @@ -278,6 +277,14 @@ void Overlays::deleteOverlay(unsigned int id) { _overlaysToDelete.push_back(overlayToDelete); } +QString Overlays::getOverlayType(unsigned int overlayId) const { + Overlay::Pointer overlay = getOverlay(overlayId); + if (overlay) { + return overlay->getType(); + } + return ""; +} + unsigned int Overlays::getOverlayAtPoint(const glm::vec2& point) { glm::vec2 pointCopy = point; if (qApp->isHMDMode()) { @@ -322,8 +329,9 @@ OverlayPropertyResult Overlays::getProperty(unsigned int id, const QString& prop QReadLocker lock(&_lock); if (thisOverlay) { if (property == "attachedPanel") { - if (FloatingUIPanel* panel = dynamic_cast(thisOverlay.get())) { - result.value = _panels.key(FloatingUIPanel::Pointer(panel)); + auto panelAttachable = std::dynamic_pointer_cast(thisOverlay); + if (panelAttachable) { + result.value = _panels.key(panelAttachable->getAttachedPanel()); } else { result.value = 0; } @@ -499,19 +507,18 @@ QSizeF Overlays::textSize(unsigned int id, const QString& text) const { return QSizeF(0.0f, 0.0f); } -unsigned int Overlays::addPanel(FloatingUIPanel* panel) { +unsigned int Overlays::addPanel(FloatingUIPanel::Pointer panel) { QWriteLocker lock(&_lock); - FloatingUIPanel::Pointer panelPointer(panel); unsigned int thisID = _nextOverlayID; _nextOverlayID++; - _panels[thisID] = panelPointer; + _panels[thisID] = panel; return thisID; } unsigned int Overlays::addPanel(const QScriptValue& properties) { - FloatingUIPanel* panel = new FloatingUIPanel(); + FloatingUIPanel::Pointer panel = std::make_shared(); panel->init(_scriptEngine); panel->setProperties(properties); return addPanel(panel); diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index 80da2e6db1..ee4fa5b7d4 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -66,12 +66,16 @@ public: void update(float deltatime); void renderHUD(RenderArgs* renderArgs); + Overlay::Pointer getOverlay(unsigned int id) const; + FloatingUIPanel::Pointer getPanel(unsigned int id) const { return _panels[id]; } + public slots: /// adds an overlay with the specific properties unsigned int addOverlay(const QString& type, const QScriptValue& properties); /// adds an overlay that's already been created - unsigned int addOverlay(Overlay* overlay); + unsigned int addOverlay(Overlay* overlay) { return addOverlay(Overlay::Pointer(overlay)); } + unsigned int addOverlay(Overlay::Pointer overlay); /// clones an existing overlay unsigned int cloneOverlay(unsigned int id); @@ -83,6 +87,9 @@ public slots: /// deletes a particle void deleteOverlay(unsigned int id); + /// + QString getOverlayType(unsigned int overlayId) const; + /// returns the top most 2D overlay at the screen point, or 0 if not overlay at that point unsigned int getOverlayAtPoint(const glm::vec2& point); @@ -101,7 +108,7 @@ public slots: /// adds a panel that has already been created - unsigned int addPanel(FloatingUIPanel* panel); + unsigned int addPanel(FloatingUIPanel::Pointer panel); /// creates and adds a panel based on a set of properties unsigned int addPanel(const QScriptValue& properties); @@ -117,8 +124,7 @@ public slots: private: void cleanupOverlaysToDelete(); - Overlay::Pointer getOverlay(unsigned int id) const; - void setAttachedPanel(Overlay* overlay, unsigned int overlayId, const QScriptValue& property); + void setAttachedPanel(Overlay::Pointer overlay, unsigned int overlayId, const QScriptValue& property); QMap _overlaysHUD; QMap _overlaysWorld; diff --git a/interface/src/ui/overlays/PanelAttachable.cpp b/interface/src/ui/overlays/PanelAttachable.cpp index 7e2ce19099..bedf7d9fd5 100644 --- a/interface/src/ui/overlays/PanelAttachable.cpp +++ b/interface/src/ui/overlays/PanelAttachable.cpp @@ -3,31 +3,33 @@ // hifi // // Created by Zander Otavka on 7/15/15. +// 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 "PanelAttachable.h" PanelAttachable::PanelAttachable() : _attachedPanel(nullptr), - _offsetPosition(glm::vec3()) + _facingRotation(1, 0, 0, 0) { } PanelAttachable::PanelAttachable(const PanelAttachable* panelAttachable) : _attachedPanel(panelAttachable->_attachedPanel), - _offsetPosition(panelAttachable->_offsetPosition) + _offsetPosition(panelAttachable->_offsetPosition), + _facingRotation(panelAttachable->_facingRotation) { } -bool PanelAttachable::setTransforms(Transform* transform) { +void PanelAttachable::setTransforms(Transform* transform) { Q_ASSERT(transform != nullptr); if (getAttachedPanel()) { transform->setTranslation(getAttachedPanel()->getAnchorPosition()); transform->setRotation(getAttachedPanel()->getOffsetRotation()); transform->postTranslate(getOffsetPosition() + getAttachedPanel()->getOffsetPosition()); transform->postRotate(getFacingRotation() * getAttachedPanel()->getFacingRotation()); - return true; } - return false; } diff --git a/interface/src/ui/overlays/PanelAttachable.h b/interface/src/ui/overlays/PanelAttachable.h index d44504f5d4..097fc9a517 100644 --- a/interface/src/ui/overlays/PanelAttachable.h +++ b/interface/src/ui/overlays/PanelAttachable.h @@ -3,7 +3,7 @@ // interface/src/ui/overlays // // Created by Zander Otavka on 7/1/15. -// Copyright 2014 High Fidelity, Inc. +// 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 @@ -22,19 +22,19 @@ public: PanelAttachable(); PanelAttachable(const PanelAttachable* panelAttachable); - FloatingUIPanel* getAttachedPanel() const { return _attachedPanel; } + FloatingUIPanel::Pointer getAttachedPanel() const { return _attachedPanel; } glm::vec3 getOffsetPosition() const { return _offsetPosition; } glm::quat getFacingRotation() const { return _facingRotation; } - void setAttachedPanel(FloatingUIPanel* panel) { _attachedPanel = panel; } + void setAttachedPanel(FloatingUIPanel::Pointer panel) { _attachedPanel = panel; } void setOffsetPosition(glm::vec3 position) { _offsetPosition = position; } void setFacingRotation(glm::quat rotation) { _facingRotation = rotation; } protected: - bool setTransforms(Transform* transform); + void setTransforms(Transform* transform); private: - FloatingUIPanel* _attachedPanel; + FloatingUIPanel::Pointer _attachedPanel; glm::vec3 _offsetPosition; glm::quat _facingRotation; }; diff --git a/interface/src/ui/overlays/Rectangle3DOverlay.cpp b/interface/src/ui/overlays/Rectangle3DOverlay.cpp index 1d5183b833..64c5e4c819 100644 --- a/interface/src/ui/overlays/Rectangle3DOverlay.cpp +++ b/interface/src/ui/overlays/Rectangle3DOverlay.cpp @@ -13,6 +13,9 @@ #include #include + +QString const Rectangle3DOverlay::TYPE = "rectangle3d"; + Rectangle3DOverlay::Rectangle3DOverlay() : _geometryCacheID(DependencyManager::get()->allocateID()) { diff --git a/interface/src/ui/overlays/Rectangle3DOverlay.h b/interface/src/ui/overlays/Rectangle3DOverlay.h index f59ab21198..e5eba8bce8 100644 --- a/interface/src/ui/overlays/Rectangle3DOverlay.h +++ b/interface/src/ui/overlays/Rectangle3DOverlay.h @@ -17,6 +17,9 @@ class Rectangle3DOverlay : public Planar3DOverlay { Q_OBJECT public: + static QString const TYPE; + virtual QString getType() const { return TYPE; } + Rectangle3DOverlay(); Rectangle3DOverlay(const Rectangle3DOverlay* rectangle3DOverlay); ~Rectangle3DOverlay(); diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp index 307b60b2ce..9712375209 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.cpp +++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp @@ -15,6 +15,8 @@ #include #include +QString const Sphere3DOverlay::TYPE = "sphere"; + Sphere3DOverlay::Sphere3DOverlay(const Sphere3DOverlay* Sphere3DOverlay) : Volume3DOverlay(Sphere3DOverlay) { diff --git a/interface/src/ui/overlays/Sphere3DOverlay.h b/interface/src/ui/overlays/Sphere3DOverlay.h index b82dc548f1..d371077502 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.h +++ b/interface/src/ui/overlays/Sphere3DOverlay.h @@ -17,6 +17,9 @@ class Sphere3DOverlay : public Volume3DOverlay { Q_OBJECT public: + static QString const TYPE; + virtual QString getType() const { return TYPE; } + Sphere3DOverlay() {} Sphere3DOverlay(const Sphere3DOverlay* Sphere3DOverlay); diff --git a/interface/src/ui/overlays/Text3DOverlay.cpp b/interface/src/ui/overlays/Text3DOverlay.cpp index 737b2d0bc5..00a7d75090 100644 --- a/interface/src/ui/overlays/Text3DOverlay.cpp +++ b/interface/src/ui/overlays/Text3DOverlay.cpp @@ -23,6 +23,8 @@ const int FIXED_FONT_POINT_SIZE = 40; const int FIXED_FONT_SCALING_RATIO = FIXED_FONT_POINT_SIZE * 80.0f; // this is a ratio determined through experimentation const float LINE_SCALE_RATIO = 1.2f; +QString const Text3DOverlay::TYPE = "text3d"; + Text3DOverlay::Text3DOverlay() : _backgroundColor(DEFAULT_BACKGROUND_COLOR), _backgroundAlpha(DEFAULT_BACKGROUND_ALPHA), diff --git a/interface/src/ui/overlays/Text3DOverlay.h b/interface/src/ui/overlays/Text3DOverlay.h index 666b43d8b1..abd5ef54bd 100644 --- a/interface/src/ui/overlays/Text3DOverlay.h +++ b/interface/src/ui/overlays/Text3DOverlay.h @@ -21,6 +21,9 @@ class Text3DOverlay : public Planar3DOverlay { Q_OBJECT public: + static QString const TYPE; + virtual QString getType() const { return TYPE; } + Text3DOverlay(); Text3DOverlay(const Text3DOverlay* text3DOverlay); ~Text3DOverlay(); diff --git a/interface/src/ui/overlays/TextOverlay.cpp b/interface/src/ui/overlays/TextOverlay.cpp index ea0c4f6026..e9fda2def8 100644 --- a/interface/src/ui/overlays/TextOverlay.cpp +++ b/interface/src/ui/overlays/TextOverlay.cpp @@ -78,6 +78,8 @@ QString toQmlColor(const glm::vec4& v) { arg((int)(v.b * 255), 2, 16, QChar('0')); } +QString const TextOverlay::TYPE = "text"; + TextOverlay::TextOverlay() : _backgroundColor(DEFAULT_BACKGROUND_COLOR), _backgroundAlpha(DEFAULT_BACKGROUND_ALPHA), diff --git a/interface/src/ui/overlays/TextOverlay.h b/interface/src/ui/overlays/TextOverlay.h index b70b95ca3b..a8e6967871 100644 --- a/interface/src/ui/overlays/TextOverlay.h +++ b/interface/src/ui/overlays/TextOverlay.h @@ -29,6 +29,9 @@ class TextOverlay : public Overlay2D { Q_OBJECT public: + static QString const TYPE; + virtual QString getType() const { return TYPE; } + TextOverlay(); TextOverlay(const TextOverlay* textOverlay); ~TextOverlay(); From bc5ef8eb113f32295a7a12e1d8fc1b5096e49658 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 24 Jul 2015 17:50:17 -0700 Subject: [PATCH 205/242] Clean up C++ relating to panels. --- .../src/ui/overlays/BillboardOverlay.cpp | 28 ++----- interface/src/ui/overlays/FloatingUIPanel.cpp | 53 +++++++------ interface/src/ui/overlays/FloatingUIPanel.h | 22 ++++-- interface/src/ui/overlays/Overlays.cpp | 75 ++++++++++--------- interface/src/ui/overlays/Overlays.h | 10 ++- interface/src/ui/overlays/PanelAttachable.cpp | 46 ++++++++++++ interface/src/ui/overlays/PanelAttachable.h | 7 +- 7 files changed, 151 insertions(+), 90 deletions(-) diff --git a/interface/src/ui/overlays/BillboardOverlay.cpp b/interface/src/ui/overlays/BillboardOverlay.cpp index 3e8333b859..8ffa909e12 100644 --- a/interface/src/ui/overlays/BillboardOverlay.cpp +++ b/interface/src/ui/overlays/BillboardOverlay.cpp @@ -13,17 +13,15 @@ #include +#include #include #include #include +#include #include "Application.h" #include "GeometryUtil.h" -#include "DeferredLightingEffect.h" - -#include - QString const BillboardOverlay::TYPE = "billboard"; @@ -107,7 +105,7 @@ void BillboardOverlay::render(RenderArgs* args) { batch->setModelTransform(transform); batch->setResourceTexture(0, _texture->getGPUTexture()); - DependencyManager::get()->bindSimpleProgram(*batch, true, true, false); + DependencyManager::get()->bindSimpleProgram(*batch, true, true, false, true); DependencyManager::get()->renderQuad( *batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, glm::vec4(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha) @@ -118,6 +116,7 @@ void BillboardOverlay::render(RenderArgs* args) { void BillboardOverlay::setProperties(const QScriptValue &properties) { Planar3DOverlay::setProperties(properties); + PanelAttachable::setProperties(properties); QScriptValue urlValue = properties.property("url"); if (urlValue.isValid()) { @@ -162,21 +161,6 @@ void BillboardOverlay::setProperties(const QScriptValue &properties) { if (isFacingAvatarValue.isValid()) { _isFacingAvatar = isFacingAvatarValue.toVariant().toBool(); } - - QScriptValue offsetPosition = properties.property("offsetPosition"); - if (offsetPosition.isValid()) { - QScriptValue x = offsetPosition.property("x"); - QScriptValue y = offsetPosition.property("y"); - QScriptValue z = offsetPosition.property("z"); - - if (x.isValid() && y.isValid() && z.isValid()) { - glm::vec3 newPosition; - newPosition.x = x.toVariant().toFloat(); - newPosition.y = y.toVariant().toFloat(); - newPosition.z = z.toVariant().toFloat(); - setOffsetPosition(newPosition); - } - } } QScriptValue BillboardOverlay::getProperty(const QString& property) { @@ -193,6 +177,10 @@ QScriptValue BillboardOverlay::getProperty(const QString& property) { return vec3toScriptValue(_scriptEngine, getOffsetPosition()); } + QScriptValue value = PanelAttachable::getProperty(_scriptEngine, property); + if (value.isValid()) { + return value; + } return Planar3DOverlay::getProperty(property); } diff --git a/interface/src/ui/overlays/FloatingUIPanel.cpp b/interface/src/ui/overlays/FloatingUIPanel.cpp index a862e4fcd4..eedb4bffd1 100644 --- a/interface/src/ui/overlays/FloatingUIPanel.cpp +++ b/interface/src/ui/overlays/FloatingUIPanel.cpp @@ -20,7 +20,6 @@ #include "Application.h" #include "Base3DOverlay.h" - std::function const FloatingUIPanel::AVATAR_POSITION = []() -> glm::vec3 { return DependencyManager::get()->getMyAvatar()->getPosition(); }; @@ -30,12 +29,6 @@ std::function const FloatingUIPanel::AVATAR_ORIENTATION = []() -> g glm::angleAxis(glm::pi(), IDENTITY_UP); }; -FloatingUIPanel::FloatingUIPanel() : - _anchorPosition(AVATAR_POSITION), - _offsetRotation(AVATAR_ORIENTATION) -{ -} - glm::vec3 FloatingUIPanel::getPosition() const { return getOffsetRotation() * getOffsetPosition() + getAnchorPosition(); } @@ -44,18 +37,30 @@ glm::quat FloatingUIPanel::getRotation() const { return getOffsetRotation() * getFacingRotation(); } -void FloatingUIPanel::setAnchorPosition(glm::vec3 position) { +void FloatingUIPanel::setAnchorPosition(const glm::vec3& position) { setAnchorPosition([position]() -> glm::vec3 { return position; }); } -void FloatingUIPanel::setOffsetRotation(glm::quat rotation) { +void FloatingUIPanel::setOffsetRotation(const glm::quat& rotation) { setOffsetRotation([rotation]() -> glm::quat { return rotation; }); } +void FloatingUIPanel::addChild(unsigned int childId) { + if (!_children.contains(childId)) { + _children.append(childId); + } +} + +void FloatingUIPanel::removeChild(unsigned int childId) { + if (_children.contains(childId)) { + _children.removeOne(childId); + } +} + QScriptValue FloatingUIPanel::getProperty(const QString &property) { if (property == "anchorPosition") { return vec3toScriptValue(_scriptEngine, getAnchorPosition()); @@ -76,15 +81,15 @@ QScriptValue FloatingUIPanel::getProperty(const QString &property) { void FloatingUIPanel::setProperties(const QScriptValue &properties) { QScriptValue anchor = properties.property("anchorPosition"); if (anchor.isValid()) { - QScriptValue type = anchor.property("type"); + QScriptValue bindType = anchor.property("bind"); QScriptValue value = anchor.property("value"); - if (type.isValid()) { - QString typeString = type.toVariant().toString(); - if (typeString == "myAvatar") { + if (bindType.isValid()) { + QString bindTypeString = bindType.toVariant().toString(); + if (bindTypeString == "myAvatar") { setAnchorPosition(AVATAR_POSITION); } else if (value.isValid()) { - if (typeString == "overlay") { + if (bindTypeString == "overlay") { Overlay::Pointer overlay = Application::getInstance()->getOverlays() .getOverlay(value.toVariant().toUInt()); if (overlay->is3D()) { @@ -93,13 +98,13 @@ void FloatingUIPanel::setProperties(const QScriptValue &properties) { return overlay3D->getPosition(); }); } - } else if (typeString == "panel") { + } else if (bindTypeString == "panel") { FloatingUIPanel::Pointer panel = Application::getInstance()->getOverlays() .getPanel(value.toVariant().toUInt()); setAnchorPosition([panel]() -> glm::vec3 { return panel->getPosition(); }); - } else if (typeString == "vec3") { + } else if (bindTypeString == "vec3") { QScriptValue x = value.property("x"); QScriptValue y = value.property("y"); QScriptValue z = value.property("z"); @@ -117,15 +122,15 @@ void FloatingUIPanel::setProperties(const QScriptValue &properties) { QScriptValue offsetRotation = properties.property("offsetRotation"); if (offsetRotation.isValid()) { - QScriptValue type = offsetRotation.property("type"); + QScriptValue bindType = offsetRotation.property("bind"); QScriptValue value = offsetRotation.property("value"); - if (type.isValid()) { - QString typeString = type.toVariant().toString(); - if (typeString == "myAvatar") { + if (bindType.isValid()) { + QString bindTypeString = bindType.toVariant().toString(); + if (bindTypeString == "myAvatar") { setOffsetRotation(AVATAR_ORIENTATION); } else if (value.isValid()) { - if (typeString == "overlay") { + if (bindTypeString == "overlay") { Overlay::Pointer overlay = Application::getInstance()->getOverlays() .getOverlay(value.toVariant().toUInt()); if (overlay->is3D()) { @@ -134,13 +139,13 @@ void FloatingUIPanel::setProperties(const QScriptValue &properties) { return overlay3D->getRotation(); }); } - } else if (typeString == "panel") { + } else if (bindTypeString == "panel") { FloatingUIPanel::Pointer panel = Application::getInstance()->getOverlays() .getPanel(value.toVariant().toUInt()); setOffsetRotation([panel]() -> glm::quat { return panel->getRotation(); }); - } else if (typeString == "quat") { + } else if (bindTypeString == "quat") { QScriptValue x = value.property("x"); QScriptValue y = value.property("y"); QScriptValue z = value.property("z"); @@ -174,7 +179,7 @@ void FloatingUIPanel::setProperties(const QScriptValue &properties) { } QScriptValue facingRotation = properties.property("facingRotation"); - if (offsetRotation.isValid()) { + if (facingRotation.isValid()) { QScriptValue x = facingRotation.property("x"); QScriptValue y = facingRotation.property("y"); QScriptValue z = facingRotation.property("z"); diff --git a/interface/src/ui/overlays/FloatingUIPanel.h b/interface/src/ui/overlays/FloatingUIPanel.h index abc1328928..f65fa93c26 100644 --- a/interface/src/ui/overlays/FloatingUIPanel.h +++ b/interface/src/ui/overlays/FloatingUIPanel.h @@ -12,6 +12,8 @@ #ifndef hifi_FloatingUIPanel_h #define hifi_FloatingUIPanel_h +#include + #include #include #include @@ -21,8 +23,6 @@ class FloatingUIPanel : public QObject { public: typedef std::shared_ptr Pointer; - QList children; - void init(QScriptEngine* scriptEngine) { _scriptEngine = scriptEngine; } glm::vec3 getAnchorPosition() const { return _anchorPosition(); } @@ -33,11 +33,16 @@ public: glm::quat getRotation() const; void setAnchorPosition(const std::function& func) { _anchorPosition = func; } - void setAnchorPosition(glm::vec3 position); + void setAnchorPosition(const glm::vec3& position); void setOffsetRotation(const std::function& func) { _offsetRotation = func; } - void setOffsetRotation(glm::quat rotation); - void setOffsetPosition(glm::vec3 position) { _offsetPosition = position; } - void setFacingRotation(glm::quat rotation) { _facingRotation = rotation; } + void setOffsetRotation(const glm::quat& rotation); + void setOffsetPosition(const glm::vec3& position) { _offsetPosition = position; } + void setFacingRotation(const glm::quat& rotation) { _facingRotation = rotation; } + + const QList& getChildren() { return _children; } + void addChild(unsigned int childId); + void removeChild(unsigned int childId); + unsigned int popLastChild() { return _children.takeLast(); } QScriptValue getProperty(const QString& property); void setProperties(const QScriptValue& properties); @@ -46,11 +51,12 @@ private: static std::function const AVATAR_POSITION; static std::function const AVATAR_ORIENTATION; - std::function _anchorPosition; - std::function _offsetRotation; + std::function _anchorPosition{AVATAR_POSITION}; + std::function _offsetRotation{AVATAR_ORIENTATION}; glm::vec3 _offsetPosition{0, 0, 0}; glm::quat _facingRotation{1, 0, 0, 0}; QScriptEngine* _scriptEngine; + QList _children; }; #endif // hifi_FloatingUIPanel_h diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index fb9f64c84e..bce219b4b4 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -135,23 +135,6 @@ Overlay::Pointer Overlays::getOverlay(unsigned int id) const { return nullptr; } -void Overlays::setAttachedPanel(Overlay::Pointer overlay, unsigned int overlayId, const QScriptValue& property) { - auto attachable = std::dynamic_pointer_cast(overlay); - if (attachable) { - if (property.isValid()) { - unsigned int attachedPanelId = property.toVariant().toUInt(); - if (_panels.contains(attachedPanelId)) { - auto panel = _panels[attachedPanelId]; - panel->children.append(overlayId); - attachable->setAttachedPanel(panel); - } else { - attachable->getAttachedPanel()->children.removeAll(overlayId); - attachable->setAttachedPanel(nullptr); - } - } - } -} - unsigned int Overlays::addOverlay(const QString& type, const QScriptValue& properties) { Overlay::Pointer thisOverlay = nullptr; @@ -183,9 +166,7 @@ unsigned int Overlays::addOverlay(const QString& type, const QScriptValue& prope if (thisOverlay) { thisOverlay->setProperties(properties); - unsigned int overlayId = addOverlay(thisOverlay); - setAttachedPanel(thisOverlay, overlayId, properties.property("attachedPanel")); - return overlayId; + return addOverlay(thisOverlay); } return 0; } @@ -252,8 +233,6 @@ bool Overlays::editOverlay(unsigned int id, const QScriptValue& properties) { thisOverlay->setProperties(properties); } - setAttachedPanel(thisOverlay, id, properties.property("attachedPanel")); - return true; } return false; @@ -273,8 +252,16 @@ void Overlays::deleteOverlay(unsigned int id) { } } + auto attachable = std::dynamic_pointer_cast(overlayToDelete); + if (attachable && attachable->getAttachedPanel()) { + attachable->getAttachedPanel()->removeChild(id); + attachable->setAttachedPanel(nullptr); + } + QWriteLocker lock(&_deleteLock); _overlaysToDelete.push_back(overlayToDelete); + + emit overlayDeleted(id); } QString Overlays::getOverlayType(unsigned int overlayId) const { @@ -285,6 +272,33 @@ QString Overlays::getOverlayType(unsigned int overlayId) const { return ""; } +unsigned int Overlays::getAttachedPanel(unsigned int childId) const { + Overlay::Pointer overlay = getOverlay(childId); + auto attachable = std::dynamic_pointer_cast(overlay); + if (attachable) { + return _panels.key(attachable->getAttachedPanel()); + } + return 0; +} + +void Overlays::setAttachedPanel(unsigned int childId, unsigned int panelId) { + Overlay::Pointer overlay = getOverlay(childId); + auto attachable = std::dynamic_pointer_cast(overlay); + if (attachable) { + if (_panels.contains(panelId)) { + auto panel = _panels[panelId]; + panel->addChild(childId); + attachable->setAttachedPanel(panel); + } else { + auto panel = attachable->getAttachedPanel(); + if (panel) { + panel->removeChild(childId); + attachable->setAttachedPanel(nullptr); + } + } + } +} + unsigned int Overlays::getOverlayAtPoint(const glm::vec2& point) { glm::vec2 pointCopy = point; if (qApp->isHMDMode()) { @@ -328,16 +342,7 @@ OverlayPropertyResult Overlays::getProperty(unsigned int id, const QString& prop Overlay::Pointer thisOverlay = getOverlay(id); QReadLocker lock(&_lock); if (thisOverlay) { - if (property == "attachedPanel") { - auto panelAttachable = std::dynamic_pointer_cast(thisOverlay); - if (panelAttachable) { - result.value = _panels.key(panelAttachable->getAttachedPanel()); - } else { - result.value = 0; - } - } else { - result.value = thisOverlay->getProperty(property); - } + result.value = thisOverlay->getProperty(property); } return result; } @@ -553,7 +558,9 @@ void Overlays::deletePanel(unsigned int panelId) { } } - while (!panelToDelete->children.isEmpty()) { - deleteOverlay(panelToDelete->children.takeLast()); + while (!panelToDelete->getChildren().isEmpty()) { + deleteOverlay(panelToDelete->popLastChild()); } + + emit panelDeleted(panelId); } diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index ee4fa5b7d4..ce2c3efeae 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -87,9 +87,12 @@ public slots: /// deletes a particle void deleteOverlay(unsigned int id); - /// + /// get the string type of the overlay used in addOverlay QString getOverlayType(unsigned int overlayId) const; + unsigned int getAttachedPanel(unsigned int childId) const; + void setAttachedPanel(unsigned int childId, unsigned int panelId); + /// returns the top most 2D overlay at the screen point, or 0 if not overlay at that point unsigned int getOverlayAtPoint(const glm::vec2& point); @@ -122,9 +125,12 @@ public slots: /// deletes a panel and all child overlays void deletePanel(unsigned int panelId); +signals: + void overlayDeleted(unsigned int id); + void panelDeleted(unsigned int id); + private: void cleanupOverlaysToDelete(); - void setAttachedPanel(Overlay::Pointer overlay, unsigned int overlayId, const QScriptValue& property); QMap _overlaysHUD; QMap _overlaysWorld; diff --git a/interface/src/ui/overlays/PanelAttachable.cpp b/interface/src/ui/overlays/PanelAttachable.cpp index bedf7d9fd5..e114fa143e 100644 --- a/interface/src/ui/overlays/PanelAttachable.cpp +++ b/interface/src/ui/overlays/PanelAttachable.cpp @@ -11,6 +11,8 @@ #include "PanelAttachable.h" +#include + PanelAttachable::PanelAttachable() : _attachedPanel(nullptr), _facingRotation(1, 0, 0, 0) @@ -33,3 +35,47 @@ void PanelAttachable::setTransforms(Transform* transform) { transform->postRotate(getFacingRotation() * getAttachedPanel()->getFacingRotation()); } } + +QScriptValue PanelAttachable::getProperty(QScriptEngine* scriptEngine, const QString &property) { + if (property == "offsetPosition") { + return vec3toScriptValue(scriptEngine, getOffsetPosition()); + } + if (property == "facingRotation") { + return quatToScriptValue(scriptEngine, getFacingRotation()); + } + return QScriptValue(); +} + +void PanelAttachable::setProperties(const QScriptValue &properties) { + QScriptValue offsetPosition = properties.property("offsetPosition"); + if (offsetPosition.isValid()) { + QScriptValue x = offsetPosition.property("x"); + QScriptValue y = offsetPosition.property("y"); + QScriptValue z = offsetPosition.property("z"); + + if (x.isValid() && y.isValid() && z.isValid()) { + glm::vec3 newPosition; + newPosition.x = x.toVariant().toFloat(); + newPosition.y = y.toVariant().toFloat(); + newPosition.z = z.toVariant().toFloat(); + setOffsetPosition(newPosition); + } + } + + QScriptValue facingRotation = properties.property("facingRotation"); + if (facingRotation.isValid()) { + QScriptValue x = facingRotation.property("x"); + QScriptValue y = facingRotation.property("y"); + QScriptValue z = facingRotation.property("z"); + QScriptValue w = facingRotation.property("w"); + + if (x.isValid() && y.isValid() && z.isValid() && w.isValid()) { + glm::quat newRotation; + newRotation.x = x.toVariant().toFloat(); + newRotation.y = y.toVariant().toFloat(); + newRotation.z = z.toVariant().toFloat(); + newRotation.w = w.toVariant().toFloat(); + setFacingRotation(newRotation); + } + } +} diff --git a/interface/src/ui/overlays/PanelAttachable.h b/interface/src/ui/overlays/PanelAttachable.h index 097fc9a517..e1fc490d00 100644 --- a/interface/src/ui/overlays/PanelAttachable.h +++ b/interface/src/ui/overlays/PanelAttachable.h @@ -27,8 +27,11 @@ public: glm::quat getFacingRotation() const { return _facingRotation; } void setAttachedPanel(FloatingUIPanel::Pointer panel) { _attachedPanel = panel; } - void setOffsetPosition(glm::vec3 position) { _offsetPosition = position; } - void setFacingRotation(glm::quat rotation) { _facingRotation = rotation; } + void setOffsetPosition(const glm::vec3& position) { _offsetPosition = position; } + void setFacingRotation(const glm::quat& rotation) { _facingRotation = rotation; } + + QScriptValue getProperty(QScriptEngine* scriptEngine, const QString& property); + void setProperties(const QScriptValue& properties); protected: void setTransforms(Transform* transform); From ba190b4b3453a3908a73ad36b3d51640d4386ead Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 24 Jul 2015 18:00:33 -0700 Subject: [PATCH 206/242] Improve JavaScript abstraction layer for overlays. Also move it to it's own file. --- examples/libraries/overlayManager.js | 451 +++++++++++++++++++++++++++ examples/libraries/overlayUtils.js | 259 +-------------- 2 files changed, 459 insertions(+), 251 deletions(-) create mode 100644 examples/libraries/overlayManager.js diff --git a/examples/libraries/overlayManager.js b/examples/libraries/overlayManager.js new file mode 100644 index 0000000000..55575badee --- /dev/null +++ b/examples/libraries/overlayManager.js @@ -0,0 +1,451 @@ +// +// overlayManager.js +// examples/libraries +// +// Created by Zander Otavka on 7/24/15 +// Copyright 2015 High Fidelity, Inc. +// +// Manage overlays with object oriented goodness, instead of ugly `Overlays.h` methods. +// Instead of: +// +// var billboard = Overlays.addOverlay("billboard", { visible: false }); +// ... +// Overlays.editOverlay(billboard, { visible: true }); +// ... +// Overlays.deleteOverlay(billboard); +// +// You can now do: +// +// var billboard = new BillboardOverlay({ visible: false }); +// ... +// billboard.visible = true; +// ... +// billboard.destroy(); +// +// See more on usage below. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +(function() { + // Delete `Overlays` from the global scope. + var Overlays = this.Overlays; + delete this.Overlays; + + var overlays = {}; + var panels = {}; + + var overlayTypes; + var Overlay, Overlay2D, Base3DOverlay, Planar3DOverlay, Volume3DOverlay; + + + // + // Create a new JavaScript object for an overlay of given ID. + // + function makeOverlayFromId(id) { + var type = Overlays.getOverlayType(id); + if (!type) { + return null; + } + var overlay = new overlayTypes[type](); + overlay._id = id; + var panelID = Overlays.getAttachedPanel(id) + if (panelID && panelID in panels) { + panels[panelID].addChild(overlay); + } + overlays[id] = overlay; + return overlay; + } + + // + // Get or create an overlay object from the id. + // + // @param knownOverlaysOnly (Optional: Boolean) + // If true, a new object will not be created. + // @param searchList (Optional: Object) + // Map of overlay id's and overlay objects. Can be generated with + // `OverlayManager.makeSearchList`. + // + function findOverlay(id, knownOverlaysOnly, searchList) { + if (id > 0) { + knownOverlaysOnly = Boolean(knownOverlaysOnly) || Boolean(searchList); + searchList = searchList || overlays; + var foundOverlay = searchList[id]; + if (foundOverlay) { + return foundOverlay; + } + if (!knownOverlaysOnly) { + return makeOverlayFromId(id); + } + } + return null; + } + + + // + // Perform global scoped operations on overlays, such as finding by ray intersection. + // + OverlayManager = { + findOnRay: function(pickRay, knownOverlaysOnly, searchList) { + var rayPickResult = Overlays.findRayIntersection(pickRay); + print("raypick " + rayPickResult.overlayID); + if (rayPickResult.intersects) { + return findOverlay(rayPickResult.overlayID, knownOverlaysOnly, searchList); + } + return null; + }, + findAtPoint: function(point, knownOverlaysOnly, searchList) { + var foundID = Overlays.getOverlayAtPoint(point); + print("at point " + foundID); + if (foundID) { + return findOverlay(foundID, knownOverlaysOnly, searchList); + } else { + var pickRay = Camera.computePickRay(point.x, point.y); + return OverlayManager.findOnRay(pickRay, knownOverlaysOnly, searchList); + } + }, + makeSearchList: function(overlayArray) { + var searchList = {}; + overlayArray.forEach(function(overlay){ + searchList[overlay._id] = overlay; + }); + return searchList; + } + }; + + + // + // Object oriented abstraction layer for overlays. + // + // Usage: + // // Create an overlay + // var billboard = new BillboardOverlay({ + // visible: true, + // isFacingAvatar: true, + // ignoreRayIntersections: false + // }); + // + // // Get a property + // var isVisible = billboard.visible; + // + // // Set a single property + // billboard.position = { x: 1, y: 3, z: 2 }; + // + // // Set multiple properties at the same time + // billboard.setProperties({ + // url: "http://images.com/overlayImage.jpg", + // dimensions: { x: 2, y: 2 } + // }); + // + // // Clone an overlay + // var clonedBillboard = billboard.clone(); + // + // // Remove an overlay from the world + // billboard.destroy(); + // + // // Remember, there is a poor orphaned JavaScript object left behind. You should + // // remove any references to it so you don't accidentally try to modify an overlay that + // // isn't there. + // billboard = undefined; + // + (function() { + var ABSTRACT = null; + overlayTypes = {}; + + function generateOverlayClass(superclass, type, properties) { + var that; + if (type == ABSTRACT) { + that = function(type, params) { + superclass.call(this, type, params); + }; + } else { + that = function(params) { + superclass.call(this, type, params); + }; + overlayTypes[type] = that; + } + + that.prototype = new superclass(); + that.prototype.constructor = that; + + properties.forEach(function(prop) { + Object.defineProperty(that.prototype, prop, { + get: function() { + return Overlays.getProperty(this._id, prop); + }, + set: function(newValue) { + var keyValuePair = {}; + keyValuePair[prop] = newValue; + this.setProperties(keyValuePair); + }, + configurable: false + }); + }); + + return that; + } + + // Supports multiple inheritance of properties. Just `concat` them onto the end of the + // properties list. + var PANEL_ATTACHABLE_FIELDS = ["offsetPosition", "facingRotation"]; + + Overlay = (function() { + var that = function(type, params) { + if (type && params) { + this._id = Overlays.addOverlay(type, params); + overlays[this._id] = this; + } else { + this._id = 0; + } + this._attachedPanelPointer = null; + }; + + that.prototype.constructor = that; + + Object.defineProperty(that.prototype, "isLoaded", { + get: function() { + return Overlays.isLoaded(this._id); + } + }); + + Object.defineProperty(that.prototype, "attachedPanel", { + get: function() { + return this._attachedPanelPointer; + } + }); + + that.prototype.getTextSize = function(text) { + return Overlays.textSize(this._id, text); + }; + + that.prototype.setProperties = function(properties) { + Overlays.editOverlay(this._id, properties); + }; + + that.prototype.clone = function() { + return makeOverlayFromId(Overlays.cloneOverlay(this._id)); + }; + + that.prototype.destroy = function() { + Overlays.deleteOverlay(this._id); + }; + + return generateOverlayClass(that, ABSTRACT, [ + "alpha", "glowLevel", "pulseMax", "pulseMin", "pulsePeriod", "glowLevelPulse", + "alphaPulse", "colorPulse", "visible", "anchor" + ]); + })(); + + Overlay2D = generateOverlayClass(Overlay, ABSTRACT, [ + "bounds", "x", "y", "width", "height" + ]); + + Base3DOverlay = generateOverlayClass(Overlay, ABSTRACT, [ + "position", "lineWidth", "rotation", "isSolid", "isFilled", "isWire", "isDashedLine", + "ignoreRayIntersection", "drawInFront", "drawOnHUD" + ]); + + Planar3DOverlay = generateOverlayClass(Base3DOverlay, ABSTRACT, [ + "dimensions" + ]); + + Volume3DOverlay = generateOverlayClass(Base3DOverlay, ABSTRACT, [ + "dimensions" + ]); + + generateOverlayClass(Overlay2D, "image", [ + "subImage", "imageURL" + ]); + + generateOverlayClass(Overlay2D, "text", [ + "font", "text", "backgroundColor", "backgroundAlpha", "leftMargin", "topMargin" + ]); + + generateOverlayClass(Planar3DOverlay, "text3d", [ + "text", "backgroundColor", "backgroundAlpha", "lineHeight", "leftMargin", "topMargin", + "rightMargin", "bottomMargin", "isFacingAvatar" + ]); + + generateOverlayClass(Volume3DOverlay, "cube", [ + "borderSize" + ]); + + generateOverlayClass(Volume3DOverlay, "sphere", [ + ]); + + generateOverlayClass(Planar3DOverlay, "circle3d", [ + "startAt", "endAt", "outerRadius", "innerRadius", "hasTickMarks", + "majorTickMarksAngle", "minorTickMarksAngle", "majorTickMarksLength", + "minorTickMarksLength", "majorTickMarksColor", "minorTickMarksColor" + ]); + + generateOverlayClass(Planar3DOverlay, "rectangle3d", [ + ]); + + generateOverlayClass(Base3DOverlay, "line3d", [ + "start", "end" + ]); + + generateOverlayClass(Planar3DOverlay, "grid", [ + "minorGridWidth", "majorGridEvery" + ]); + + generateOverlayClass(Volume3DOverlay, "localmodels", [ + ]); + + generateOverlayClass(Volume3DOverlay, "model", [ + "url", "dimensions", "textures" + ]); + + generateOverlayClass(Planar3DOverlay, "billboard", [ + "url", "subImage", "isFacingAvatar" + ].concat(PANEL_ATTACHABLE_FIELDS)); + })(); + + ImageOverlay = overlayTypes["image"]; + TextOverlay = overlayTypes["text"]; + Text3DOverlay = overlayTypes["text3d"]; + Cube3DOverlay = overlayTypes["cube"]; + Sphere3DOverlay = overlayTypes["sphere"]; + Circle3DOverlay = overlayTypes["circle3d"]; + Rectangle3DOverlay = overlayTypes["rectangle3d"]; + Line3DOverlay = overlayTypes["line3d"]; + Grid3DOverlay = overlayTypes["grid"]; + LocalModelsOverlay = overlayTypes["localmodels"]; + ModelOverlay = overlayTypes["model"]; + BillboardOverlay = overlayTypes["billboard"]; + + + // + // Object oriented abstraction layer for panels. + // + FloatingUIPanel = (function() { + var that = function(params) { + this._id = Overlays.addPanel(params); + this._children = []; + this._visible = Boolean(params.visible); + panels[this._id] = this; + this._attachedPanelPointer = null; + }; + + that.prototype.constructor = that; + + var FIELDS = ["offsetPosition", "offsetRotation", "facingRotation"]; + FIELDS.forEach(function(prop) { + Object.defineProperty(that.prototype, prop, { + get: function() { + return Overlays.getPanelProperty(this._id, prop); + }, + set: function(newValue) { + var keyValuePair = {}; + keyValuePair[prop] = newValue; + this.setProperties(keyValuePair); + }, + configurable: false + }); + }); + + var PSEUDO_FIELDS = []; + + PSEUDO_FIELDS.push("children"); + Object.defineProperty(that.prototype, "children", { + get: function() { + return this._children.slice(); + } + }); + + PSEUDO_FIELDS.push("visible"); + Object.defineProperty(that.prototype, "visible", { + get: function() { + return this._visible; + }, + set: function(visible) { + this._visible = visible; + this._children.forEach(function(child) { + child.visible = visible; + }); + } + }); + + that.prototype.addChild = function(child) { + if (child instanceof Overlay) { + Overlays.setAttachedPanel(child._id, this._id); + } else if (child instanceof FloatingUIPanel) { + child.setProperties({ + anchorPosition: { + bind: "panel", + value: this._id + }, + offsetRotation: { + bind: "panel", + value: this._id + } + }); + } + child._attachedPanelPointer = this; + child.visible = this.visible; + this._children.push(child); + return child; + }; + + that.prototype.removeChild = function(child) { + var i = this._children.indexOf(child); + if (i >= 0) { + if (child instanceof Overlay) { + Overlays.setAttachedPanel(child._id, 0); + } else if (child instanceof FloatingUIPanel) { + child.setProperties({ + anchorPosition: { + bind: "myAvatar" + }, + offsetRotation: { + bind: "myAvatar" + } + }); + } + child._attachedPanelPointer = null; + this._children.splice(i, 1); + } + }; + + that.prototype.setProperties = function(properties) { + for (var i in PSEUDO_FIELDS) { + if (properties[PSEUDO_FIELDS[i]] !== undefined) { + this[PSEUDO_FIELDS[i]] = properties[PSEUDO_FIELDS[i]]; + } + } + Overlays.editPanel(this._id, properties); + }; + + that.prototype.destroy = function() { + Overlays.deletePanel(this._id); + }; + + return that; + })(); + + + function onOverlayDeleted(id) { + if (id in overlays) { + if (overlays[id]._attachedPanelPointer) { + overlays[id]._attachedPanelPointer.removeChild(overlays[id]); + } + delete overlays[id]; + } + } + + function onPanelDeleted(id) { + if (id in panels) { + panels[id]._children.forEach(function(child) { + print(JSON.stringify(child.destroy)); + child.destroy(); + }); + delete panels[id]; + } + } + + Overlays.overlayDeleted.connect(onOverlayDeleted); + Overlays.panelDeleted.connect(onPanelDeleted); +})(); diff --git a/examples/libraries/overlayUtils.js b/examples/libraries/overlayUtils.js index 3acab0103b..a5622ec435 100644 --- a/examples/libraries/overlayUtils.js +++ b/examples/libraries/overlayUtils.js @@ -2,39 +2,16 @@ // overlayUtils.js // examples/libraries // -// Modified by Zander Otavka on 7/15/15 -// Copyright 2014 High Fidelity, Inc. -// -// Manage overlays with object oriented goodness, instead of ugly `Overlays.h` methods. -// Instead of: -// -// var billboard = Overlays.addOverlay("billboard", { visible: false }); -// ... -// Overlays.editOverlay(billboard, { visible: true }); -// ... -// Overlays.deleteOverlay(billboard); -// -// You can now do: -// -// var billboard = new BillboardOverlay({ visible: false }); -// ... -// billboard.visible = true; -// ... -// billboard.destroy(); -// -// See more on usage below. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// Copyright 2015 High Fidelity, Inc. // -/** - * DEPRECATION WARNING: Will be deprecated soon in favor of FloatingUIPanel. - * - * OverlayGroup provides a way to create composite overlays and control their - * position relative to a settable rootPosition and rootRotation. - */ +// +// DEPRECATION WARNING: Will be deprecated soon in favor of FloatingUIPanel. +// +// OverlayGroup provides a way to create composite overlays and control their +// position relative to a settable rootPosition and rootRotation. +// OverlayGroup = function(opts) { var that = {}; @@ -92,226 +69,6 @@ OverlayGroup = function(opts) { } overlays = {}; } - + return that; }; - - -/** - * Object oriented abstraction layer for overlays. - * - * Usage: - * // Create an overlay - * var billboard = new BillboardOverlay({ - * visible: true, - * isFacingAvatar: true, - * ignoreRayIntersections: false - * }); - * - * // Get a property - * var isVisible = billboard.visible; - * - * // Set a single property - * billboard.position = { x: 1, y: 3, z: 2 }; - * - * // Set multiple properties at the same time - * billboard.setProperties({ - * url: "http://images.com/overlayImage.jpg", - * dimensions: { x: 2, y: 2 } - * }); - * - * // Clone an overlay - * var clonedBillboard = billboard.clone(); - * - * // Remove an overlay from the world - * billboard.destroy(); - * - * // Remember, there is a poor orphaned JavaScript object left behind. You should remove any - * // references to it so you don't accidentally try to modify an overlay that isn't there. - * billboard = undefined; - */ -(function() { - var ABSTRACT = null; - - function generateOverlayClass(superclass, type, properties) { - var that; - if (type == ABSTRACT) { - that = function(type, params) { - superclass.apply(this, [type, params]); - }; - } else { - that = function(params) { - superclass.apply(this, [type, params]); - }; - } - - that.prototype = new superclass(); - that.prototype.constructor = that; - - properties.forEach(function(prop) { - Object.defineProperty(that.prototype, prop, { - get: function() { - return Overlays.getProperty(this._id, prop); - }, - set: function(newValue) { - var keyValuePair = {}; - keyValuePair[prop] = newValue; - this.setProperties(keyValuePair); - }, - configurable: true - }); - }); - - return that; - } - - - // Supports multiple inheritance of properties. Just `concat` them onto the end of the - // properties list. - var PANEL_ATTACHABLE_FIELDS = ["attachedPanel"]; - - // TODO: finish exposing all overlay classes. - - var Overlay = (function() { - var BaseOverlay = (function() { - var that = function(type, params) { - Object.apply(this, []); - if (type && params) { - this._type = type; - this._id = Overlays.addOverlay(type, params); - } else { - this._type = ""; - this._id = 0; - } - this._attachedPanelPointer = null; - }; - - that.prototype = new Object(); - that.prototype.constructor = that; - - Object.defineProperty(that.prototype, "overlayType", { - get: function() { - return this._type; - } - }); - - that.prototype.setProperties = function(properties) { - Overlays.editOverlay(this._id, properties); - }; - - that.prototype.clone = function() { - var clone = new this.constructor(); - clone._type = this._type; - clone._id = Overlays.cloneOverlay(this._id); - if (this._attachedPanelPointer) { - this._attachedPanelPointer.addChild(clone); - } - return clone; - }; - - that.prototype.destroy = function() { - Overlays.deleteOverlay(this._id); - }; - - return that; - }()); - - return generateOverlayClass(BaseOverlay, ABSTRACT, [ - "alpha", "glowLevel", "pulseMax", "pulseMin", "pulsePeriod", "glowLevelPulse", - "alphaPulse", "colorPulse", "visible", "anchor" - ]); - }()); - - var Base3DOverlay = generateOverlayClass(Overlay, ABSTRACT, [ - "position", "lineWidth", "rotation", "isSolid", "isFilled", "isWire", "isDashedLine", - "ignoreRayIntersection", "drawInFront", "drawOnHUD" - ]); - - var Planar3DOverlay = generateOverlayClass(Base3DOverlay, ABSTRACT, [ - "dimensions" - ]); - - BillboardOverlay = generateOverlayClass(Planar3DOverlay, "billboard", [ - "url", "subImage", "isFacingAvatar", "offsetPosition" - ].concat(PANEL_ATTACHABLE_FIELDS)); -}()); - - -/** - * Object oriented abstraction layer for panels. - */ -FloatingUIPanel = (function() { - var that = function(params) { - this._id = Overlays.addPanel(params); - this._children = []; - }; - - var FIELDS = ["offsetPosition", "offsetRotation", "facingRotation"]; - FIELDS.forEach(function(prop) { - Object.defineProperty(that.prototype, prop, { - get: function() { - return Overlays.getPanelProperty(this._id, prop); - }, - set: function(newValue) { - var keyValuePair = {}; - keyValuePair[prop] = newValue; - this.setProperties(keyValuePair); - }, - configurable: false - }); - }); - - Object.defineProperty(that.prototype, "children", { - get: function() { - return this._children.slice(); - } - }) - - that.prototype.addChild = function(overlay) { - overlay.attachedPanel = this._id; - overlay._attachedPanelPointer = this; - this._children.push(overlay); - return overlay; - }; - - that.prototype.removeChild = function(overlay) { - var i = this._children.indexOf(overlay); - if (i >= 0) { - overlay.attachedPanel = 0; - overlay._attachedPanelPointer = null; - this._children.splice(i, 1); - } - }; - - that.prototype.setVisible = function(visible) { - for (var i in this._children) { - this._children[i].visible = visible; - } - }; - - that.prototype.setProperties = function(properties) { - Overlays.editPanel(this._id, properties); - }; - - that.prototype.destroy = function() { - Overlays.deletePanel(this._id); - var i = _panels.indexOf(this); - if (i >= 0) { - _panels.splice(i, 1); - } - }; - - that.prototype.findRayIntersection = function(pickRay) { - var rayPickResult = Overlays.findRayIntersection(pickRay); - if (rayPickResult.intersects) { - for (var i in this._children) { - if (this._children[i]._id == rayPickResult.overlayID) { - return this._children[i]; - } - } - } - return null; - }; - - return that; -}()); From 37a1ad76262a1b9b7e109548e012ee7640e0777b Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 24 Jul 2015 18:02:18 -0700 Subject: [PATCH 207/242] Improve floatingUIExample.js. Demonstrates overlayManager.js functionality. --- examples/example/ui/floatingUIExample.js | 247 ++++++++++++----------- 1 file changed, 133 insertions(+), 114 deletions(-) diff --git a/examples/example/ui/floatingUIExample.js b/examples/example/ui/floatingUIExample.js index 3b555efce3..9c4e43be94 100644 --- a/examples/example/ui/floatingUIExample.js +++ b/examples/example/ui/floatingUIExample.js @@ -10,140 +10,159 @@ // Script.include([ - "../../libraries/globals.js", - "../../libraries/overlayUtils.js", + "../../libraries/globals.js", + "../../libraries/overlayManager.js", ]); var BG_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/card-bg.svg"; var RED_DOT_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/red-dot.svg"; var BLUE_SQUARE_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/blue-square.svg"; -var BLANK_ROTATION = { x: 0, y: 0, z: 0, w: 0 }; - -function isBlank(rotation) { - return rotation.x == BLANK_ROTATION.x && - rotation.y == BLANK_ROTATION.y && - rotation.z == BLANK_ROTATION.z && - rotation.w == BLANK_ROTATION.w; -} - -var panel = new FloatingUIPanel({ - offsetPosition: { x: 0, y: 0, z: 1 }, +var mainPanel = new FloatingUIPanel({ + offsetRotation: { + bind: "quat", + value: { w: 1, x: 0, y: 0, z: 0 } + }, + offsetPosition: { x: 0, y: 0.4, z: 1 } }); -var bg = panel.addChild(new BillboardOverlay({ - url: BG_IMAGE_URL, - dimensions: { - x: 0.5, - y: 0.5, - }, - isFacingAvatar: false, - visible: true, - alpha: 1.0, - ignoreRayIntersection: false, +var bluePanel = mainPanel.addChild(new FloatingUIPanel ({ + offsetPosition: { x: 0.1, y: 0.1, z: -0.2 } })); -var redDot = panel.addChild(new BillboardOverlay({ - url: RED_DOT_IMAGE_URL, - dimensions: { - x: 0.1, - y: 0.1, - }, - isFacingAvatar: false, - visible: true, - alpha: 1.0, - ignoreRayIntersection: false, - offsetPosition: { - x: -0.15, - y: -0.15, - z: -0.001 - } +var mainPanelBackground = new BillboardOverlay({ + url: BG_IMAGE_URL, + dimensions: { + x: 0.5, + y: 0.5, + }, + isFacingAvatar: false, + alpha: 1.0, + ignoreRayIntersection: false, + offsetPosition: { + x: 0, + y: 0, + z: 0.001 + } +}); + +var bluePanelBackground = mainPanelBackground.clone(); +bluePanelBackground.dimensions = { + x: 0.3, + y: 0.3 +}; + +mainPanel.addChild(mainPanelBackground); +bluePanel.addChild(bluePanelBackground); + +var redDot = mainPanel.addChild(new BillboardOverlay({ + url: RED_DOT_IMAGE_URL, + dimensions: { + x: 0.1, + y: 0.1, + }, + isFacingAvatar: false, + alpha: 1.0, + ignoreRayIntersection: false, + offsetPosition: { + x: -0.15, + y: -0.15, + z: 0 + } })); -var redDot2 = panel.addChild(new BillboardOverlay({ - url: RED_DOT_IMAGE_URL, - dimensions: { - x: 0.1, - y: 0.1, - }, - isFacingAvatar: false, - visible: true, - alpha: 1.0, - ignoreRayIntersection: false, - offsetPosition: { - x: -0.15, - y: 0, - z: -0.001 - } +var redDot2 = mainPanel.addChild(new BillboardOverlay({ + url: RED_DOT_IMAGE_URL, + dimensions: { + x: 0.1, + y: 0.1, + }, + isFacingAvatar: false, + alpha: 1.0, + ignoreRayIntersection: false, + offsetPosition: { + x: -0.155, + y: 0.005, + z: 0 + } })); -var blueSquare = panel.addChild(new BillboardOverlay({ - url: BLUE_SQUARE_IMAGE_URL, - dimensions: { - x: 0.1, - y: 0.1, - }, - isFacingAvatar: false, - visible: true, - alpha: 1.0, - ignoreRayIntersection: false, - offsetPosition: { - x: 0.1, - y: 0, - z: -0.001 - } +var blueSquare = bluePanel.addChild(new BillboardOverlay({ + url: BLUE_SQUARE_IMAGE_URL, + dimensions: { + x: 0.1, + y: 0.1, + }, + isFacingAvatar: false, + alpha: 1.0, + ignoreRayIntersection: false, + offsetPosition: { + x: 0.055, + y: -0.055, + z: 0 + } })); -var blueSquare2 = panel.addChild(new BillboardOverlay({ - url: BLUE_SQUARE_IMAGE_URL, - dimensions: { - x: 0.1, - y: 0.1, - }, - isFacingAvatar: false, - visible: true, - alpha: 1.0, - ignoreRayIntersection: false, - offsetPosition: { - x: 0.1, - y: 0.11, - z: -0.001 - } +var blueSquare2 = bluePanel.addChild(new BillboardOverlay({ + url: BLUE_SQUARE_IMAGE_URL, + dimensions: { + x: 0.1, + y: 0.1, + }, + isFacingAvatar: false, + alpha: 1.0, + ignoreRayIntersection: false, + offsetPosition: { + x: 0.055, + y: 0.055, + z: 0 + } })); var blueSquare3 = blueSquare2.clone(); blueSquare3.offsetPosition = { - x: -0.01, - y: 0.11, - z: -0.001 + x: -0.055, + y: 0.055, + z: 0 }; -blueSquare3.ignoreRayIntersection = false; -Controller.mousePressEvent.connect(function(event) { - if (event.isRightButton) { - var newOffsetRotation; - print(JSON.stringify(panel.offsetRotation)) - if (isBlank(panel.offsetRotation)) { - newOffsetRotation = Quat.multiply(MyAvatar.orientation, { x: 0, y: 1, z: 0, w: 0 }); - } else { - newOffsetRotation = BLANK_ROTATION; - } - panel.offsetRotation = newOffsetRotation; - } else if (event.isLeftButton) { - var pickRay = Camera.computePickRay(event.x, event.y) - var overlay = panel.findRayIntersection(pickRay); - if (overlay) { - var oldPos = overlay.offsetPosition; - var newPos = { - x: Number(oldPos.x), - y: Number(oldPos.y), - z: Number(oldPos.z) + 0.1 - } - overlay.offsetPosition = newPos; - } - } -}); -Script.scriptEnding.connect(function() { - panel.destroy(); -}); \ No newline at end of file +function onMouseDown(event) { + isLeftClick = event.isLeftButton; + isRightClick = event.isRightButton; +} + +function onMouseMove(event) { + isLeftClick = isRightClick = false; +} + +function onMouseUp(event) { + if (isLeftClick && event.isLeftButton) { + var overlay = OverlayManager.findAtPoint({ x: event.x, y: event.y }); + print(overlay.attachedPanel); + if (overlay.attachedPanel === bluePanel) { + overlay.destroy(); + } else if (overlay) { + var oldPos = overlay.offsetPosition; + var newPos = { + x: Number(oldPos.x), + y: Number(oldPos.y), + z: Number(oldPos.z) + 0.1 + }; + overlay.offsetPosition = newPos; + } + } + if (isRightClick && event.isRightButton) { + mainPanel.visible = !mainPanel.visible; + } + isLeftClick = isRightClick = false; +} + +function onScriptEnd() { + mainPanel.destroy(); +} + +Controller.mousePressEvent.connect(onMouseDown); +Controller.mouseMoveEvent.connect(onMouseMove); +Controller.mouseReleaseEvent.connect(onMouseUp); +Script.scriptEnding.connect(onScriptEnd); \ No newline at end of file From 3983c31219f4de1b93fb861d83e2fe9bebb13fcf Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 24 Jul 2015 18:03:21 -0700 Subject: [PATCH 208/242] Add controlPanel.js script. Right click to open a control panel. Control panel allows toggling of face mute, mic mute, and address bar. --- examples/controlPanel.js | 221 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 examples/controlPanel.js diff --git a/examples/controlPanel.js b/examples/controlPanel.js new file mode 100644 index 0000000000..6373dfc921 --- /dev/null +++ b/examples/controlPanel.js @@ -0,0 +1,221 @@ +// +// controlPanel.js +// examples +// +// Created by Zander Otavka on 7/15/15. +// Copyright 2015 High Fidelity, Inc. +// +// Shows a few common controls in a FloatingUIPanel on right click. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include([ + "libraries/globals.js", + "libraries/overlayManager.js", +]); + +var BG_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/card-bg.svg"; +var CLOSE_IMAGE_URL = "file:///Users/zander/Desktop/assets/close.svg"; +var MIC_IMAGE_URL = "file:///Users/zander/Desktop/assets/mic-composite.svg"; +var FACE_IMAGE_URL = "file:///Users/zander/Desktop/assets/face-composite.svg"; +var ADDRESS_BAR_IMAGE_URL = "file:///Users/zander/Desktop/assets/address-bar-composite.svg"; + +var panel = new FloatingUIPanel({ + anchorPosition: { + bind: "myAvatar" + }, + offsetPosition: { x: 0, y: 0.4, z: 1 } +}); + +var background = new BillboardOverlay({ + url: BG_IMAGE_URL, + dimensions: { + x: 0.5, + y: 0.5, + }, + isFacingAvatar: false, + visible: true, + alpha: 1.0, + ignoreRayIntersection: false +}); +panel.addChild(background); + +var closeButton = new BillboardOverlay({ + url: CLOSE_IMAGE_URL, + dimensions: { + x: 0.15, + y: 0.15, + }, + isFacingAvatar: false, + visible: true, + alpha: 1.0, + ignoreRayIntersection: false, + offsetPosition: { + x: -0.1, + y: 0.1, + z: -0.001 + } +}); +closeButton.onClick = function(event) { + panel.visible = false; +}; +panel.addChild(closeButton); + +var micMuteButton = new BillboardOverlay({ + url: MIC_IMAGE_URL, + subImage: { + x: 0, + y: 0, + width: 45, + height: 45 + }, + dimensions: { + x: 0.15, + y: 0.15, + }, + isFacingAvatar: false, + visible: true, + alpha: 1.0, + ignoreRayIntersection: false, + offsetPosition: { + x: 0.1, + y: 0.1, + z: -0.001 + } +}); +micMuteButton.onClick = function(event) { + AudioDevice.toggleMute(); +}; +panel.addChild(micMuteButton); + +var faceMuteButton = new BillboardOverlay({ + url: FACE_IMAGE_URL, + subImage: { + x: 0, + y: 0, + width: 45, + height: 45 + }, + dimensions: { + x: 0.15, + y: 0.15, + }, + isFacingAvatar: false, + visible: true, + alpha: 1.0, + ignoreRayIntersection: false, + offsetPosition: { + x: 0.1, + y: -0.1, + z: -0.001 + } +}); +faceMuteButton.onClick = function(event) { + FaceTracker.toggleMute(); +}; +panel.addChild(faceMuteButton); + +var addressBarButton = new BillboardOverlay({ + url: ADDRESS_BAR_IMAGE_URL, + subImage: { + x: 0, + y: 0, + width: 45, + height: 45 + }, + dimensions: { + x: 0.15, + y: 0.15, + }, + isFacingAvatar: false, + visible: true, + alpha: 1.0, + ignoreRayIntersection: false, + offsetPosition: { + x: -0.1, + y: -0.1, + z: -0.001 + } +}); +addressBarButton.onClick = function(event) { + DialogsManager.toggleAddressBar(); +}; +panel.addChild(addressBarButton); + + +function onMicMuteToggled() { + var offset; + if (AudioDevice.getMuted()) { + offset = 45; + } else { + offset = 0; + } + micMuteButton.subImage = { + x: offset, + y: 0, + width: 45, + height: 45 + }; +} +onMicMuteToggled(); + +function onFaceMuteToggled() { + var offset; + if (FaceTracker.getMuted()) { + offset = 45; + } else { + offset = 0; + } + faceMuteButton.subImage = { + x: offset, + y: 0, + width: 45, + height: 45 + }; +} +onFaceMuteToggled(); + +var isLeftClick = false, + isRightClick = false; + +function onMouseDown(event) { + isLeftClick = event.isLeftButton; + isRightClick = event.isRightButton; +} + +function onMouseMove(event) { + isLeftClick = isRightClick = false; +} + +function onMouseUp(event) { + if (isLeftClick && event.isLeftButton) { + var overlay = OverlayManager.findAtPoint({ x: event.x, y: event.y }); + if (overlay && overlay.onClick) { + overlay.onClick(event); + } + } + if (isRightClick && event.isRightButton) { + panel.setProperties({ + visible: true, + offsetRotation: { + bind: "quat", + value: Quat.multiply(MyAvatar.orientation, { x: 0, y: 1, z: 0, w: 0 }) + } + }); + } + isLeftClick = isRightClick = false; +} + +function onScriptEnd(event) { + panel.destroy(); + panel2.destroy(); +} + +Controller.mousePressEvent.connect(onMouseDown); +Controller.mouseMoveEvent.connect(onMouseMove); +Controller.mouseReleaseEvent.connect(onMouseUp); +AudioDevice.muteToggled.connect(onMicMuteToggled); +FaceTracker.muteToggled.connect(onFaceMuteToggled); +Script.scriptEnding.connect(onScriptEnd); From c37c3ec2d2f7ed5f31abf290230a7d8ac72d2746 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 24 Jul 2015 19:30:30 -0700 Subject: [PATCH 209/242] Use images off the amazon bucket, not file://. --- examples/controlPanel.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/examples/controlPanel.js b/examples/controlPanel.js index 6373dfc921..837b59ffa0 100644 --- a/examples/controlPanel.js +++ b/examples/controlPanel.js @@ -17,10 +17,10 @@ Script.include([ ]); var BG_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/card-bg.svg"; -var CLOSE_IMAGE_URL = "file:///Users/zander/Desktop/assets/close.svg"; -var MIC_IMAGE_URL = "file:///Users/zander/Desktop/assets/mic-composite.svg"; -var FACE_IMAGE_URL = "file:///Users/zander/Desktop/assets/face-composite.svg"; -var ADDRESS_BAR_IMAGE_URL = "file:///Users/zander/Desktop/assets/address-bar-composite.svg"; +var CLOSE_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/close.svg"; +var MIC_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/mic-toggle.svg"; +var FACE_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/face-toggle.svg"; +var ADDRESS_BAR_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/address-bar-toggle.svg"; var panel = new FloatingUIPanel({ anchorPosition: { @@ -36,7 +36,6 @@ var background = new BillboardOverlay({ y: 0.5, }, isFacingAvatar: false, - visible: true, alpha: 1.0, ignoreRayIntersection: false }); @@ -49,7 +48,6 @@ var closeButton = new BillboardOverlay({ y: 0.15, }, isFacingAvatar: false, - visible: true, alpha: 1.0, ignoreRayIntersection: false, offsetPosition: { @@ -76,7 +74,6 @@ var micMuteButton = new BillboardOverlay({ y: 0.15, }, isFacingAvatar: false, - visible: true, alpha: 1.0, ignoreRayIntersection: false, offsetPosition: { @@ -103,7 +100,6 @@ var faceMuteButton = new BillboardOverlay({ y: 0.15, }, isFacingAvatar: false, - visible: true, alpha: 1.0, ignoreRayIntersection: false, offsetPosition: { @@ -130,7 +126,6 @@ var addressBarButton = new BillboardOverlay({ y: 0.15, }, isFacingAvatar: false, - visible: true, alpha: 1.0, ignoreRayIntersection: false, offsetPosition: { @@ -198,7 +193,7 @@ function onMouseUp(event) { } if (isRightClick && event.isRightButton) { panel.setProperties({ - visible: true, + visible: !panel.visible, offsetRotation: { bind: "quat", value: Quat.multiply(MyAvatar.orientation, { x: 0, y: 1, z: 0, w: 0 }) @@ -210,7 +205,6 @@ function onMouseUp(event) { function onScriptEnd(event) { panel.destroy(); - panel2.destroy(); } Controller.mousePressEvent.connect(onMouseDown); From 9bd72e5769c7348b6791a6b5c7273a2c4af0c1dc Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 24 Jul 2015 20:07:38 -0700 Subject: [PATCH 210/242] Tweak PanelAttachable::setTransforms. Use reference instead of pointer, and make it virtual. --- interface/src/ui/overlays/BillboardOverlay.cpp | 8 ++++---- interface/src/ui/overlays/BillboardOverlay.h | 2 +- interface/src/ui/overlays/PanelAttachable.cpp | 11 +++++------ interface/src/ui/overlays/PanelAttachable.h | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/interface/src/ui/overlays/BillboardOverlay.cpp b/interface/src/ui/overlays/BillboardOverlay.cpp index 8ffa909e12..3bd1f56ba3 100644 --- a/interface/src/ui/overlays/BillboardOverlay.cpp +++ b/interface/src/ui/overlays/BillboardOverlay.cpp @@ -39,7 +39,7 @@ BillboardOverlay::BillboardOverlay(const BillboardOverlay* billboardOverlay) : { } -void BillboardOverlay::setTransforms(Transform *transform) { +void BillboardOverlay::setTransforms(Transform& transform) { PanelAttachable::setTransforms(transform); if (_isFacingAvatar) { glm::quat rotation = Application::getInstance()->getCamera()->getOrientation(); @@ -49,7 +49,7 @@ void BillboardOverlay::setTransforms(Transform *transform) { } void BillboardOverlay::update(float deltatime) { - setTransforms(&_transform); + setTransforms(_transform); } void BillboardOverlay::render(RenderArgs* args) { @@ -98,7 +98,7 @@ void BillboardOverlay::render(RenderArgs* args) { xColor color = getColor(); float alpha = getAlpha(); - setTransforms(&_transform); + setTransforms(_transform); Transform transform = _transform; transform.postScale(glm::vec3(getDimensions(), 1.0f)); @@ -197,7 +197,7 @@ bool BillboardOverlay::findRayIntersection(const glm::vec3& origin, const glm::v float& distance, BoxFace& face) { if (_texture && _texture->isLoaded()) { // Make sure position and rotation is updated. - setTransforms(&_transform); + setTransforms(_transform); // Produce the dimensions of the billboard based on the image's aspect ratio and the overlay's scale. bool isNull = _fromImage.isNull(); diff --git a/interface/src/ui/overlays/BillboardOverlay.h b/interface/src/ui/overlays/BillboardOverlay.h index 47be764f2f..a034612e71 100644 --- a/interface/src/ui/overlays/BillboardOverlay.h +++ b/interface/src/ui/overlays/BillboardOverlay.h @@ -43,7 +43,7 @@ public: virtual BillboardOverlay* createClone() const; protected: - void setTransforms(Transform* transform); + virtual void setTransforms(Transform& transform); private: void setBillboardURL(const QString& url); diff --git a/interface/src/ui/overlays/PanelAttachable.cpp b/interface/src/ui/overlays/PanelAttachable.cpp index e114fa143e..ac4730cfe8 100644 --- a/interface/src/ui/overlays/PanelAttachable.cpp +++ b/interface/src/ui/overlays/PanelAttachable.cpp @@ -26,13 +26,12 @@ PanelAttachable::PanelAttachable(const PanelAttachable* panelAttachable) : { } -void PanelAttachable::setTransforms(Transform* transform) { - Q_ASSERT(transform != nullptr); +void PanelAttachable::setTransforms(Transform& transform) { if (getAttachedPanel()) { - transform->setTranslation(getAttachedPanel()->getAnchorPosition()); - transform->setRotation(getAttachedPanel()->getOffsetRotation()); - transform->postTranslate(getOffsetPosition() + getAttachedPanel()->getOffsetPosition()); - transform->postRotate(getFacingRotation() * getAttachedPanel()->getFacingRotation()); + transform.setTranslation(getAttachedPanel()->getAnchorPosition()); + transform.setRotation(getAttachedPanel()->getOffsetRotation()); + transform.postTranslate(getOffsetPosition() + getAttachedPanel()->getOffsetPosition()); + transform.postRotate(getFacingRotation() * getAttachedPanel()->getFacingRotation()); } } diff --git a/interface/src/ui/overlays/PanelAttachable.h b/interface/src/ui/overlays/PanelAttachable.h index e1fc490d00..9776ac5ecb 100644 --- a/interface/src/ui/overlays/PanelAttachable.h +++ b/interface/src/ui/overlays/PanelAttachable.h @@ -34,7 +34,7 @@ public: void setProperties(const QScriptValue& properties); protected: - void setTransforms(Transform* transform); + virtual void setTransforms(Transform& transform); private: FloatingUIPanel::Pointer _attachedPanel; From 7973c2d469f1be8747488cfbe690476887e922a6 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 31 Jul 2015 10:55:51 -0700 Subject: [PATCH 211/242] Fix compile issue on Linux and Windows. Shoutout to Andrew for help finding the missing #include. --- interface/src/ui/overlays/FloatingUIPanel.h | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/src/ui/overlays/FloatingUIPanel.h b/interface/src/ui/overlays/FloatingUIPanel.h index f65fa93c26..f84ac32fac 100644 --- a/interface/src/ui/overlays/FloatingUIPanel.h +++ b/interface/src/ui/overlays/FloatingUIPanel.h @@ -13,6 +13,7 @@ #define hifi_FloatingUIPanel_h #include +#include #include #include From b0afdb21ad3daf1b6ae6b139d39f32ba4c9fe48f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 31 Jul 2015 14:29:05 -0700 Subject: [PATCH 212/242] correct the unique_ptr char allocations --- assignment-client/src/Agent.cpp | 2 +- ice-server/src/IceServer.cpp | 2 +- libraries/networking/src/NLPacket.cpp | 4 ++-- libraries/networking/src/NLPacket.h | 4 ++-- libraries/networking/src/PacketReceiver.cpp | 2 +- libraries/networking/src/udt/Packet.cpp | 6 +++--- libraries/networking/src/udt/Packet.h | 6 +++--- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index da588bc316..43866ce3a6 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -70,7 +70,7 @@ void Agent::handleOctreePacket(QSharedPointer packet, SharedNodePointe // pull out the piggybacked packet and create a new QSharedPointer for it int piggyBackedSizeWithHeader = packet->getPayloadSize() - statsMessageLength; - std::unique_ptr buffer = std::unique_ptr(new char[piggyBackedSizeWithHeader]); + auto buffer = std::unique_ptr(new char[piggyBackedSizeWithHeader]); memcpy(buffer.get(), packet->getPayload() + statsMessageLength, piggyBackedSizeWithHeader); auto newPacket = NLPacket::fromReceivedPacket(std::move(buffer), piggyBackedSizeWithHeader, packet->getSenderSockAddr()); diff --git a/ice-server/src/IceServer.cpp b/ice-server/src/IceServer.cpp index 46c1582e7d..2a4e2b4300 100644 --- a/ice-server/src/IceServer.cpp +++ b/ice-server/src/IceServer.cpp @@ -50,7 +50,7 @@ void IceServer::processDatagrams() { while (_serverSocket.hasPendingDatagrams()) { // setup a buffer to read the packet into int packetSizeWithHeader = _serverSocket.pendingDatagramSize(); - std::unique_ptr buffer = std::unique_ptr(new char[packetSizeWithHeader]); + auto buffer = std::unique_ptr(new char[packetSizeWithHeader]); _serverSocket.readDatagram(buffer.get(), packetSizeWithHeader, sendingSockAddr.getAddressPointer(), sendingSockAddr.getPortPointer()); diff --git a/libraries/networking/src/NLPacket.cpp b/libraries/networking/src/NLPacket.cpp index 7a6503dbc3..7ec10fc4ce 100644 --- a/libraries/networking/src/NLPacket.cpp +++ b/libraries/networking/src/NLPacket.cpp @@ -46,7 +46,7 @@ std::unique_ptr NLPacket::create(PacketType::Value type, qint64 size) return packet; } -std::unique_ptr NLPacket::fromReceivedPacket(std::unique_ptr data, qint64 size, +std::unique_ptr NLPacket::fromReceivedPacket(std::unique_ptr data, qint64 size, const HifiSockAddr& senderSockAddr) { // Fail with null data Q_ASSERT(data); @@ -85,7 +85,7 @@ NLPacket::NLPacket(const NLPacket& other) : Packet(other) { } -NLPacket::NLPacket(std::unique_ptr data, qint64 size, const HifiSockAddr& senderSockAddr) : +NLPacket::NLPacket(std::unique_ptr data, qint64 size, const HifiSockAddr& senderSockAddr) : Packet(std::move(data), size, senderSockAddr) { adjustPayloadStartAndCapacity(); diff --git a/libraries/networking/src/NLPacket.h b/libraries/networking/src/NLPacket.h index 669278ed65..6ebf15ffec 100644 --- a/libraries/networking/src/NLPacket.h +++ b/libraries/networking/src/NLPacket.h @@ -20,7 +20,7 @@ class NLPacket : public Packet { Q_OBJECT public: static std::unique_ptr create(PacketType::Value type, qint64 size = -1); - static std::unique_ptr fromReceivedPacket(std::unique_ptr data, qint64 size, + static std::unique_ptr fromReceivedPacket(std::unique_ptr data, qint64 size, const HifiSockAddr& senderSockAddr); // Provided for convenience, try to limit use static std::unique_ptr createCopy(const NLPacket& other); @@ -45,7 +45,7 @@ protected: NLPacket(PacketType::Value type); NLPacket(PacketType::Value type, qint64 size); - NLPacket(std::unique_ptr data, qint64 size, const HifiSockAddr& senderSockAddr); + NLPacket(std::unique_ptr data, qint64 size, const HifiSockAddr& senderSockAddr); NLPacket(const NLPacket& other); void readSourceID(); diff --git a/libraries/networking/src/PacketReceiver.cpp b/libraries/networking/src/PacketReceiver.cpp index 5fc327673d..b1a77d4c39 100644 --- a/libraries/networking/src/PacketReceiver.cpp +++ b/libraries/networking/src/PacketReceiver.cpp @@ -243,7 +243,7 @@ void PacketReceiver::processDatagrams() { while (nodeList && nodeList->getNodeSocket().hasPendingDatagrams()) { // setup a buffer to read the packet into int packetSizeWithHeader = nodeList->getNodeSocket().pendingDatagramSize(); - std::unique_ptr buffer = std::unique_ptr(new char[packetSizeWithHeader]); + auto buffer = std::unique_ptr(new char[packetSizeWithHeader]); // if we're supposed to drop this packet then break out here if (_shouldDropPackets) { diff --git a/libraries/networking/src/udt/Packet.cpp b/libraries/networking/src/udt/Packet.cpp index 02a44c4a4f..13f8a39e26 100644 --- a/libraries/networking/src/udt/Packet.cpp +++ b/libraries/networking/src/udt/Packet.cpp @@ -31,7 +31,7 @@ std::unique_ptr Packet::create(PacketType::Value type, qint64 size) { return packet; } -std::unique_ptr Packet::fromReceivedPacket(std::unique_ptr data, qint64 size, const HifiSockAddr& senderSockAddr) { +std::unique_ptr Packet::fromReceivedPacket(std::unique_ptr data, qint64 size, const HifiSockAddr& senderSockAddr) { // Fail with invalid size Q_ASSERT(size >= 0); @@ -82,7 +82,7 @@ Packet::Packet(PacketType::Value type, qint64 size) : } } -Packet::Packet(std::unique_ptr data, qint64 size, const HifiSockAddr& senderSockAddr) : +Packet::Packet(std::unique_ptr data, qint64 size, const HifiSockAddr& senderSockAddr) : _packetSize(size), _packet(std::move(data)), _senderSockAddr(senderSockAddr) @@ -110,7 +110,7 @@ Packet& Packet::operator=(const Packet& other) { _type = other._type; _packetSize = other._packetSize; - _packet = std::unique_ptr(new char[_packetSize]); + _packet = std::unique_ptr(new char[_packetSize]); memcpy(_packet.get(), other._packet.get(), _packetSize); _payloadStart = _packet.get() + (other._payloadStart - other._packet.get()); diff --git a/libraries/networking/src/udt/Packet.h b/libraries/networking/src/udt/Packet.h index b4c53b8165..91b5974e09 100644 --- a/libraries/networking/src/udt/Packet.h +++ b/libraries/networking/src/udt/Packet.h @@ -27,7 +27,7 @@ public: static const qint64 PACKET_WRITE_ERROR; static std::unique_ptr create(PacketType::Value type, qint64 size = -1); - static std::unique_ptr fromReceivedPacket(std::unique_ptr data, qint64 size, const HifiSockAddr& senderSockAddr); + static std::unique_ptr fromReceivedPacket(std::unique_ptr data, qint64 size, const HifiSockAddr& senderSockAddr); // Provided for convenience, try to limit use static std::unique_ptr createCopy(const Packet& other); @@ -88,7 +88,7 @@ public: protected: Packet(PacketType::Value type, qint64 size); - Packet(std::unique_ptr data, qint64 size, const HifiSockAddr& senderSockAddr); + Packet(std::unique_ptr data, qint64 size, const HifiSockAddr& senderSockAddr); Packet(const Packet& other); Packet& operator=(const Packet& other); Packet(Packet&& other); @@ -109,7 +109,7 @@ protected: PacketVersion _version; // Packet version qint64 _packetSize = 0; // Total size of the allocated memory - std::unique_ptr _packet; // Allocated memory + std::unique_ptr _packet; // Allocated memory char* _payloadStart = nullptr; // Start of the payload qint64 _payloadCapacity = 0; // Total capacity of the payload From c88e0360b3714890b9f6ba1774514f246b27a672 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 31 Jul 2015 14:46:43 -0700 Subject: [PATCH 213/242] Change the inspect.js pan direction and rate This makes camera zoom, orbit, and panning more similar to Second Life. --- examples/inspect.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/inspect.js b/examples/inspect.js index 0df90fbac3..555b4105b7 100644 --- a/examples/inspect.js +++ b/examples/inspect.js @@ -23,7 +23,7 @@ var RAD_TO_DEG = 180.0 / PI; var AZIMUTH_RATE = 90.0; var ALTITUDE_RATE = 200.0; var RADIUS_RATE = 1.0 / 100.0; -var PAN_RATE = 50.0; +var PAN_RATE = 250.0; var Y_AXIS = { x: 0, @@ -139,7 +139,7 @@ function handlePanMode(dx, dy) { var right = Quat.getRight(Camera.getOrientation()); var distance = Vec3.length(vector); - var dv = Vec3.sum(Vec3.multiply(up, -distance * dy / PAN_RATE), Vec3.multiply(right, distance * dx / PAN_RATE)); + var dv = Vec3.sum(Vec3.multiply(up, distance * dy / PAN_RATE), Vec3.multiply(right, -distance * dx / PAN_RATE)); center = Vec3.sum(center, dv); position = Vec3.sum(position, dv); From 12ad60a6b5afa77c73b4d899922afa5e637e04f8 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 31 Jul 2015 14:48:54 -0700 Subject: [PATCH 214/242] add a missed char[] in OctreePacketProcessor --- interface/src/octree/OctreePacketProcessor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/octree/OctreePacketProcessor.cpp b/interface/src/octree/OctreePacketProcessor.cpp index 7bb94323b7..1abbb21089 100644 --- a/interface/src/octree/OctreePacketProcessor.cpp +++ b/interface/src/octree/OctreePacketProcessor.cpp @@ -57,7 +57,7 @@ void OctreePacketProcessor::processPacket(QSharedPointer packet, Share if (piggybackBytes) { // construct a new packet from the piggybacked one - std::unique_ptr buffer = std::unique_ptr(new char[piggybackBytes]); + auto buffer = std::unique_ptr(new char[piggybackBytes]); memcpy(buffer.get(), packet->getPayload() + statsMessageLength, piggybackBytes); auto newPacket = NLPacket::fromReceivedPacket(std::move(buffer), piggybackBytes, packet->getSenderSockAddr()); From 0dad1adf7d928fc6cc8ef5ec616b92b3a3cf0b2e Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 31 Jul 2015 18:05:14 -0700 Subject: [PATCH 215/242] Speculative guard against crashing. --- libraries/render-utils/src/Model.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 1dc08b7a06..72a601cd20 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -243,6 +243,9 @@ QVector Model::createJointStates(const FBXGeometry& geometry) { }; void Model::initJointTransforms() { + if (!_geometry) { + return; + } const FBXGeometry& geometry = _geometry->getFBXGeometry(); glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; _rig->initJointTransforms(parentTransform); @@ -424,6 +427,9 @@ bool Model::updateGeometry() { deleteGeometry(); _dilatedTextures.clear(); + if (!geometry) { + std::cout << "WARNING: no geometry in Model::updateGeometry\n"; + } setGeometry(geometry); _meshGroupsKnown = false; From 1dd6c1117d434ef1555ca7f5b698b6b557fea153 Mon Sep 17 00:00:00 2001 From: ZappoMan Date: Sat, 1 Aug 2015 11:29:28 -0700 Subject: [PATCH 216/242] change GeometryCache::renderQuad() to use TRIANGLES as lower level render primitive --- libraries/gpu/src/gpu/Format.h | 2 + libraries/model/src/model/Geometry.h | 2 +- libraries/render-utils/src/GeometryCache.cpp | 97 ++++++++++++++++---- libraries/render-utils/src/GeometryCache.h | 2 + 4 files changed, 86 insertions(+), 17 deletions(-) diff --git a/libraries/gpu/src/gpu/Format.h b/libraries/gpu/src/gpu/Format.h index 981a560965..01d3f37ef8 100644 --- a/libraries/gpu/src/gpu/Format.h +++ b/libraries/gpu/src/gpu/Format.h @@ -213,6 +213,8 @@ enum Primitive { TRIANGLES, TRIANGLE_STRIP, TRIANGLE_FAN, + + // FIXME - remove these QUADS, QUAD_STRIP, diff --git a/libraries/model/src/model/Geometry.h b/libraries/model/src/model/Geometry.h index 16ebb60b72..5ef414a2d1 100755 --- a/libraries/model/src/model/Geometry.h +++ b/libraries/model/src/model/Geometry.h @@ -71,7 +71,7 @@ public: LINE_STRIP, TRIANGLES, TRIANGLE_STRIP, - QUADS, + QUADS, // NOTE: These must be translated to triangles before rendering QUAD_STRIP, NUM_TOPOLOGIES, diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index def5f38db4..12a6d46be6 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1012,21 +1012,27 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co } const int FLOATS_PER_VERTEX = 2; // vertices - const int vertices = 4; + const int NUMBER_OF_INDICES = 6; // 1 quad = 2 triangles + const int VERTICES = 4; // 1 quad = 4 vertices if (!details.isCreated) { details.isCreated = true; - details.vertices = vertices; + details.vertices = VERTICES; + details.indices = NUMBER_OF_INDICES; details.vertexSize = FLOATS_PER_VERTEX; auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); + auto indicesBuffer = std::make_shared(); + auto streamFormat = std::make_shared(); auto stream = std::make_shared(); details.verticesBuffer = verticesBuffer; details.colorBuffer = colorBuffer; + details.indicesBuffer = indicesBuffer; + details.streamFormat = streamFormat; details.stream = stream; @@ -1037,7 +1043,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride); - float vertexBuffer[vertices * FLOATS_PER_VERTEX] = { + float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = { minCorner.x, minCorner.y, maxCorner.x, minCorner.y, maxCorner.x, maxCorner.y, @@ -1050,14 +1056,24 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co ((int(color.w * 255.0f) & 0xFF) << 24); int colors[NUM_COLOR_SCALARS_PER_QUAD] = { compactColor, compactColor, compactColor, compactColor }; + // Sam's recommended triangle slices + // Triangle tri1 = { v0, v1, v3 }; + // Triangle tri2 = { v1, v2, v3 }; + // NOTE: Random guy on the internet's recommended triangle slices + // Triangle tri1 = { v0, v1, v2 }; + // Triangle tri2 = { v2, v3, v0 }; + + quint16 indices[NUMBER_OF_INDICES] = { 0, 1, 3, 1, 2, 3 }; details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors); + details.indicesBuffer->append(sizeof(indices), (gpu::Byte*) indices); } batch.setInputFormat(details.streamFormat); batch.setInputStream(0, *details.stream); - batch.draw(gpu::QUADS, 4, 0); + batch.setIndexBuffer(gpu::UINT16, details.indicesBuffer, 0); + batch.drawIndexed(gpu::TRIANGLES, NUMBER_OF_INDICES, 0); } void GeometryCache::renderUnitCube(gpu::Batch& batch) { @@ -1102,23 +1118,29 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co } const int FLOATS_PER_VERTEX = 2 * 2; // text coords & vertices - const int vertices = 4; + const int VERTICES = 4; // 1 quad = 4 vertices const int NUM_POS_COORDS = 2; const int VERTEX_TEXCOORD_OFFSET = NUM_POS_COORDS * sizeof(float); + const int NUMBER_OF_INDICES = 6; // 1 quad = 2 triangles if (!details.isCreated) { details.isCreated = true; - details.vertices = vertices; + details.vertices = VERTICES; + details.indices = NUMBER_OF_INDICES; details.vertexSize = FLOATS_PER_VERTEX; auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); + auto indicesBuffer = std::make_shared(); + auto streamFormat = std::make_shared(); auto stream = std::make_shared(); details.verticesBuffer = verticesBuffer; details.colorBuffer = colorBuffer; + details.indicesBuffer = indicesBuffer; + details.streamFormat = streamFormat; details.stream = stream; @@ -1130,7 +1152,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride); - float vertexBuffer[vertices * FLOATS_PER_VERTEX] = { + float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = { minCorner.x, minCorner.y, texCoordMinCorner.x, texCoordMinCorner.y, maxCorner.x, minCorner.y, texCoordMaxCorner.x, texCoordMinCorner.y, maxCorner.x, maxCorner.y, texCoordMaxCorner.x, texCoordMaxCorner.y, @@ -1144,14 +1166,24 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co ((int(color.w * 255.0f) & 0xFF) << 24); int colors[NUM_COLOR_SCALARS_PER_QUAD] = { compactColor, compactColor, compactColor, compactColor }; + // Sam's recommended triangle slices + // Triangle tri1 = { v0, v1, v3 }; + // Triangle tri2 = { v1, v2, v3 }; + // NOTE: Random guy on the internet's recommended triangle slices + // Triangle tri1 = { v0, v1, v2 }; + // Triangle tri2 = { v2, v3, v0 }; + + quint16 indices[NUMBER_OF_INDICES] = { 0, 1, 3, 1, 2, 3 }; details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors); + details.indicesBuffer->append(sizeof(indices), (gpu::Byte*) indices); } batch.setInputFormat(details.streamFormat); batch.setInputStream(0, *details.stream); - batch.draw(gpu::QUADS, 4, 0); + batch.setIndexBuffer(gpu::UINT16, details.indicesBuffer, 0); + batch.drawIndexed(gpu::TRIANGLES, NUMBER_OF_INDICES, 0); } void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, const glm::vec3& maxCorner, const glm::vec4& color, int id) { @@ -1177,21 +1209,27 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, co } const int FLOATS_PER_VERTEX = 3; // vertices - const int vertices = 4; + const int NUMBER_OF_INDICES = 6; // 1 quad = 2 triangles + const int VERTICES = 4; // 1 quad = 4 vertices if (!details.isCreated) { details.isCreated = true; - details.vertices = vertices; + details.vertices = VERTICES; + details.indices = NUMBER_OF_INDICES; details.vertexSize = FLOATS_PER_VERTEX; auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); + auto indicesBuffer = std::make_shared(); + auto streamFormat = std::make_shared(); auto stream = std::make_shared(); details.verticesBuffer = verticesBuffer; details.colorBuffer = colorBuffer; + details.indicesBuffer = indicesBuffer; + details.streamFormat = streamFormat; details.stream = stream; @@ -1202,7 +1240,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, co details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride); - float vertexBuffer[vertices * FLOATS_PER_VERTEX] = { + float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = { minCorner.x, minCorner.y, minCorner.z, maxCorner.x, minCorner.y, minCorner.z, maxCorner.x, maxCorner.y, maxCorner.z, @@ -1215,14 +1253,24 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, co ((int(color.w * 255.0f) & 0xFF) << 24); int colors[NUM_COLOR_SCALARS_PER_QUAD] = { compactColor, compactColor, compactColor, compactColor }; + // Sam's recommended triangle slices + // Triangle tri1 = { v0, v1, v3 }; + // Triangle tri2 = { v1, v2, v3 }; + // NOTE: Random guy on the internet's recommended triangle slices + // Triangle tri1 = { v0, v1, v2 }; + // Triangle tri2 = { v2, v3, v0 }; + + quint16 indices[NUMBER_OF_INDICES] = { 0, 1, 3, 1, 2, 3 }; details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors); + details.indicesBuffer->append(sizeof(indices), (gpu::Byte*) indices); } batch.setInputFormat(details.streamFormat); batch.setInputStream(0, *details.stream); - batch.draw(gpu::QUADS, 4, 0); + batch.setIndexBuffer(gpu::UINT16, details.indicesBuffer, 0); + batch.drawIndexed(gpu::TRIANGLES, NUMBER_OF_INDICES, 0); } void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, const glm::vec3& bottomLeft, @@ -1267,23 +1315,29 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, cons } const int FLOATS_PER_VERTEX = 3 + 2; // 3d vertices + text coords - const int vertices = 4; + const int NUMBER_OF_INDICES = 6; // 1 quad = 2 triangles + const int VERTICES = 4; // 1 quad = 4 vertices const int NUM_POS_COORDS = 3; const int VERTEX_TEXCOORD_OFFSET = NUM_POS_COORDS * sizeof(float); if (!details.isCreated) { details.isCreated = true; - details.vertices = vertices; + details.vertices = VERTICES; + details.indices = NUMBER_OF_INDICES; details.vertexSize = FLOATS_PER_VERTEX; // NOTE: this isn't used for BatchItemDetails maybe we can get rid of it auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); + auto indicesBuffer = std::make_shared(); + auto streamFormat = std::make_shared(); auto stream = std::make_shared(); details.verticesBuffer = verticesBuffer; details.colorBuffer = colorBuffer; + details.indicesBuffer = indicesBuffer; + details.streamFormat = streamFormat; details.stream = stream; @@ -1295,7 +1349,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, cons details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride); - float vertexBuffer[vertices * FLOATS_PER_VERTEX] = { + float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = { topLeft.x, topLeft.y, topLeft.z, texCoordTopLeft.x, texCoordTopLeft.y, bottomLeft.x, bottomLeft.y, bottomLeft.z, texCoordBottomLeft.x, texCoordBottomLeft.y, bottomRight.x, bottomRight.y, bottomRight.z, texCoordBottomRight.x, texCoordBottomRight.y, @@ -1309,13 +1363,24 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, cons ((int(color.w * 255.0f) & 0xFF) << 24); int colors[NUM_COLOR_SCALARS_PER_QUAD] = { compactColor, compactColor, compactColor, compactColor }; + // Sam's recommended triangle slices + // Triangle tri1 = { v0, v1, v3 }; + // Triangle tri2 = { v1, v2, v3 }; + // NOTE: Random guy on the internet's recommended triangle slices + // Triangle tri1 = { v0, v1, v2 }; + // Triangle tri2 = { v2, v3, v0 }; + + quint16 indices[NUMBER_OF_INDICES] = { 0, 1, 3, 1, 2, 3 }; + details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors); + details.indicesBuffer->append(sizeof(indices), (gpu::Byte*) indices); } batch.setInputFormat(details.streamFormat); batch.setInputStream(0, *details.stream); - batch.draw(gpu::QUADS, 4, 0); + batch.setIndexBuffer(gpu::UINT16, details.indicesBuffer, 0); + batch.drawIndexed(gpu::TRIANGLES, NUMBER_OF_INDICES, 0); } void GeometryCache::renderDashedLine(gpu::Batch& batch, const glm::vec3& start, const glm::vec3& end, const glm::vec4& color, int id) { diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 812d12b846..19e84e9346 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -235,10 +235,12 @@ private: static int population; gpu::BufferPointer verticesBuffer; gpu::BufferPointer colorBuffer; + gpu::BufferPointer indicesBuffer; gpu::Stream::FormatPointer streamFormat; gpu::BufferStreamPointer stream; int vertices; + int indices; int vertexSize; bool isCreated; From 452225ddaf87ba387f6c06ffc96e863f2dfc95be Mon Sep 17 00:00:00 2001 From: ZappoMan Date: Sat, 1 Aug 2015 11:51:07 -0700 Subject: [PATCH 217/242] removed QUADS from Font::drawString() --- libraries/render-utils/src/text/Font.cpp | 30 ++++++++++++++++++++++-- libraries/render-utils/src/text/Font.h | 2 ++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index b341f444e6..3cedcdcbc1 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -18,8 +18,12 @@ struct TextureVertex { TextureVertex(const glm::vec2& pos, const glm::vec2& tex) : pos(pos), tex(tex) {} }; +static const int NUMBER_OF_INDICES_PER_QUAD = 6; // 1 quad = 2 triangles +static const int VERTICES_PER_QUAD = 4; // 1 quad = 4 vertices + struct QuadBuilder { - TextureVertex vertices[4]; + TextureVertex vertices[VERTICES_PER_QUAD]; + QuadBuilder(const glm::vec2& min, const glm::vec2& size, const glm::vec2& texMin, const glm::vec2& texSize) { // min = bottomLeft @@ -249,6 +253,9 @@ void Font::setupGPU() { void Font::rebuildVertices(float x, float y, const QString& str, const glm::vec2& bounds) { _verticesBuffer = std::make_shared(); _numVertices = 0; + _indicesBuffer = std::make_shared(); + _numIndices = 0; + _lastStringRendered = str; _lastBounds = bounds; @@ -284,10 +291,28 @@ void Font::rebuildVertices(float x, float y, const QString& str, const glm::vec2 if (!isNewLine) { for (auto c : token) { auto glyph = _glyphs[c]; + quint16 verticesOffset = _numVertices; QuadBuilder qd(glyph, advance - glm::vec2(0.0f, _ascent)); _verticesBuffer->append(sizeof(QuadBuilder), (const gpu::Byte*)&qd); _numVertices += 4; + + // Sam's recommended triangle slices + // Triangle tri1 = { v0, v1, v3 }; + // Triangle tri2 = { v1, v2, v3 }; + // NOTE: Random guy on the internet's recommended triangle slices + // Triangle tri1 = { v0, v1, v2 }; + // Triangle tri2 = { v2, v3, v0 }; + quint16 indices[NUMBER_OF_INDICES_PER_QUAD]; + indices[0] = verticesOffset + 0; + indices[1] = verticesOffset + 1; + indices[2] = verticesOffset + 3; + indices[3] = verticesOffset + 1; + indices[4] = verticesOffset + 2; + indices[5] = verticesOffset + 3; + _indicesBuffer->append(sizeof(indices), (const gpu::Byte*)indices); + _numIndices += NUMBER_OF_INDICES_PER_QUAD; + // Advance by glyph size advance.x += glyph.d; @@ -318,5 +343,6 @@ void Font::drawString(gpu::Batch& batch, float x, float y, const QString& str, c batch.setInputFormat(_format); batch.setInputBuffer(0, _verticesBuffer, 0, _format->getChannels().at(0)._stride); - batch.draw(gpu::QUADS, _numVertices, 0); + batch.setIndexBuffer(gpu::UINT16, _indicesBuffer, 0); + batch.drawIndexed(gpu::TRIANGLES, _numIndices, 0); } diff --git a/libraries/render-utils/src/text/Font.h b/libraries/render-utils/src/text/Font.h index 55801419f9..e10360d45f 100644 --- a/libraries/render-utils/src/text/Font.h +++ b/libraries/render-utils/src/text/Font.h @@ -64,8 +64,10 @@ private: gpu::TexturePointer _texture; gpu::Stream::FormatPointer _format; gpu::BufferPointer _verticesBuffer; + gpu::BufferPointer _indicesBuffer; gpu::BufferStreamPointer _stream; unsigned int _numVertices = 0; + unsigned int _numIndices = 0; int _fontLoc = -1; int _outlineLoc = -1; From 05a4a6aa9bb99c6b55d38d6127d3f18db711d08b Mon Sep 17 00:00:00 2001 From: ZappoMan Date: Sat, 1 Aug 2015 13:07:56 -0700 Subject: [PATCH 218/242] implement on-the-fly conversion of FBXMeshParts that are quads, into their triangle equivalents --- libraries/fbx/src/FBXReader.cpp | 51 +++++++++++++++++++++++++++- libraries/fbx/src/FBXReader.h | 11 ++++-- libraries/render-utils/src/Model.cpp | 5 ++- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index e18b334264..51f91de71a 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -830,6 +830,56 @@ public: std::vector attributes; }; +gpu::BufferPointer FBXMeshPart::getTrianglesForQuads() const { + // if we've been asked for our triangulation of the original quads, but we don't yet have them + // then create them now. + if (!trianglesForQuadsAvailable) { + trianglesForQuadsAvailable = true; + + quadsAsTrianglesIndicesBuffer = std::make_shared(); + + // QVector quadIndices; // original indices from the FBX mesh + QVector quadsAsTrianglesIndices; // triangle versions of quads converted when first needed + const int INDICES_PER_ORIGINAL_QUAD = 4; + const int INDICES_PER_TRIANGULATED_QUAD = 6; + int numberOfQuads = quadIndices.size() / INDICES_PER_ORIGINAL_QUAD; + + quadsAsTrianglesIndices.resize(numberOfQuads * INDICES_PER_TRIANGULATED_QUAD); + + int originalIndex = 0; + int triangulatedIndex = 0; + for (int fromQuad = 0; fromQuad < numberOfQuads; fromQuad++) { + int i0 = quadIndices[originalIndex + 0]; + int i1 = quadIndices[originalIndex + 1]; + int i2 = quadIndices[originalIndex + 2]; + int i3 = quadIndices[originalIndex + 3]; + + // Sam's recommended triangle slices + // Triangle tri1 = { v0, v1, v3 }; + // Triangle tri2 = { v1, v2, v3 }; + // NOTE: Random guy on the internet's recommended triangle slices + // Triangle tri1 = { v0, v1, v2 }; + // Triangle tri2 = { v2, v3, v0 }; + + quadsAsTrianglesIndices[triangulatedIndex + 0] = i0; + quadsAsTrianglesIndices[triangulatedIndex + 1] = i1; + quadsAsTrianglesIndices[triangulatedIndex + 2] = i3; + + quadsAsTrianglesIndices[triangulatedIndex + 3] = i1; + quadsAsTrianglesIndices[triangulatedIndex + 4] = i2; + quadsAsTrianglesIndices[triangulatedIndex + 5] = i3; + + originalIndex += INDICES_PER_ORIGINAL_QUAD; + triangulatedIndex += INDICES_PER_TRIANGULATED_QUAD; + } + + trianglesForQuadsIndicesCount = INDICES_PER_TRIANGULATED_QUAD * numberOfQuads; + quadsAsTrianglesIndicesBuffer->append(quadsAsTrianglesIndices.size() * sizeof(quint32), (gpu::Byte*)quadsAsTrianglesIndices.data()); + + } + return quadsAsTrianglesIndicesBuffer; +} + void appendIndex(MeshData& data, QVector& indices, int index) { if (index >= data.polygonIndices.size()) { return; @@ -1089,7 +1139,6 @@ ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex) { appendIndex(data, part.quadIndices, beginIndex++); appendIndex(data, part.quadIndices, beginIndex++); appendIndex(data, part.quadIndices, beginIndex++); - } else { for (int nextIndex = beginIndex + 1;; ) { appendIndex(data, part.triangleIndices, beginIndex); diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 198e2d3534..a20dd4a07f 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -109,9 +109,10 @@ public: class FBXMeshPart { public: - QVector quadIndices; - QVector triangleIndices; - + QVector quadIndices; // original indices from the FBX mesh + QVector triangleIndices; // original indices from the FBX mesh + mutable gpu::BufferPointer quadsAsTrianglesIndicesBuffer; + glm::vec3 diffuseColor; glm::vec3 specularColor; glm::vec3 emissiveColor; @@ -126,6 +127,10 @@ public: QString materialID; model::MaterialPointer _material; + mutable bool trianglesForQuadsAvailable = false; + mutable int trianglesForQuadsIndicesCount = 0; + + gpu::BufferPointer getTrianglesForQuads() const; }; /// A single mesh (with optional blendshapes) extracted from an FBX document. diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 1e4f3f7190..b77bafc354 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -2096,8 +2096,11 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran } if (part.quadIndices.size() > 0) { - batch.drawIndexed(gpu::QUADS, part.quadIndices.size(), offset); + batch.setIndexBuffer(gpu::UINT32, part.getTrianglesForQuads(), 0); + batch.drawIndexed(gpu::TRIANGLES, part.trianglesForQuadsIndicesCount, 0); + offset += part.quadIndices.size() * sizeof(int); + batch.setIndexBuffer(gpu::UINT32, (networkMesh._indexBuffer), 0); // restore this in case there are triangles too } if (part.triangleIndices.size() > 0) { From b138c16c7f689e249cdca0436d1bbcd4b52fbf14 Mon Sep 17 00:00:00 2001 From: ZappoMan Date: Sat, 1 Aug 2015 16:50:36 -0700 Subject: [PATCH 219/242] use Austins QUAD to TRIANGLE_STRIP approach --- libraries/render-utils/src/GeometryCache.cpp | 89 +++----------------- libraries/render-utils/src/GeometryCache.h | 2 - 2 files changed, 14 insertions(+), 77 deletions(-) diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 12a6d46be6..3d43cc6934 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1012,27 +1012,21 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co } const int FLOATS_PER_VERTEX = 2; // vertices - const int NUMBER_OF_INDICES = 6; // 1 quad = 2 triangles const int VERTICES = 4; // 1 quad = 4 vertices if (!details.isCreated) { details.isCreated = true; details.vertices = VERTICES; - details.indices = NUMBER_OF_INDICES; details.vertexSize = FLOATS_PER_VERTEX; auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); - auto indicesBuffer = std::make_shared(); - auto streamFormat = std::make_shared(); auto stream = std::make_shared(); details.verticesBuffer = verticesBuffer; details.colorBuffer = colorBuffer; - details.indicesBuffer = indicesBuffer; - details.streamFormat = streamFormat; details.stream = stream; @@ -1046,8 +1040,9 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = { minCorner.x, minCorner.y, maxCorner.x, minCorner.y, - maxCorner.x, maxCorner.y, - minCorner.x, maxCorner.y }; + minCorner.x, maxCorner.y, + maxCorner.x, maxCorner.y + }; const int NUM_COLOR_SCALARS_PER_QUAD = 4; int compactColor = ((int(color.x * 255.0f) & 0xFF)) | @@ -1056,24 +1051,13 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co ((int(color.w * 255.0f) & 0xFF) << 24); int colors[NUM_COLOR_SCALARS_PER_QUAD] = { compactColor, compactColor, compactColor, compactColor }; - // Sam's recommended triangle slices - // Triangle tri1 = { v0, v1, v3 }; - // Triangle tri2 = { v1, v2, v3 }; - // NOTE: Random guy on the internet's recommended triangle slices - // Triangle tri1 = { v0, v1, v2 }; - // Triangle tri2 = { v2, v3, v0 }; - - quint16 indices[NUMBER_OF_INDICES] = { 0, 1, 3, 1, 2, 3 }; - details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors); - details.indicesBuffer->append(sizeof(indices), (gpu::Byte*) indices); } batch.setInputFormat(details.streamFormat); batch.setInputStream(0, *details.stream); - batch.setIndexBuffer(gpu::UINT16, details.indicesBuffer, 0); - batch.drawIndexed(gpu::TRIANGLES, NUMBER_OF_INDICES, 0); + batch.draw(gpu::TRIANGLE_STRIP, 4, 0); } void GeometryCache::renderUnitCube(gpu::Batch& batch) { @@ -1121,25 +1105,21 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co const int VERTICES = 4; // 1 quad = 4 vertices const int NUM_POS_COORDS = 2; const int VERTEX_TEXCOORD_OFFSET = NUM_POS_COORDS * sizeof(float); - const int NUMBER_OF_INDICES = 6; // 1 quad = 2 triangles if (!details.isCreated) { details.isCreated = true; details.vertices = VERTICES; - details.indices = NUMBER_OF_INDICES; details.vertexSize = FLOATS_PER_VERTEX; auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); - auto indicesBuffer = std::make_shared(); auto streamFormat = std::make_shared(); auto stream = std::make_shared(); details.verticesBuffer = verticesBuffer; details.colorBuffer = colorBuffer; - details.indicesBuffer = indicesBuffer; details.streamFormat = streamFormat; details.stream = stream; @@ -1155,8 +1135,9 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = { minCorner.x, minCorner.y, texCoordMinCorner.x, texCoordMinCorner.y, maxCorner.x, minCorner.y, texCoordMaxCorner.x, texCoordMinCorner.y, - maxCorner.x, maxCorner.y, texCoordMaxCorner.x, texCoordMaxCorner.y, - minCorner.x, maxCorner.y, texCoordMinCorner.x, texCoordMaxCorner.y }; + minCorner.x, maxCorner.y, texCoordMinCorner.x, texCoordMaxCorner.y, + maxCorner.x, maxCorner.y, texCoordMaxCorner.x, texCoordMaxCorner.y + }; const int NUM_COLOR_SCALARS_PER_QUAD = 4; @@ -1166,24 +1147,13 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co ((int(color.w * 255.0f) & 0xFF) << 24); int colors[NUM_COLOR_SCALARS_PER_QUAD] = { compactColor, compactColor, compactColor, compactColor }; - // Sam's recommended triangle slices - // Triangle tri1 = { v0, v1, v3 }; - // Triangle tri2 = { v1, v2, v3 }; - // NOTE: Random guy on the internet's recommended triangle slices - // Triangle tri1 = { v0, v1, v2 }; - // Triangle tri2 = { v2, v3, v0 }; - - quint16 indices[NUMBER_OF_INDICES] = { 0, 1, 3, 1, 2, 3 }; - details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors); - details.indicesBuffer->append(sizeof(indices), (gpu::Byte*) indices); } batch.setInputFormat(details.streamFormat); batch.setInputStream(0, *details.stream); - batch.setIndexBuffer(gpu::UINT16, details.indicesBuffer, 0); - batch.drawIndexed(gpu::TRIANGLES, NUMBER_OF_INDICES, 0); + batch.draw(gpu::TRIANGLE_STRIP, 4, 0); } void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, const glm::vec3& maxCorner, const glm::vec4& color, int id) { @@ -1209,26 +1179,22 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, co } const int FLOATS_PER_VERTEX = 3; // vertices - const int NUMBER_OF_INDICES = 6; // 1 quad = 2 triangles const int VERTICES = 4; // 1 quad = 4 vertices if (!details.isCreated) { details.isCreated = true; details.vertices = VERTICES; - details.indices = NUMBER_OF_INDICES; details.vertexSize = FLOATS_PER_VERTEX; auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); - auto indicesBuffer = std::make_shared(); auto streamFormat = std::make_shared(); auto stream = std::make_shared(); details.verticesBuffer = verticesBuffer; details.colorBuffer = colorBuffer; - details.indicesBuffer = indicesBuffer; details.streamFormat = streamFormat; details.stream = stream; @@ -1243,8 +1209,9 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, co float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = { minCorner.x, minCorner.y, minCorner.z, maxCorner.x, minCorner.y, minCorner.z, - maxCorner.x, maxCorner.y, maxCorner.z, - minCorner.x, maxCorner.y, maxCorner.z }; + minCorner.x, maxCorner.y, maxCorner.z, + maxCorner.x, maxCorner.y, maxCorner.z + }; const int NUM_COLOR_SCALARS_PER_QUAD = 4; int compactColor = ((int(color.x * 255.0f) & 0xFF)) | @@ -1253,24 +1220,13 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, co ((int(color.w * 255.0f) & 0xFF) << 24); int colors[NUM_COLOR_SCALARS_PER_QUAD] = { compactColor, compactColor, compactColor, compactColor }; - // Sam's recommended triangle slices - // Triangle tri1 = { v0, v1, v3 }; - // Triangle tri2 = { v1, v2, v3 }; - // NOTE: Random guy on the internet's recommended triangle slices - // Triangle tri1 = { v0, v1, v2 }; - // Triangle tri2 = { v2, v3, v0 }; - - quint16 indices[NUMBER_OF_INDICES] = { 0, 1, 3, 1, 2, 3 }; - details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors); - details.indicesBuffer->append(sizeof(indices), (gpu::Byte*) indices); } batch.setInputFormat(details.streamFormat); batch.setInputStream(0, *details.stream); - batch.setIndexBuffer(gpu::UINT16, details.indicesBuffer, 0); - batch.drawIndexed(gpu::TRIANGLES, NUMBER_OF_INDICES, 0); + batch.draw(gpu::TRIANGLE_STRIP, 4, 0); } void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, const glm::vec3& bottomLeft, @@ -1315,7 +1271,6 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, cons } const int FLOATS_PER_VERTEX = 3 + 2; // 3d vertices + text coords - const int NUMBER_OF_INDICES = 6; // 1 quad = 2 triangles const int VERTICES = 4; // 1 quad = 4 vertices const int NUM_POS_COORDS = 3; const int VERTEX_TEXCOORD_OFFSET = NUM_POS_COORDS * sizeof(float); @@ -1324,20 +1279,15 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, cons details.isCreated = true; details.vertices = VERTICES; - details.indices = NUMBER_OF_INDICES; details.vertexSize = FLOATS_PER_VERTEX; // NOTE: this isn't used for BatchItemDetails maybe we can get rid of it auto verticesBuffer = std::make_shared(); auto colorBuffer = std::make_shared(); - auto indicesBuffer = std::make_shared(); - auto streamFormat = std::make_shared(); auto stream = std::make_shared(); details.verticesBuffer = verticesBuffer; details.colorBuffer = colorBuffer; - details.indicesBuffer = indicesBuffer; - details.streamFormat = streamFormat; details.stream = stream; @@ -1350,9 +1300,9 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, cons float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = { - topLeft.x, topLeft.y, topLeft.z, texCoordTopLeft.x, texCoordTopLeft.y, bottomLeft.x, bottomLeft.y, bottomLeft.z, texCoordBottomLeft.x, texCoordBottomLeft.y, bottomRight.x, bottomRight.y, bottomRight.z, texCoordBottomRight.x, texCoordBottomRight.y, + topLeft.x, topLeft.y, topLeft.z, texCoordTopLeft.x, texCoordTopLeft.y, topRight.x, topRight.y, topRight.z, texCoordTopRight.x, texCoordTopRight.y, }; @@ -1363,24 +1313,13 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, cons ((int(color.w * 255.0f) & 0xFF) << 24); int colors[NUM_COLOR_SCALARS_PER_QUAD] = { compactColor, compactColor, compactColor, compactColor }; - // Sam's recommended triangle slices - // Triangle tri1 = { v0, v1, v3 }; - // Triangle tri2 = { v1, v2, v3 }; - // NOTE: Random guy on the internet's recommended triangle slices - // Triangle tri1 = { v0, v1, v2 }; - // Triangle tri2 = { v2, v3, v0 }; - - quint16 indices[NUMBER_OF_INDICES] = { 0, 1, 3, 1, 2, 3 }; - details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer); details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors); - details.indicesBuffer->append(sizeof(indices), (gpu::Byte*) indices); } batch.setInputFormat(details.streamFormat); batch.setInputStream(0, *details.stream); - batch.setIndexBuffer(gpu::UINT16, details.indicesBuffer, 0); - batch.drawIndexed(gpu::TRIANGLES, NUMBER_OF_INDICES, 0); + batch.draw(gpu::TRIANGLE_STRIP, 4, 0); } void GeometryCache::renderDashedLine(gpu::Batch& batch, const glm::vec3& start, const glm::vec3& end, const glm::vec4& color, int id) { diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 19e84e9346..812d12b846 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -235,12 +235,10 @@ private: static int population; gpu::BufferPointer verticesBuffer; gpu::BufferPointer colorBuffer; - gpu::BufferPointer indicesBuffer; gpu::Stream::FormatPointer streamFormat; gpu::BufferStreamPointer stream; int vertices; - int indices; int vertexSize; bool isCreated; From b02f751830bdc9cbd526af7c57608ba1dadd7348 Mon Sep 17 00:00:00 2001 From: ZappoMan Date: Sat, 1 Aug 2015 16:53:35 -0700 Subject: [PATCH 220/242] diff redux --- libraries/render-utils/src/GeometryCache.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 3d43cc6934..ca14e80cd3 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1041,7 +1041,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co minCorner.x, minCorner.y, maxCorner.x, minCorner.y, minCorner.x, maxCorner.y, - maxCorner.x, maxCorner.y + maxCorner.x, maxCorner.y, }; const int NUM_COLOR_SCALARS_PER_QUAD = 4; @@ -1136,7 +1136,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co minCorner.x, minCorner.y, texCoordMinCorner.x, texCoordMinCorner.y, maxCorner.x, minCorner.y, texCoordMaxCorner.x, texCoordMinCorner.y, minCorner.x, maxCorner.y, texCoordMinCorner.x, texCoordMaxCorner.y, - maxCorner.x, maxCorner.y, texCoordMaxCorner.x, texCoordMaxCorner.y + maxCorner.x, maxCorner.y, texCoordMaxCorner.x, texCoordMaxCorner.y, }; @@ -1210,7 +1210,7 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, co minCorner.x, minCorner.y, minCorner.z, maxCorner.x, minCorner.y, minCorner.z, minCorner.x, maxCorner.y, maxCorner.z, - maxCorner.x, maxCorner.y, maxCorner.z + maxCorner.x, maxCorner.y, maxCorner.z, }; const int NUM_COLOR_SCALARS_PER_QUAD = 4; From 771ce6dca311ef6ec5be54e0075881b7da1c9d7e Mon Sep 17 00:00:00 2001 From: ZappoMan Date: Sat, 1 Aug 2015 17:20:48 -0700 Subject: [PATCH 221/242] remove QUAD_STRIP from Circle3DOverlay --- interface/src/ui/overlays/Circle3DOverlay.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/interface/src/ui/overlays/Circle3DOverlay.cpp b/interface/src/ui/overlays/Circle3DOverlay.cpp index 53f1b4ce21..fafb759439 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.cpp +++ b/interface/src/ui/overlays/Circle3DOverlay.cpp @@ -119,19 +119,21 @@ void Circle3DOverlay::render(RenderArgs* args) { float angle = startAt; float angleInRadians = glm::radians(angle); - glm::vec2 firstInnerPoint(cosf(angleInRadians) * innerRadius, sinf(angleInRadians) * innerRadius); - glm::vec2 firstOuterPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); - - points << firstInnerPoint << firstOuterPoint; + glm::vec2 mostRecentInnerPoint(cosf(angleInRadians) * innerRadius, sinf(angleInRadians) * innerRadius); + glm::vec2 mostRecentOuterPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); while (angle < endAt) { angleInRadians = glm::radians(angle); glm::vec2 thisInnerPoint(cosf(angleInRadians) * innerRadius, sinf(angleInRadians) * innerRadius); glm::vec2 thisOuterPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); - points << thisOuterPoint << thisInnerPoint; + points << mostRecentInnerPoint << mostRecentOuterPoint << thisOuterPoint; // first triangle + points << mostRecentInnerPoint << thisInnerPoint << thisOuterPoint; // second triangle angle += SLICE_ANGLE; + + mostRecentInnerPoint = thisInnerPoint; + mostRecentOuterPoint = thisOuterPoint; } // get the last slice portion.... @@ -139,13 +141,14 @@ void Circle3DOverlay::render(RenderArgs* args) { angleInRadians = glm::radians(angle); glm::vec2 lastInnerPoint(cosf(angleInRadians) * innerRadius, sinf(angleInRadians) * innerRadius); glm::vec2 lastOuterPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); - - points << lastOuterPoint << lastInnerPoint; + + points << mostRecentInnerPoint << mostRecentOuterPoint << lastOuterPoint; // first triangle + points << mostRecentInnerPoint << lastInnerPoint << lastOuterPoint; // second triangle geometryCache->updateVertices(_quadVerticesID, points, color); } - geometryCache->renderVertices(batch, gpu::QUAD_STRIP, _quadVerticesID); + geometryCache->renderVertices(batch, gpu::TRIANGLES, _quadVerticesID); } else { if (_lineVerticesID == GeometryCache::UNKNOWN_ID) { From 493836e36312647ab5fa25313e9937ed704000f7 Mon Sep 17 00:00:00 2001 From: ZappoMan Date: Sat, 1 Aug 2015 17:26:38 -0700 Subject: [PATCH 222/242] remove QUADS from GLBackendShared and Format --- libraries/gpu/src/gpu/Format.h | 5 ----- libraries/gpu/src/gpu/GLBackendShared.h | 2 -- 2 files changed, 7 deletions(-) diff --git a/libraries/gpu/src/gpu/Format.h b/libraries/gpu/src/gpu/Format.h index 01d3f37ef8..b710a44f39 100644 --- a/libraries/gpu/src/gpu/Format.h +++ b/libraries/gpu/src/gpu/Format.h @@ -213,11 +213,6 @@ enum Primitive { TRIANGLES, TRIANGLE_STRIP, TRIANGLE_FAN, - - // FIXME - remove these - QUADS, - QUAD_STRIP, - NUM_PRIMITIVES, }; diff --git a/libraries/gpu/src/gpu/GLBackendShared.h b/libraries/gpu/src/gpu/GLBackendShared.h index 888fd1164d..7ce54665be 100644 --- a/libraries/gpu/src/gpu/GLBackendShared.h +++ b/libraries/gpu/src/gpu/GLBackendShared.h @@ -23,8 +23,6 @@ static const GLenum _primitiveToGLmode[gpu::NUM_PRIMITIVES] = { GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, - GL_QUADS, - GL_QUAD_STRIP, }; static const GLenum _elementTypeToGLType[gpu::NUM_TYPES] = { From 8f616c04fb08154b6b4f4dee5a78131c57e75cd8 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sun, 2 Aug 2015 21:46:39 -0700 Subject: [PATCH 223/242] Fix https://app.asana.com/0/32622044445063/43589567731045 --- libraries/audio-client/src/AudioClient.cpp | 17 ++++++++--------- libraries/audio-client/src/AudioClient.h | 3 --- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 908310d504..a3d3a72043 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1037,9 +1037,14 @@ bool AudioClient::outputLocalInjector(bool isStereo, AudioInjector* injector) { localOutput->moveToThread(injector->getLocalBuffer()->thread()); // have it be stopped when that local buffer is about to close - connect(localOutput, &QAudioOutput::stateChanged, this, &AudioClient::audioStateChanged); - connect(this, &AudioClient::audioFinished, localOutput, &QAudioOutput::stop); - connect(this, &AudioClient::audioFinished, injector, &AudioInjector::stop); + // We don't want to stop this localOutput and injector whenever this AudioClient singleton goes idle, + // only when the localOutput does. But the connection is to localOutput, so that it happens on the right thread. + connect(localOutput, &QAudioOutput::stateChanged, localOutput, [=](QAudio::State state) { + if (state == QAudio::IdleState) { + localOutput->stop(); + injector->stop(); + } + }); connect(injector->getLocalBuffer(), &QIODevice::aboutToClose, localOutput, &QAudioOutput::stop); @@ -1358,9 +1363,3 @@ void AudioClient::saveSettings() { windowSecondsForDesiredReduction.set(_receivedAudioStream.getWindowSecondsForDesiredReduction()); repetitionWithFade.set(_receivedAudioStream.getRepetitionWithFade()); } - -void AudioClient::audioStateChanged(QAudio::State state) { - if (state == QAudio::IdleState) { - emit audioFinished(); - } -} diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index a1d08ec540..3205aeda1d 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -209,9 +209,6 @@ protected: deleteLater(); } -private slots: - void audioStateChanged(QAudio::State state); - private: void outputFormatChanged(); From 055c9dc59b5e064bfde03281dd894148cf487a21 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Mon, 3 Aug 2015 10:41:35 -0700 Subject: [PATCH 224/242] Fix image URLs in controlPanel.js. --- examples/controlPanel.js | 8 ++++---- examples/libraries/overlayManager.js | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/controlPanel.js b/examples/controlPanel.js index 837b59ffa0..ebad7d8d83 100644 --- a/examples/controlPanel.js +++ b/examples/controlPanel.js @@ -17,10 +17,10 @@ Script.include([ ]); var BG_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/card-bg.svg"; -var CLOSE_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/close.svg"; -var MIC_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/mic-toggle.svg"; -var FACE_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/face-toggle.svg"; -var ADDRESS_BAR_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/address-bar-toggle.svg"; +var CLOSE_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/tools/close.svg"; +var MIC_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/tools/mic-toggle.svg"; +var FACE_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/tools/face-toggle.svg"; +var ADDRESS_BAR_IMAGE_URL = HIFI_PUBLIC_BUCKET + "images/tools/address-bar-toggle.svg"; var panel = new FloatingUIPanel({ anchorPosition: { diff --git a/examples/libraries/overlayManager.js b/examples/libraries/overlayManager.js index 55575badee..cd05cd2a52 100644 --- a/examples/libraries/overlayManager.js +++ b/examples/libraries/overlayManager.js @@ -24,6 +24,10 @@ // // See more on usage below. // +// Note that including this file will delete Overlays from the global scope. All the +// functionality of Overlays is represented here, just better. If you try to use Overlays in +// tandem, there may be performance problems or nasty surprises. +// // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // From d24006e71593ae6db7d03b921d6b6dca011eb462 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Mon, 3 Aug 2015 11:14:55 -0700 Subject: [PATCH 225/242] Improve logic around handling click and drag. --- examples/controlPanel.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/examples/controlPanel.js b/examples/controlPanel.js index ebad7d8d83..59d66fdaf9 100644 --- a/examples/controlPanel.js +++ b/examples/controlPanel.js @@ -172,26 +172,25 @@ function onFaceMuteToggled() { } onFaceMuteToggled(); -var isLeftClick = false, - isRightClick = false; +var mouseDown = {}; function onMouseDown(event) { - isLeftClick = event.isLeftButton; - isRightClick = event.isRightButton; -} - -function onMouseMove(event) { - isLeftClick = isRightClick = false; + if (event.isLeftButton) { + mouseDown.overlay = OverlayManager.findAtPoint({ x: event.x, y: event.y }); + } + if (event.isRightButton) { + mouseDown.pos = { x: event.x, y: event.y }; + } } function onMouseUp(event) { - if (isLeftClick && event.isLeftButton) { + if (event.isLeftButton) { var overlay = OverlayManager.findAtPoint({ x: event.x, y: event.y }); - if (overlay && overlay.onClick) { + if (overlay && overlay === mouseDown.overlay && overlay.onClick) { overlay.onClick(event); } } - if (isRightClick && event.isRightButton) { + if (event.isRightButton && Vec3.distance(mouseDown.pos, { x: event.x, y: event.y }) < 5) { panel.setProperties({ visible: !panel.visible, offsetRotation: { @@ -200,7 +199,8 @@ function onMouseUp(event) { } }); } - isLeftClick = isRightClick = false; + + mouseDown = {}; } function onScriptEnd(event) { @@ -208,7 +208,6 @@ function onScriptEnd(event) { } Controller.mousePressEvent.connect(onMouseDown); -Controller.mouseMoveEvent.connect(onMouseMove); Controller.mouseReleaseEvent.connect(onMouseUp); AudioDevice.muteToggled.connect(onMicMuteToggled); FaceTracker.muteToggled.connect(onFaceMuteToggled); From 1b4ba75b5a597313576940810833cb5f2a59d82c Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Mon, 3 Aug 2015 12:01:15 -0700 Subject: [PATCH 226/242] Fix click and drag logic for floatingUIExample.js. --- examples/example/ui/floatingUIExample.js | 43 ++++++++++++------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/examples/example/ui/floatingUIExample.js b/examples/example/ui/floatingUIExample.js index 9c4e43be94..1e957fe6b3 100644 --- a/examples/example/ui/floatingUIExample.js +++ b/examples/example/ui/floatingUIExample.js @@ -127,35 +127,37 @@ blueSquare3.offsetPosition = { }; -function onMouseDown(event) { - isLeftClick = event.isLeftButton; - isRightClick = event.isRightButton; -} +var mouseDown = {}; -function onMouseMove(event) { - isLeftClick = isRightClick = false; +function onMouseDown(event) { + if (event.isLeftButton) { + mouseDown.overlay = OverlayManager.findAtPoint({ x: event.x, y: event.y }); + } + if (event.isRightButton) { + mouseDown.pos = { x: event.x, y: event.y }; + } } function onMouseUp(event) { - if (isLeftClick && event.isLeftButton) { + if (event.isLeftButton) { var overlay = OverlayManager.findAtPoint({ x: event.x, y: event.y }); - print(overlay.attachedPanel); - if (overlay.attachedPanel === bluePanel) { - overlay.destroy(); - } else if (overlay) { - var oldPos = overlay.offsetPosition; - var newPos = { - x: Number(oldPos.x), - y: Number(oldPos.y), - z: Number(oldPos.z) + 0.1 - }; - overlay.offsetPosition = newPos; + if (overlay === mouseDown.overlay) { + if (overlay.attachedPanel === bluePanel) { + overlay.destroy(); + } else if (overlay) { + var oldPos = overlay.offsetPosition; + var newPos = { + x: Number(oldPos.x), + y: Number(oldPos.y), + z: Number(oldPos.z) + 0.1 + }; + overlay.offsetPosition = newPos; + } } } - if (isRightClick && event.isRightButton) { + if (event.isRightButton && Vec3.distance(mouseDown.pos, { x: event.x, y: event.y }) < 5) { mainPanel.visible = !mainPanel.visible; } - isLeftClick = isRightClick = false; } function onScriptEnd() { @@ -163,6 +165,5 @@ function onScriptEnd() { } Controller.mousePressEvent.connect(onMouseDown); -Controller.mouseMoveEvent.connect(onMouseMove); Controller.mouseReleaseEvent.connect(onMouseUp); Script.scriptEnding.connect(onScriptEnd); \ No newline at end of file From 5e7cb728438b4ce6391c3241251e129249046de0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 3 Aug 2015 13:02:12 -0700 Subject: [PATCH 227/242] Reduce the speed of movement when in HMD view When using hmdControls.js. --- examples/hmdControls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/hmdControls.js b/examples/hmdControls.js index e14ddca3ef..04c1dade0a 100644 --- a/examples/hmdControls.js +++ b/examples/hmdControls.js @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var MOVE_DISTANCE = 10.0; +var MOVE_DISTANCE = 2.0; var PITCH_INCREMENT = 0.5; // degrees var pitchChange = 0; // degrees var YAW_INCREMENT = 0.5; // degrees From 4c1d1a65d50aab90ca19ccc5d482c1c28732f9e6 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 3 Aug 2015 15:41:15 -0700 Subject: [PATCH 228/242] Provide initial fight club animations and use them in rig. Still only active when you do Settings.setValue('enableRig', true) and restart. (Will be more exposed after fadein/fadeout is implemented.) --- libraries/animation/src/Rig.cpp | 100 ++++++++++++++++++++++---------- libraries/animation/src/Rig.h | 4 +- 2 files changed, 70 insertions(+), 34 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 8406b61d12..45828d44b6 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -90,8 +90,34 @@ void Rig::addAnimationByRole(const QString& role, const QString& url, float fps, } } AnimationHandlePointer handle = createAnimationHandle(); + QString standard = ""; + if (url.isEmpty()) { // Default animations for fight club + const QString& base = "https://hifi-public.s3.amazonaws.com/ozan/"; + if (role == "walk") { + standard = base + "support/FightClubBotTest1/Animations/standard_walk.fbx"; + lastFrame = 60; + } else if (role == "leftTurn") { + standard = base + "support/FightClubBotTest1/Animations/left_turn_noHipRotation.fbx"; + lastFrame = 29; + } else if (role == "rightTurn") { + standard = base + "support/FightClubBotTest1/Animations/right_turn_noHipRotation.fbx"; + lastFrame = 31; + } else if (role == "leftStrafe") { + standard = base + "animations/fightclub_bot_anims/side_step_left_inPlace.fbx"; + lastFrame = 31; + } else if (role == "rightStrafe") { + standard = base + "animations/fightclub_bot_anims/side_step_right_inPlace.fbx"; + lastFrame = 31; + } else if (role == "idle") { + standard = base + "support/FightClubBotTest1/Animations/standard_idle.fbx"; + fps = 25.0f; + } + if (!standard.isEmpty()) { + loop = true; + } + } handle->setRole(role); - handle->setURL(url); + handle->setURL(url.isEmpty() ? standard : url); handle->setFPS(fps); handle->setPriority(priority); handle->setLoop(loop); @@ -135,6 +161,14 @@ void Rig::addRunningAnimation(AnimationHandlePointer animationHandle) { bool Rig::isRunningAnimation(AnimationHandlePointer animationHandle) { return _runningAnimations.contains(animationHandle); } +bool Rig::isRunningRole(const QString& role) { //obviously, there are more efficient ways to do this + for (auto animation : _runningAnimations) { + if (animation->getRole() == role) { + return true; + } + } + return false; +} void Rig::deleteAnimations() { for (auto animation : _animationHandles) { @@ -356,37 +390,41 @@ glm::mat4 Rig::getJointVisibleTransform(int jointIndex) const { } void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation) { - if (_enableRig) { - glm::vec3 front = worldRotation * IDENTITY_FRONT; - float forwardSpeed = glm::dot(worldVelocity, front); - float rotationalSpeed = glm::angle(front, _lastFront) / deltaTime; - bool isWalking = std::abs(forwardSpeed) > 0.01f; - bool isTurning = std::abs(rotationalSpeed) > 0.5f; - - // Crude, until we have blending: - isTurning = isTurning && !isWalking; // Only one of walk/turn, walk wins. - isTurning = false; // FIXME - bool isIdle = !isWalking && !isTurning; - auto singleRole = [](bool walking, bool turning, bool idling) { - return walking ? "walk" : (turning ? "turn" : (idling ? "idle" : "")); - }; - QString toStop = singleRole(_isWalking && !isWalking, _isTurning && !isTurning, _isIdle && !isIdle); - if (!toStop.isEmpty()) { - //qCDebug(animation) << "isTurning" << isTurning << "fronts" << front << _lastFront << glm::angle(front, _lastFront) << rotationalSpeed; - stopAnimationByRole(toStop); - } - QString newRole = singleRole(isWalking && !_isWalking, isTurning && !_isTurning, isIdle && !_isIdle); - if (!newRole.isEmpty()) { - startAnimationByRole(newRole); - qCDebug(animation) << deltaTime << ":" << worldVelocity << "." << front << "=> " << forwardSpeed << newRole; - } - - _lastPosition = worldPosition; - _lastFront = front; - _isWalking = isWalking; - _isTurning = isTurning; - _isIdle = isIdle; + if (!_enableRig) { + return; } + bool isMoving = false; + glm::vec3 front = worldRotation * IDENTITY_FRONT; + float forwardSpeed = glm::dot(worldVelocity, front); + float rightLateralSpeed = glm::dot(worldVelocity, worldRotation * IDENTITY_RIGHT); + float rightTurningSpeed = glm::orientedAngle(front, _lastFront, IDENTITY_UP) / deltaTime; + auto updateRole = [=](const QString& role, bool isOn) mutable { + isMoving = isMoving || isOn; + if (isOn) { + if (!isRunningRole(role)) { + qCDebug(animation) << "Rig STARTING" << role; + if (role == "leftTurn" || role == "rightTurn") { + qCDebug(animation) << front.x << front.y << front.z << "=>" << rightTurningSpeed; + } + startAnimationByRole(role); + } + } else { + if (isRunningRole(role)) { + qCDebug(animation) << "Rig stopping" << role; + stopAnimationByRole(role); + } + } + }; + updateRole("walk", std::abs(forwardSpeed) > 0.01f); + bool isTurning = std::abs(rightTurningSpeed) > 0.5f; + updateRole("rightTurn", isTurning && (rightTurningSpeed > 0)); + updateRole("leftTurn", isTurning && (rightTurningSpeed < 0)); + bool isStrafing = std::abs(rightLateralSpeed) > 0.01f; + updateRole("rightStrafe", isStrafing && (rightLateralSpeed > 0.0f)); + updateRole("leftStrafe", isStrafing && (rightLateralSpeed < 0.0f)); + updateRole("idle", !isMoving); // Must be last, as it makes isMoving bogus. + _lastFront = front; + _lastPosition = worldPosition; } void Rig::updateAnimations(float deltaTime, glm::mat4 parentTransform) { diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 52db16826a..a79b99f4dc 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -76,6 +76,7 @@ public: bool removeRunningAnimation(AnimationHandlePointer animationHandle); void addRunningAnimation(AnimationHandlePointer animationHandle); bool isRunningAnimation(AnimationHandlePointer animationHandle); + bool isRunningRole(const QString& role); // There can be multiple animations per role, so this is more general than isRunningAnimation. const QList& getRunningAnimations() const { return _runningAnimations; } void deleteAnimations(); const QList& getAnimationHandles() const { return _animationHandles; } @@ -161,9 +162,6 @@ public: QList _runningAnimations; bool _enableRig; - bool _isWalking; - bool _isTurning; - bool _isIdle; glm::vec3 _lastFront; glm::vec3 _lastPosition; }; From cdb697760f30f738c79ace5ab87cea573f8e334c Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 3 Aug 2015 15:51:07 -0700 Subject: [PATCH 229/242] Remove some debug. --- libraries/animation/src/Rig.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 45828d44b6..4a03ab83ee 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -403,9 +403,6 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos if (isOn) { if (!isRunningRole(role)) { qCDebug(animation) << "Rig STARTING" << role; - if (role == "leftTurn" || role == "rightTurn") { - qCDebug(animation) << front.x << front.y << front.z << "=>" << rightTurningSpeed; - } startAnimationByRole(role); } } else { From 2e5142e9bb5110544fea62f597039cf9021bc0ea Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 3 Aug 2015 16:09:48 -0700 Subject: [PATCH 230/242] Fix end of idle. --- libraries/animation/src/Rig.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 4a03ab83ee..f8419509d4 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -398,7 +398,7 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos float forwardSpeed = glm::dot(worldVelocity, front); float rightLateralSpeed = glm::dot(worldVelocity, worldRotation * IDENTITY_RIGHT); float rightTurningSpeed = glm::orientedAngle(front, _lastFront, IDENTITY_UP) / deltaTime; - auto updateRole = [=](const QString& role, bool isOn) mutable { + auto updateRole = [&](const QString& role, bool isOn) { isMoving = isMoving || isOn; if (isOn) { if (!isRunningRole(role)) { From 61afc362e770d3287832aa390696b4b9ab5bdf7e Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Mon, 3 Aug 2015 17:00:39 -0700 Subject: [PATCH 231/242] For startAnimation (but not startAnimationByRole), use the specified parameters even if the animation was already playing. This fixes the behavior exercised by the squeezeHands.js script. Fixes https://app.asana.com/0/32622044445063/44025709513292 --- libraries/animation/src/Rig.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 8406b61d12..b91606dc8e 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -58,15 +58,18 @@ void Rig::removeAnimationHandle(const AnimationHandlePointer& handle) { void Rig::startAnimation(const QString& url, float fps, float priority, bool loop, bool hold, float firstFrame, float lastFrame, const QStringList& maskedJoints) { - //qCDebug(animation) << "startAnimation" << url << fps << priority << loop << hold << firstFrame << lastFrame << maskedJoints; + // This is different than startAnimationByRole, in which we use the existing values if the animation already exists. + // Here we reuse the animation handle if possible, but in any case, we set the values to those given (or defaulted). + AnimationHandlePointer handle = nullptr; foreach (const AnimationHandlePointer& candidate, _animationHandles) { if (candidate->getURL() == url) { - candidate->start(); - return; + handle = candidate; } } - AnimationHandlePointer handle = createAnimationHandle(); - handle->setURL(url); + if (!handle) { + handle = createAnimationHandle(); + handle->setURL(url); + } handle->setFPS(fps); handle->setPriority(priority); handle->setLoop(loop); From 4bf6f8ed7798a056eaa788535c261c0e8449f837 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 4 Aug 2015 14:47:13 +0200 Subject: [PATCH 232/242] ModelOverlay causes a crash in the Model class. This is because ModelOverlay doesn't give a valid rig to the Model. --- interface/src/ui/overlays/ModelOverlay.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index de8e05323b..49321e7d6d 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -10,6 +10,7 @@ // #include "ModelOverlay.h" +#include "EntityRig.h" #include "Application.h" @@ -17,7 +18,7 @@ QString const ModelOverlay::TYPE = "model"; ModelOverlay::ModelOverlay() - : _model(nullptr), + : _model(std::make_shared()), _modelTextures(QVariantMap()), _updateModel(false) { @@ -27,7 +28,7 @@ ModelOverlay::ModelOverlay() ModelOverlay::ModelOverlay(const ModelOverlay* modelOverlay) : Volume3DOverlay(modelOverlay), - _model(nullptr), + _model(std::make_shared()), _modelTextures(QVariantMap()), _url(modelOverlay->_url), _updateModel(false) From adcd91e3ff007e6d89b1684038ece4be87395167 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 4 Aug 2015 07:23:24 -0700 Subject: [PATCH 233/242] Added an option to domain-server settings to persist entities as gzipped json. With this setting enabled, persist files take much less space on disk than with the other two options. --- .../resources/describe-settings.json | 4 + interface/CMakeLists.txt | 31 ++-- .../entities/src/EntityItemProperties.cpp | 3 +- libraries/octree/src/Octree.cpp | 122 ++++++++++---- libraries/octree/src/Octree.h | 3 +- libraries/shared/CMakeLists.txt | 4 +- libraries/shared/src/Gzip.cpp | 152 ++++++++++++++++++ libraries/shared/src/Gzip.h | 20 +++ 8 files changed, 288 insertions(+), 51 deletions(-) create mode 100644 libraries/shared/src/Gzip.cpp create mode 100644 libraries/shared/src/Gzip.h diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index ff2f4cf683..696a87d2b8 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -389,6 +389,10 @@ { "value": "json", "label": "Entity server persists data as JSON" + }, + { + "value": "json.gz", + "label": "Entity server persists data as gzipped JSON" } ], "advanced": true diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index f1ef38ade9..11af6e70a0 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -13,6 +13,7 @@ endforeach() find_package(Qt5LinguistTools REQUIRED) find_package(Qt5LinguistToolsMacros) +find_package(ZLIB REQUIRED) if (DEFINED ENV{JOB_ID}) set(BUILD_SEQ $ENV{JOB_ID}) @@ -23,8 +24,8 @@ else () endif () if (WIN32) - add_definitions(-D_USE_MATH_DEFINES) # apparently needed to get M_PI and other defines from cmath/math.h - add_definitions(-DWINDOWS_LEAN_AND_MEAN) # needed to make sure windows doesn't go to crazy with its defines + add_definitions(-D_USE_MATH_DEFINES) # apparently needed to get M_PI and other defines from cmath/math.h + add_definitions(-DWINDOWS_LEAN_AND_MEAN) # needed to make sure windows doesn't go to crazy with its defines endif() configure_file(InterfaceVersion.h.in "${PROJECT_BINARY_DIR}/includes/InterfaceVersion.h") @@ -82,19 +83,19 @@ if (APPLE) set(MACOSX_BUNDLE_BUNDLE_NAME Interface) set(MACOSX_BUNDLE_GUI_IDENTIFIER io.highfidelity.Interface) - + if (UPPER_CMAKE_BUILD_TYPE MATCHES RELEASE OR UPPER_CMAKE_BUILD_TYPE MATCHES RELWITHDEBINFO) set(ICON_FILENAME "interface.icns") else () set(ICON_FILENAME "interface-beta.icns") endif () - + # set how the icon shows up in the Info.plist file SET(MACOSX_BUNDLE_ICON_FILE "${ICON_FILENAME}") # set where in the bundle to put the resources file SET_SOURCE_FILES_PROPERTIES(${CMAKE_CURRENT_SOURCE_DIR}/icon/${ICON_FILENAME} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) - + set(DISCOVERED_RESOURCES "") # use the add_resources_to_os_x_bundle macro to recurse into resources @@ -102,7 +103,7 @@ if (APPLE) # append the discovered resources to our list of interface sources list(APPEND INTERFACE_SRCS ${DISCOVERED_RESOURCES}) - + set(INTERFACE_SRCS ${INTERFACE_SRCS} "${CMAKE_CURRENT_SOURCE_DIR}/icon/${ICON_FILENAME}") endif() @@ -145,34 +146,34 @@ add_dependency_external_projects(sdl2) # perform standard include and linking for found externals foreach(EXTERNAL ${OPTIONAL_EXTERNALS}) - + if (${${EXTERNAL}_UPPERCASE}_REQUIRED) find_package(${EXTERNAL} REQUIRED) else () find_package(${EXTERNAL}) endif () - + if (${${EXTERNAL}_UPPERCASE}_FOUND AND NOT DISABLE_${${EXTERNAL}_UPPERCASE}) add_definitions(-DHAVE_${${EXTERNAL}_UPPERCASE}) - + # include the library directories (ignoring warnings) if (NOT ${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS) set(${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIR}) endif () - + include_directories(SYSTEM ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS}) - + # perform the system include hack for OS X to ignore warnings if (APPLE) foreach(EXTERNAL_INCLUDE_DIR ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS}) SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -isystem ${EXTERNAL_INCLUDE_DIR}") endforeach() endif () - + if (NOT ${${EXTERNAL}_UPPERCASE}_LIBRARIES) set(${${EXTERNAL}_UPPERCASE}_LIBRARIES ${${${EXTERNAL}_UPPERCASE}_LIBRARY}) endif () - + if (NOT APPLE OR NOT ${${EXTERNAL}_UPPERCASE} MATCHES "SIXENSE") target_link_libraries(${TARGET_NAME} ${${${EXTERNAL}_UPPERCASE}_LIBRARIES}) elseif (APPLE AND NOT INSTALLER_BUILD) @@ -199,13 +200,12 @@ target_link_libraries( # assume we are using a Qt build without bearer management add_definitions(-DQT_NO_BEARERMANAGEMENT) + if (APPLE) # link in required OS X frameworks and include the right GL headers find_library(OpenGL OpenGL) find_library(AppKit AppKit) - target_link_libraries(${TARGET_NAME} ${OpenGL} ${AppKit}) - # install command for OS X bundle INSTALL(TARGETS ${TARGET_NAME} BUNDLE DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/install" COMPONENT Runtime @@ -224,7 +224,6 @@ else (APPLE) # target_link_libraries(${TARGET_NAME} wsock32.lib Winmm.lib) target_link_libraries(${TARGET_NAME} wsock32.lib Winmm.lib) else (WIN32) - # Nothing else required on linux apparently endif() endif (APPLE) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index ebe4ac6014..4e7228e573 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -71,7 +71,8 @@ CONSTRUCT_PROPERTY(exponent, 0.0f), CONSTRUCT_PROPERTY(cutoff, ENTITY_ITEM_DEFAULT_CUTOFF), CONSTRUCT_PROPERTY(locked, ENTITY_ITEM_DEFAULT_LOCKED), CONSTRUCT_PROPERTY(textures, ""), -CONSTRUCT_PROPERTY(animationSettings, ""), +CONSTRUCT_PROPERTY(animationSettings, "{\"firstFrame\":0,\"fps\":30,\"frameIndex\":0,\"hold\":false," + "\"lastFrame\":100000,\"loop\":false,\"running\":false,\"startAutomatically\":false}"), CONSTRUCT_PROPERTY(userData, ENTITY_ITEM_DEFAULT_USER_DATA), CONSTRUCT_PROPERTY(simulationOwner, SimulationOwner()), CONSTRUCT_PROPERTY(text, TextEntityItem::DEFAULT_TEXT), diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index 203ff2b072..25970d66d5 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include "CoverageMap.h" #include "OctreeConstants.h" @@ -49,7 +50,7 @@ #include "OctreeLogging.h" -QVector PERSIST_EXTENSIONS = {"svo", "json"}; +QVector PERSIST_EXTENSIONS = {"svo", "json", "json.gz"}; float boundaryDistanceForRenderLevel(unsigned int renderLevel, float voxelSizeScale) { return voxelSizeScale / powf(2, renderLevel); @@ -1809,29 +1810,52 @@ int Octree::encodeTreeBitstreamRecursion(OctreeElement* element, } bool Octree::readFromFile(const char* fileName) { - bool fileOk = false; - QString qFileName = findMostRecentFileExtension(fileName, PERSIST_EXTENSIONS); - QFile file(qFileName); - fileOk = file.open(QIODevice::ReadOnly); - if(fileOk) { - QDataStream fileInputStream(&file); - QFileInfo fileInfo(qFileName); - unsigned long fileLength = fileInfo.size(); - - emit importSize(1.0f, 1.0f, 1.0f); - emit importProgress(0); - - qCDebug(octree) << "Loading file" << qFileName << "..."; - - fileOk = readFromStream(fileLength, fileInputStream); - - emit importProgress(100); - file.close(); + if (qFileName.endsWith(".json.gz")) { + return readJSONFromGzippedFile(qFileName); } - return fileOk; + QFile file(qFileName); + + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "unable to open for reading: " << fileName; + return false; + } + + QDataStream fileInputStream(&file); + QFileInfo fileInfo(qFileName); + unsigned long fileLength = fileInfo.size(); + + emit importSize(1.0f, 1.0f, 1.0f); + emit importProgress(0); + + qCDebug(octree) << "Loading file" << qFileName << "..."; + + bool success = readFromStream(fileLength, fileInputStream); + + emit importProgress(100); + file.close(); + + return success; +} + +bool Octree::readJSONFromGzippedFile(QString qFileName) { + QFile file(qFileName); + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Cannot open gzipped json file for reading: " << qFileName; + return false; + } + QByteArray compressedJsonData = file.readAll(); + QByteArray jsonData; + + if (!gunzip(compressedJsonData, jsonData)) { + qCritical() << "json File not in gzip format: " << qFileName; + return false; + } + + QDataStream jsonStream(jsonData); + return readJSONFromStream(-1, jsonStream); } bool Octree::readFromURL(const QString& urlString) { @@ -1868,17 +1892,17 @@ bool Octree::readFromURL(const QString& urlString) { bool Octree::readFromStream(unsigned long streamLength, QDataStream& inputStream) { - // decide if this is SVO or JSON + // decide if this is binary SVO or JSON encoded SVO QIODevice *device = inputStream.device(); char firstChar; device->getChar(&firstChar); device->ungetChar(firstChar); if (firstChar == (char) PacketType::EntityData) { - qCDebug(octree) << "Reading from SVO Stream length:" << streamLength; + qCDebug(octree) << "Reading from binary SVO Stream length:" << streamLength; return readSVOFromStream(streamLength, inputStream); } else { - qCDebug(octree) << "Reading from JSON Stream length:" << streamLength; + qCDebug(octree) << "Reading from JSON SVO Stream length:" << streamLength; return readJSONFromStream(streamLength, inputStream); } } @@ -2013,12 +2037,28 @@ bool Octree::readSVOFromStream(unsigned long streamLength, QDataStream& inputStr return fileOk; } -bool Octree::readJSONFromStream(unsigned long streamLength, QDataStream& inputStream) { - char* rawData = new char[streamLength + 1]; // allocate enough room to null terminate - inputStream.readRawData(rawData, streamLength); - rawData[streamLength] = 0; // make sure we null terminate this string +const int READ_JSON_BUFFER_SIZE = 2048; - QJsonDocument asDocument = QJsonDocument::fromJson(rawData); +bool Octree::readJSONFromStream(unsigned long streamLength, QDataStream& inputStream) { + // QuaGzipFile doesn't appear to give a useful bytesAvailable() result, so just keep reading until + // we get an eof. Leave streamLength parameter for consistency. + + QByteArray jsonBuffer; + char* rawData = new char[READ_JSON_BUFFER_SIZE]; + while (true) { + int got = inputStream.readRawData(rawData, READ_JSON_BUFFER_SIZE - 1); + if (got < 0) { + qCritical() << "error while reading from json stream"; + delete[] rawData; + return false; + } + if (got == 0) { + break; + } + jsonBuffer += QByteArray(rawData, got); + } + + QJsonDocument asDocument = QJsonDocument::fromJson(jsonBuffer); QVariant asVariant = asDocument.toVariant(); QVariantMap asMap = asVariant.toMap(); readFromMap(asMap); @@ -2036,13 +2076,14 @@ void Octree::writeToFile(const char* fileName, OctreeElement* element, QString p writeToSVOFile(fileName, element); } else if (persistAsFileType == "json") { writeToJSONFile(cFileName, element); + } else if (persistAsFileType == "json.gz") { + writeToJSONFile(cFileName, element, true); } else { qCDebug(octree) << "unable to write octree to file of type" << persistAsFileType; } } -void Octree::writeToJSONFile(const char* fileName, OctreeElement* element) { - QFile persistFile(fileName); +void Octree::writeToJSONFile(const char* fileName, OctreeElement* element, bool doGzip) { QVariantMap entityDescription; qCDebug(octree, "Saving JSON SVO to file %s...", fileName); @@ -2061,10 +2102,27 @@ void Octree::writeToJSONFile(const char* fileName, OctreeElement* element) { // store the entity data bool entityDescriptionSuccess = writeToMap(entityDescription, top, true); + if (!entityDescriptionSuccess) { + qCritical("Failed to convert Entities to QVariantMap while saving to json."); + return; + } // convert the QVariantMap to JSON - if (entityDescriptionSuccess && persistFile.open(QIODevice::WriteOnly)) { - persistFile.write(QJsonDocument::fromVariant(entityDescription).toJson()); + QByteArray jsonData = QJsonDocument::fromVariant(entityDescription).toJson(); + QByteArray jsonDataForFile; + + if (doGzip) { + if (!gzip(jsonData, jsonDataForFile, -1)) { + qCritical("unable to gzip data while saving to json."); + return; + } + } else { + jsonDataForFile = jsonData; + } + + QFile persistFile(fileName); + if (persistFile.open(QIODevice::WriteOnly)) { + persistFile.write(jsonDataForFile); } else { qCritical("Could not write to JSON description of entities."); } diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index e00434be80..244359e394 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -330,7 +330,7 @@ public: // Octree exporters void writeToFile(const char* filename, OctreeElement* element = NULL, QString persistAsFileType = "svo"); - void writeToJSONFile(const char* filename, OctreeElement* element = NULL); + void writeToJSONFile(const char* filename, OctreeElement* element = NULL, bool doGzip = false); void writeToSVOFile(const char* filename, OctreeElement* element = NULL); virtual bool writeToMap(QVariantMap& entityDescription, OctreeElement* element, bool skipDefaultValues) = 0; @@ -340,6 +340,7 @@ public: bool readFromStream(unsigned long streamLength, QDataStream& inputStream); bool readSVOFromStream(unsigned long streamLength, QDataStream& inputStream); bool readJSONFromStream(unsigned long streamLength, QDataStream& inputStream); + bool readJSONFromGzippedFile(QString qFileName); virtual bool readFromMap(QVariantMap& entityDescription) = 0; unsigned long getOctreeElementsCount(); diff --git a/libraries/shared/CMakeLists.txt b/libraries/shared/CMakeLists.txt index 8deda7f4b1..298381689d 100644 --- a/libraries/shared/CMakeLists.txt +++ b/libraries/shared/CMakeLists.txt @@ -4,7 +4,9 @@ set(TARGET_NAME shared) # TODO: there isn't really a good reason to have Script linked here - let's get what is requiring it out (RegisteredMetaTypes.cpp) setup_hifi_library(Gui Network Script Widgets) +find_package(ZLIB REQUIRED) +target_link_libraries(${TARGET_NAME} ${ZLIB_LIBRARIES}) + add_dependency_external_projects(glm) find_package(GLM REQUIRED) target_include_directories(${TARGET_NAME} PUBLIC ${GLM_INCLUDE_DIRS}) - diff --git a/libraries/shared/src/Gzip.cpp b/libraries/shared/src/Gzip.cpp new file mode 100644 index 0000000000..ae4504cd24 --- /dev/null +++ b/libraries/shared/src/Gzip.cpp @@ -0,0 +1,152 @@ +// +// Gzip.cpp +// libraries/shared/src +// +// Created by Seth Alves on 2015-08-03. +// 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 "Gzip.h" + +const int GZIP_WINDOWS_BIT = 31; +const int GZIP_CHUNK_SIZE = 4096; + +bool gunzip(QByteArray source, QByteArray &destination) { + destination.clear(); + if (source.length() == 0) { + return true; + } + + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.avail_in = 0; + strm.next_in = Z_NULL; + + int status = inflateInit2(&strm, GZIP_WINDOWS_BIT); + + if (status != Z_OK) { + return false; + } + + char *sourceData = source.data(); + int sourceDataLength = source.length(); + + for (;;) { + int chunkSize = qMin(GZIP_CHUNK_SIZE, sourceDataLength); + if (chunkSize <= 0) { + break; + } + + strm.next_in = (unsigned char*)sourceData; + strm.avail_in = chunkSize; + sourceData += chunkSize; + sourceDataLength -= chunkSize; + + for (;;) { + char out[GZIP_CHUNK_SIZE]; + + strm.next_out = (unsigned char*)out; + strm.avail_out = GZIP_CHUNK_SIZE; + + status = inflate(&strm, Z_NO_FLUSH); + + switch (status) { + case Z_NEED_DICT: + status = Z_DATA_ERROR; + case Z_DATA_ERROR: + case Z_MEM_ERROR: + case Z_STREAM_ERROR: + inflateEnd(&strm); + return false; + } + + int available = (GZIP_CHUNK_SIZE - strm.avail_out); + if (available > 0) { + destination.append((char*)out, available); + } + + if (strm.avail_out != 0) { + break; + } + } + + if (status == Z_STREAM_END) { + break; + } + } + + inflateEnd(&strm); + return status == Z_STREAM_END; +} + +bool gzip(QByteArray source, QByteArray &destination, int compressionLevel) { + destination.clear(); + if (source.length() == 0) { + return true; + } + + int flushOrFinish = 0; + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.next_in = Z_NULL; + strm.avail_in = 0; + + int status = deflateInit2(&strm, + qMax(-1, qMin(9, compressionLevel)), + Z_DEFLATED, + GZIP_WINDOWS_BIT, + 8, + Z_DEFAULT_STRATEGY); + if (status != Z_OK) { + return false; + } + char *sourceData = source.data(); + int sourceDataLength = source.length(); + + for (;;) { + int chunkSize = qMin(GZIP_CHUNK_SIZE, sourceDataLength); + strm.next_in = (unsigned char*)sourceData; + strm.avail_in = chunkSize; + sourceData += chunkSize; + sourceDataLength -= chunkSize; + + if (sourceDataLength <= 0) { + flushOrFinish = Z_FINISH; + } else { + flushOrFinish = Z_NO_FLUSH; + } + + for (;;) { + char out[GZIP_CHUNK_SIZE]; + strm.next_out = (unsigned char*)out; + strm.avail_out = GZIP_CHUNK_SIZE; + status = deflate(&strm, flushOrFinish); + if (status == Z_STREAM_ERROR) { + deflateEnd(&strm); + return false; + } + int available = (GZIP_CHUNK_SIZE - strm.avail_out); + if (available > 0) { + destination.append((char*)out, available); + } + if (strm.avail_out != 0) { + break; + } + } + + if (flushOrFinish == Z_FINISH) { + break; + } + } + + deflateEnd(&strm); + return status == Z_STREAM_END; +} diff --git a/libraries/shared/src/Gzip.h b/libraries/shared/src/Gzip.h new file mode 100644 index 0000000000..5ebbf97589 --- /dev/null +++ b/libraries/shared/src/Gzip.h @@ -0,0 +1,20 @@ +// +// Gzip.h +// libraries/shared/src +// +// Created by Seth Alves on 2015-08-03. +// 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 GZIP_H +#define GZIP_H + +#include + +bool gzip(QByteArray source, QByteArray &destination, int compressionLevel = -1); +bool gunzip(QByteArray source, QByteArray &destination); + +#endif From 7d9cf91bffa9f070522c61bedd290ff309b57b1b Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 4 Aug 2015 07:26:38 -0700 Subject: [PATCH 234/242] undo mistaken commit --- interface/CMakeLists.txt | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 11af6e70a0..f1ef38ade9 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -13,7 +13,6 @@ endforeach() find_package(Qt5LinguistTools REQUIRED) find_package(Qt5LinguistToolsMacros) -find_package(ZLIB REQUIRED) if (DEFINED ENV{JOB_ID}) set(BUILD_SEQ $ENV{JOB_ID}) @@ -24,8 +23,8 @@ else () endif () if (WIN32) - add_definitions(-D_USE_MATH_DEFINES) # apparently needed to get M_PI and other defines from cmath/math.h - add_definitions(-DWINDOWS_LEAN_AND_MEAN) # needed to make sure windows doesn't go to crazy with its defines + add_definitions(-D_USE_MATH_DEFINES) # apparently needed to get M_PI and other defines from cmath/math.h + add_definitions(-DWINDOWS_LEAN_AND_MEAN) # needed to make sure windows doesn't go to crazy with its defines endif() configure_file(InterfaceVersion.h.in "${PROJECT_BINARY_DIR}/includes/InterfaceVersion.h") @@ -83,19 +82,19 @@ if (APPLE) set(MACOSX_BUNDLE_BUNDLE_NAME Interface) set(MACOSX_BUNDLE_GUI_IDENTIFIER io.highfidelity.Interface) - + if (UPPER_CMAKE_BUILD_TYPE MATCHES RELEASE OR UPPER_CMAKE_BUILD_TYPE MATCHES RELWITHDEBINFO) set(ICON_FILENAME "interface.icns") else () set(ICON_FILENAME "interface-beta.icns") endif () - + # set how the icon shows up in the Info.plist file SET(MACOSX_BUNDLE_ICON_FILE "${ICON_FILENAME}") # set where in the bundle to put the resources file SET_SOURCE_FILES_PROPERTIES(${CMAKE_CURRENT_SOURCE_DIR}/icon/${ICON_FILENAME} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) - + set(DISCOVERED_RESOURCES "") # use the add_resources_to_os_x_bundle macro to recurse into resources @@ -103,7 +102,7 @@ if (APPLE) # append the discovered resources to our list of interface sources list(APPEND INTERFACE_SRCS ${DISCOVERED_RESOURCES}) - + set(INTERFACE_SRCS ${INTERFACE_SRCS} "${CMAKE_CURRENT_SOURCE_DIR}/icon/${ICON_FILENAME}") endif() @@ -146,34 +145,34 @@ add_dependency_external_projects(sdl2) # perform standard include and linking for found externals foreach(EXTERNAL ${OPTIONAL_EXTERNALS}) - + if (${${EXTERNAL}_UPPERCASE}_REQUIRED) find_package(${EXTERNAL} REQUIRED) else () find_package(${EXTERNAL}) endif () - + if (${${EXTERNAL}_UPPERCASE}_FOUND AND NOT DISABLE_${${EXTERNAL}_UPPERCASE}) add_definitions(-DHAVE_${${EXTERNAL}_UPPERCASE}) - + # include the library directories (ignoring warnings) if (NOT ${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS) set(${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIR}) endif () - + include_directories(SYSTEM ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS}) - + # perform the system include hack for OS X to ignore warnings if (APPLE) foreach(EXTERNAL_INCLUDE_DIR ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS}) SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -isystem ${EXTERNAL_INCLUDE_DIR}") endforeach() endif () - + if (NOT ${${EXTERNAL}_UPPERCASE}_LIBRARIES) set(${${EXTERNAL}_UPPERCASE}_LIBRARIES ${${${EXTERNAL}_UPPERCASE}_LIBRARY}) endif () - + if (NOT APPLE OR NOT ${${EXTERNAL}_UPPERCASE} MATCHES "SIXENSE") target_link_libraries(${TARGET_NAME} ${${${EXTERNAL}_UPPERCASE}_LIBRARIES}) elseif (APPLE AND NOT INSTALLER_BUILD) @@ -200,12 +199,13 @@ target_link_libraries( # assume we are using a Qt build without bearer management add_definitions(-DQT_NO_BEARERMANAGEMENT) - if (APPLE) # link in required OS X frameworks and include the right GL headers find_library(OpenGL OpenGL) find_library(AppKit AppKit) + target_link_libraries(${TARGET_NAME} ${OpenGL} ${AppKit}) + # install command for OS X bundle INSTALL(TARGETS ${TARGET_NAME} BUNDLE DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/install" COMPONENT Runtime @@ -224,6 +224,7 @@ else (APPLE) # target_link_libraries(${TARGET_NAME} wsock32.lib Winmm.lib) target_link_libraries(${TARGET_NAME} wsock32.lib Winmm.lib) else (WIN32) + # Nothing else required on linux apparently endif() endif (APPLE) From 5369f4c5ebe223ce2bab58dab9adaefb1d95b9f4 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 4 Aug 2015 08:25:33 -0700 Subject: [PATCH 235/242] adjust comments --- libraries/octree/src/Octree.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index 25970d66d5..256d0c9e4d 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -1891,8 +1891,7 @@ bool Octree::readFromURL(const QString& urlString) { bool Octree::readFromStream(unsigned long streamLength, QDataStream& inputStream) { - - // decide if this is binary SVO or JSON encoded SVO + // decide if this is binary SVO or JSON-formatted SVO QIODevice *device = inputStream.device(); char firstChar; device->getChar(&firstChar); @@ -2040,7 +2039,7 @@ bool Octree::readSVOFromStream(unsigned long streamLength, QDataStream& inputStr const int READ_JSON_BUFFER_SIZE = 2048; bool Octree::readJSONFromStream(unsigned long streamLength, QDataStream& inputStream) { - // QuaGzipFile doesn't appear to give a useful bytesAvailable() result, so just keep reading until + // if the data is gzipped we may not have a useful bytesAvailable() result, so just keep reading until // we get an eof. Leave streamLength parameter for consistency. QByteArray jsonBuffer; From fb9fa760189ef449b56b0b81dac1028723b21ce9 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 4 Aug 2015 08:26:28 -0700 Subject: [PATCH 236/242] adjust comments --- libraries/shared/src/Gzip.cpp | 2 +- libraries/shared/src/Gzip.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/shared/src/Gzip.cpp b/libraries/shared/src/Gzip.cpp index ae4504cd24..e3e9046090 100644 --- a/libraries/shared/src/Gzip.cpp +++ b/libraries/shared/src/Gzip.cpp @@ -3,7 +3,7 @@ // libraries/shared/src // // Created by Seth Alves on 2015-08-03. -// Copyright 2014 High Fidelity, Inc. +// 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 diff --git a/libraries/shared/src/Gzip.h b/libraries/shared/src/Gzip.h index 5ebbf97589..1cf4928249 100644 --- a/libraries/shared/src/Gzip.h +++ b/libraries/shared/src/Gzip.h @@ -3,7 +3,7 @@ // libraries/shared/src // // Created by Seth Alves on 2015-08-03. -// Copyright 2014 High Fidelity, Inc. +// 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 From ff477b35a82c8ae603d2cfd71fb2ea505159f139 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 4 Aug 2015 11:33:39 -0700 Subject: [PATCH 237/242] fix zlib include directory --- libraries/shared/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/shared/CMakeLists.txt b/libraries/shared/CMakeLists.txt index 298381689d..b9bcd43368 100644 --- a/libraries/shared/CMakeLists.txt +++ b/libraries/shared/CMakeLists.txt @@ -9,4 +9,4 @@ target_link_libraries(${TARGET_NAME} ${ZLIB_LIBRARIES}) add_dependency_external_projects(glm) find_package(GLM REQUIRED) -target_include_directories(${TARGET_NAME} PUBLIC ${GLM_INCLUDE_DIRS}) +target_include_directories(${TARGET_NAME} PUBLIC ${GLM_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS}) From a4bf169adecb60e8a64e440c2ec7523058880dca Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 4 Aug 2015 14:13:41 -0700 Subject: [PATCH 238/242] on windows, copy zlib dll next to programs that link with shared --- libraries/shared/CMakeLists.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/libraries/shared/CMakeLists.txt b/libraries/shared/CMakeLists.txt index b9bcd43368..72127cec65 100644 --- a/libraries/shared/CMakeLists.txt +++ b/libraries/shared/CMakeLists.txt @@ -10,3 +10,12 @@ target_link_libraries(${TARGET_NAME} ${ZLIB_LIBRARIES}) add_dependency_external_projects(glm) find_package(GLM REQUIRED) target_include_directories(${TARGET_NAME} PUBLIC ${GLM_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS}) + +if (WIN32) + # Birarda will fix this when he finds it. + get_filename_component(ZLIB_LIB_DIR "${ZLIB_LIBRARIES}" DIRECTORY) + get_filename_component(ZLIB_DIR "${ZLIB_LIB_DIR}" DIRECTORY) + set(ZLIB_BIN_DIR "${ZLIB_DIR}/bin") + file(GLOB ZLIB_DLL_PATHS "${ZLIB_BIN_DIR}/*dll*") + add_paths_to_fixup_libs(${ZLIB_DLL_PATHS}) +endif () From 45643a3aaddba3aefcb1277304a6e34229e082af Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 4 Aug 2015 14:36:16 -0700 Subject: [PATCH 239/242] try, try again --- libraries/shared/CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/shared/CMakeLists.txt b/libraries/shared/CMakeLists.txt index 72127cec65..00a80619bc 100644 --- a/libraries/shared/CMakeLists.txt +++ b/libraries/shared/CMakeLists.txt @@ -16,6 +16,5 @@ if (WIN32) get_filename_component(ZLIB_LIB_DIR "${ZLIB_LIBRARIES}" DIRECTORY) get_filename_component(ZLIB_DIR "${ZLIB_LIB_DIR}" DIRECTORY) set(ZLIB_BIN_DIR "${ZLIB_DIR}/bin") - file(GLOB ZLIB_DLL_PATHS "${ZLIB_BIN_DIR}/*dll*") - add_paths_to_fixup_libs(${ZLIB_DLL_PATHS}) + add_paths_to_fixup_libs(${ZLIB_BIN_DIR}) endif () From 52be44d6215693c88b4737cdc9de2067709e246d Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 4 Aug 2015 14:50:38 -0700 Subject: [PATCH 240/242] uninitialized value --- interface/src/scripting/ControllerScriptingInterface.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interface/src/scripting/ControllerScriptingInterface.cpp b/interface/src/scripting/ControllerScriptingInterface.cpp index 7a3f4a99b5..feceecc3fd 100644 --- a/interface/src/scripting/ControllerScriptingInterface.cpp +++ b/interface/src/scripting/ControllerScriptingInterface.cpp @@ -24,7 +24,8 @@ ControllerScriptingInterface::ControllerScriptingInterface() : _mouseCaptured(false), _touchCaptured(false), - _wheelCaptured(false) + _wheelCaptured(false), + _actionsCaptured(false) { } From f1a4e185cdaec86f3d51159796eb320c3fa2bd1f Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Wed, 5 Aug 2015 10:04:59 -0700 Subject: [PATCH 241/242] Added lifetime and better notes, per PR --- examples/gridTest.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/examples/gridTest.js b/examples/gridTest.js index a29f2be7bc..2baa4650e3 100644 --- a/examples/gridTest.js +++ b/examples/gridTest.js @@ -7,17 +7,22 @@ // // Creates a rectangular grid of objects, starting at the origin and proceeding along the X/Z plane. // Useful for testing the rendering, LOD, and octree storage aspects of the system. -// +// +// Note that when creating things quickly, the entity server will ignore data if we send updates too quickly. +// like Internet MTU, these rates are set by th domain operator, so in this script there is a RATE_PER_SECOND +// variable letting you set this speed. If entities are missing from the grid after a relog, this number +// being too high may be the reason. var SIZE = 10.0; var SEPARATION = 20.0; var ROWS_X = 30; var ROWS_Z = 30; -var TYPE = "Sphere"; // Right now this can be "Box" or "Model" or "Sphere" +var TYPE = "Sphere"; // Right now this can be "Box" or "Model" or "Sphere" var MODEL_URL = "https://hifi-public.s3.amazonaws.com/models/props/LowPolyIsland/CypressTreeGroup.fbx"; var MODEL_DIMENSION = { x: 33, y: 16, z: 49 }; -var RATE_PER_SECOND = 1000; -var SCRIPT_INTERVAL = 100; +var RATE_PER_SECOND = 1000; // The entity server will drop data if we create things too fast. +var SCRIPT_INTERVAL = 100; +var LIFETIME = 600; // By default, these entities will live in the server for 10 minutes var addRandom = false; @@ -41,7 +46,8 @@ Script.setInterval(function () { position: position, dimensions: MODEL_DIMENSION, ignoreCollisions: true, - collisionsWillMove: false + collisionsWillMove: false, + lifetime: LIFETIME }); } else { Entities.addEntity({ @@ -51,7 +57,8 @@ Script.setInterval(function () { dimensions: { x: SIZE, y: SIZE, z: SIZE }, color: { red: x / ROWS_X * 255, green: 50, blue: z / ROWS_Z * 255 }, ignoreCollisions: true, - collisionsWillMove: false + collisionsWillMove: false, + lifetime: LIFETIME }); } From eb870234160c9abfb72af4011ce369ed0a244855 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 5 Aug 2015 10:16:45 -0700 Subject: [PATCH 242/242] code review --- libraries/shared/src/Gzip.cpp | 5 +++-- libraries/shared/src/Gzip.h | 9 ++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/libraries/shared/src/Gzip.cpp b/libraries/shared/src/Gzip.cpp index e3e9046090..a77f459fc9 100644 --- a/libraries/shared/src/Gzip.cpp +++ b/libraries/shared/src/Gzip.cpp @@ -14,6 +14,7 @@ const int GZIP_WINDOWS_BIT = 31; const int GZIP_CHUNK_SIZE = 4096; +const int DEFAULT_MEM_LEVEL = 8; bool gunzip(QByteArray source, QByteArray &destination) { destination.clear(); @@ -100,10 +101,10 @@ bool gzip(QByteArray source, QByteArray &destination, int compressionLevel) { strm.avail_in = 0; int status = deflateInit2(&strm, - qMax(-1, qMin(9, compressionLevel)), + qMax(Z_DEFAULT_COMPRESSION, qMin(9, compressionLevel)), Z_DEFLATED, GZIP_WINDOWS_BIT, - 8, + DEFAULT_MEM_LEVEL, Z_DEFAULT_STRATEGY); if (status != Z_OK) { return false; diff --git a/libraries/shared/src/Gzip.h b/libraries/shared/src/Gzip.h index 1cf4928249..e3ed6bbbea 100644 --- a/libraries/shared/src/Gzip.h +++ b/libraries/shared/src/Gzip.h @@ -14,7 +14,14 @@ #include -bool gzip(QByteArray source, QByteArray &destination, int compressionLevel = -1); +// The compression level must be Z_DEFAULT_COMPRESSION (-1), or between 0 and +// 9: 1 gives best speed, 9 gives best compression, 0 gives no +// compression at all (the input data is simply copied a block at a +// time). Z_DEFAULT_COMPRESSION requests a default compromise between +// speed and compression (currently equivalent to level 6). + +bool gzip(QByteArray source, QByteArray &destination, int compressionLevel = -1); // -1 is Z_DEFAULT_COMPRESSION + bool gunzip(QByteArray source, QByteArray &destination); #endif