diff --git a/applications/flow/flow-a.svg b/applications/flow/flow-a.svg new file mode 100644 index 0000000..e304f36 --- /dev/null +++ b/applications/flow/flow-a.svg @@ -0,0 +1,55 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/applications/flow/flow-i.svg b/applications/flow/flow-i.svg new file mode 100644 index 0000000..cbb2fe4 --- /dev/null +++ b/applications/flow/flow-i.svg @@ -0,0 +1,58 @@ + + + +image/svg+xml + + + \ No newline at end of file diff --git a/applications/flow/flowAppCpp.html b/applications/flow/flowAppCpp.html new file mode 100644 index 0000000..27781e9 --- /dev/null +++ b/applications/flow/flowAppCpp.html @@ -0,0 +1,393 @@ + + + + + + + + + + + + +
+ +
+
+ +
+
+ +
+
+ +
+ + + diff --git a/applications/flow/flowAppCpp.js b/applications/flow/flowAppCpp.js new file mode 100644 index 0000000..bfca970 --- /dev/null +++ b/applications/flow/flowAppCpp.js @@ -0,0 +1,495 @@ +// +// Created by Luis Cuenca on 3/8/19 +// Copyright 2018 High Fidelity, Inc. +// Copyright 2023 Overte e.V. +// +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +/* jslint bitwise: true */ +/* global Script, MyAvatar, Tablet +*/ + +(function () { + // Adding some additional time before load fix an issue when updating the debugging overlays (local entities) + var MS_AFTER_LOADING = 500; + Script.setTimeout(function() { + Script.registerValue("FLOWAPP", true); + + var TABLET_BUTTON_NAME = "FLOW"; + var HTML_URL = Script.resolvePath("./flowAppCpp.html"); + + var SHOW_AVATAR = true; + var SHOW_DEBUG_SHAPES = false; + var SHOW_SOLID_SHAPES = false; + var POLYLINE_TEXTURE = Script.resolvePath("./polylineTexture.png"); + + var USE_COLLISIONS = false; + var IS_ACTIVE = false; + + var MSG_DOCUMENT_LOADED = 0; + var MSG_JOINT_INPUT_DATA = 1; + var MSG_COLLISION_DATA = 2; + var MSG_COLLISION_INPUT_DATA = 3; + var MSG_DISPLAY_DATA = 4; + var MSG_CREATE = 5; + + var avatarScale = MyAvatar.getSensorToWorldScale(); + + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var tabletButton = tablet.addButton({ + text: TABLET_BUTTON_NAME, + icon: Script.resolvePath("./flow-i.svg"), + activeIcon: Script.resolvePath("./flow-a.svg") + }); + + var FlowDebug = function() { + var self = this; + this.debugLines = {}; + this.debugSpheres = {}; + this.showDebugShapes = false; + this.showSolidShapes = "lines"; // wireframe + + this.setDebugLine = function(lineName, norms, startPosition, endPosition, shapeColor, width, forceRendering) { + var doRender = self.showDebugShapes || forceRendering; + if (!doRender) { + return; + } + var start = startPosition ? startPosition : {x: 0, y: 0, z: 0}; + var end = endPosition ? endPosition : {x: 0, y: 1, z: 0}; + var color = shapeColor ? shapeColor : { red: 0, green: 255, blue: 255 }; + if (self.debugLines[lineName] !== undefined) { + Entities.editEntity(self.debugLines[lineName], { + color: color, + linePoints: [start, end], + strokeWidths: [width, width], + }); + } else { + self.debugLines[lineName] = Entities.addEntity({ + type: "PolyLine", + textures: POLYLINE_TEXTURE, + color: color, + normals: norms, + linePoints: [start, end], + visible: true, + strokeWidths: [width, width], + collisionless: true + }, "local"); + } + }; + + this.setDebugSphere = function(sphereName, pos, diameter, shapeColor, forceRendering) { + var doRender = self.showDebugShapes || forceRendering; + if (!doRender) { + return; + } + var DEFAULT_SPHERE_DIAMETER = 0.02; + var scale = diameter ? diameter : DEFAULT_SPHERE_DIAMETER; + var color = shapeColor ? shapeColor : { red: 255, green: 0, blue: 255 }; + if (self.debugSpheres[sphereName] !== undefined) { + Entities.editEntity(self.debugSpheres[sphereName], { + color: color, + position: pos, + dimensions: {x: scale, y: scale, z: scale}, + primitiveMode: self.showSolidShapes, + }); + } else { + self.debugSpheres[sphereName] = Entities.addEntity({ + type: "Sphere", + color: color, + position: pos, + dimensions: {x: scale, y: scale, z: scale}, + primitiveMode: self.showSolidShapes, + visible: true, + collisionless: true + }, "local"); + } + }; + + this.deleteSphere = function(name) { + Entities.deleteEntity(self.debugSpheres[name]); + self.debugSpheres[name] = undefined; + }; + + this.deleteLine = function(name) { + Entities.deleteEntity(self.debugLines[name]); + self.debugLines[name] = undefined; + }; + + this.cleanup = function() { + for (var lineName in self.debugLines) { + if (lineName !== undefined) { + self.deleteLine(lineName); + } + } + for (var sphereName in self.debugSpheres) { + if (sphereName !== undefined) { + self.deleteSphere(sphereName); + } + } + self.debugLines = {}; + self.debugSpheres = {}; + }; + + this.setVisible = function(isVisible) { + self.showDebugShapes = isVisible; + for (var lineName in self.debugLines) { + if (lineName !== undefined) { + Entities.editEntity(self.debugLines[lineName], { + visible: isVisible + }); + } + } + for (var sphereName in self.debugSpheres) { + if (sphereName !== undefined) { + Entities.editEntity(self.debugSpheres[sphereName], { + visible: isVisible + }); + } + } + }; + + this.setSolid = function(isSolid) { + if(isSolid) { + self.showSolidShapes = "solid" + } else { + self.showSolidShapes = "lines" + } + for (var lineName in self.debugLines) { + if (lineName !== undefined) { + Entities.editEntity(self.debugLines[lineName], { + primitiveMode: self.showSolidShapes + }); + } + } + for (var sphereName in self.debugSpheres) { + if (sphereName !== undefined) { + Entities.editEntity(self.debugSpheres[sphereName], { + primitiveMode: self.showSolidShapes + }); + } + } + }; + + }; + + var flowData, initActive, initColliding, initDebugging; + updateFlowData(true); + var collisionDebug = new FlowDebug(); + var jointDebug = new FlowDebug(); + + collisionDebug.setVisible(SHOW_DEBUG_SHAPES); + collisionDebug.setSolid(SHOW_SOLID_SHAPES); + + MyAvatar.setEnableMeshVisible(SHOW_AVATAR); + jointDebug.setVisible(SHOW_DEBUG_SHAPES); + jointDebug.setSolid(SHOW_SOLID_SHAPES); + + var shown = false; + + function manageClick() { + if (shown) { + MyAvatar.useFlow(initActive, initColliding); + initDebugging = SHOW_DEBUG_SHAPES; + if (SHOW_DEBUG_SHAPES) { + toggleDebugShapes(); + } + tablet.gotoHomeScreen(); + } else { + updateFlowData(); + tablet.gotoWebScreen(HTML_URL); + } + } + + tabletButton.clicked.connect(manageClick); + + function onScreenChanged(type, url) { + console.log("Screen changed"); + if (type === "Web" && url === HTML_URL) { + tabletButton.editProperties({isActive: true}); + if (!shown) { + // hook up to event bridge + tablet.webEventReceived.connect(onWebEventReceived); + } + shown = true; + } else { + tabletButton.editProperties({isActive: false}); + if (shown) { + // disconnect from event bridge + tablet.webEventReceived.disconnect(onWebEventReceived); + } + shown = false; + } + } + + var toggleAvatarVisible = function() { + SHOW_AVATAR = !SHOW_AVATAR; + MyAvatar.setEnableMeshVisible(SHOW_AVATAR); + }; + + var toggleDebugShapes = function() { + SHOW_DEBUG_SHAPES = !SHOW_DEBUG_SHAPES; + if (USE_COLLISIONS) { + collisionDebug.setVisible(SHOW_DEBUG_SHAPES); + } + jointDebug.setVisible(SHOW_DEBUG_SHAPES); + }; + + var toggleSolidShapes = function() { + SHOW_SOLID_SHAPES = !SHOW_SOLID_SHAPES; + collisionDebug.setSolid(SHOW_SOLID_SHAPES); + jointDebug.setSolid(SHOW_SOLID_SHAPES); + }; + + var toggleCollisions = function() { + USE_COLLISIONS = !USE_COLLISIONS; + if (USE_COLLISIONS && SHOW_DEBUG_SHAPES) { + collisionDebug.setVisible(true); + } else { + collisionDebug.setVisible(false); + } + MyAvatar.useFlow(IS_ACTIVE, USE_COLLISIONS); + }; + + var getDisplayData = function() { + return {"avatar": SHOW_AVATAR, + "collisions": USE_COLLISIONS, + "debug": SHOW_DEBUG_SHAPES, + "solid": SHOW_SOLID_SHAPES}; + }; + + var jointNames = MyAvatar.getJointNames(); + + function roundFloat(number, decimals) { + var DECIMALS_MULTIPLIER = 10; + var multiplier = Math.pow(DECIMALS_MULTIPLIER, decimals); + var rounded = Math.round(number * multiplier); + return rounded / multiplier; + } + + function roundVector(vector, decimals) { + return {x: roundFloat(vector.x, decimals), y: roundFloat(vector.y, decimals), z: roundFloat(vector.z, decimals)}; + } + + function roundDataValues(decimals) { + var collisions = flowData.collisions; + var physics = flowData.physics; + Object.keys(collisions).forEach(function(key) { + var data = collisions[key]; + data.radius = roundFloat(data.radius, decimals); + data.offset = roundVector(data.offset, decimals); + }); + Object.keys(physics).forEach(function(key) { + var data = physics[key]; + data.damping = roundFloat(data.damping, decimals); + data.delta = roundFloat(data.delta, decimals); + data.gravity = roundFloat(data.gravity, decimals); + data.inertia = roundFloat(data.inertia, decimals); + data.radius = roundFloat(data.radius, decimals); + data.stiffness = roundFloat(data.stiffness, decimals); + }); + } + + function updateFlowData(catchInitValues) { + flowData = MyAvatar.getFlowData(); + if (typeof(flowData) !== "object" || typeof(flowData.collisions) !== "object") { + return; + } + var ROUND_DECIMALS = 4; + var collisionJoints = Object.keys(flowData.collisions); + var inverseScale = 1.0 / avatarScale; + for (var i = 0; i < collisionJoints.length; i++) { + var collision = flowData.collisions[collisionJoints[i]]; + collision.radius *= inverseScale; + collision.offset = Vec3.multiply(collision.offset, inverseScale); + } + roundDataValues(ROUND_DECIMALS); + IS_ACTIVE = flowData.active; + USE_COLLISIONS = flowData.colliding; + if (catchInitValues) { + initActive = flowData.active; + initColliding = flowData.colliding; + } + } + + function onWebEventReceived(msg) { + var message = JSON.parse(msg); + switch (message.type) { + case MSG_JOINT_INPUT_DATA: { + flowData.physics[message.group][message.name] = message.value; + MyAvatar.useFlow(IS_ACTIVE, USE_COLLISIONS, flowData.physics, flowData.collisions); + break; + } + case MSG_COLLISION_INPUT_DATA: { + var value = message.name === "offset" ? {x: 0.0, y: message.value, z: 0.0} : message.value; + flowData.collisions[message.group][message.name] = value; + MyAvatar.useFlow(IS_ACTIVE, USE_COLLISIONS, flowData.physics, flowData.collisions); + break; + } + case MSG_DISPLAY_DATA: { + switch (message.name) { + case "collisions": + toggleCollisions(); + break; + case "debug": + toggleDebugShapes(); + break; + case "solid": + toggleSolidShapes(); + break; + case "avatar": + toggleAvatarVisible(); + break; + } + break; + } + case MSG_DOCUMENT_LOADED: { + MyAvatar.useFlow(true, true); + updateFlowData(); + if (initDebugging && !SHOW_DEBUG_SHAPES) { + toggleDebugShapes(); + } + createHTMLMenu(); + break; + } + case MSG_COLLISION_DATA: { + switch (message.name) { + case "add": + var collisionData = {"type": "sphere", "radius": 0.05, "offset": {"x": 0.0, "y": 0.0, "z": 0.0}}; + flowData.collisions[message.value] = collisionData; + MyAvatar.useFlow(IS_ACTIVE, USE_COLLISIONS, flowData.physics, flowData.collisions); + updateFlowData(); + tablet.emitScriptEvent(JSON.stringify({ + "type": MSG_COLLISION_DATA, + "name": message.value, + "data": collisionData + })); + break; + case "remove": + var jointName = message.value; + collisionDebug.deleteSphere(jointName + "_col"); + if (flowData.collisions[jointName] !== undefined) { + delete flowData.collisions[jointName]; + MyAvatar.useFlow(IS_ACTIVE, USE_COLLISIONS, flowData.physics, flowData.collisions); + updateFlowData(); + } + break; + } + break; + } + } + } + + tablet.screenChanged.connect(onScreenChanged); + + function createHTMLMenu() { + jointNames = MyAvatar.getJointNames(); + tablet.emitScriptEvent(JSON.stringify( + { + "type": MSG_CREATE, + "data": { + "display": getDisplayData(), + "group": flowData.physics, + "collisions": flowData.collisions, + "joints": jointNames + } + } + )); + } + + function shutdownTabletApp() { + MyAvatar.useFlow(initActive, initColliding); + tablet.removeButton(tabletButton); + if (shown) { + tablet.webEventReceived.disconnect(onWebEventReceived); + tablet.gotoHomeScreen(); + } + tablet.screenChanged.disconnect(onScreenChanged); + } + + MyAvatar.skeletonChanged.connect(function() { + var MS_AFTER_AVATAR_UPDATE = 200; + collisionDebug.cleanup(); + jointDebug.cleanup(); + Script.setTimeout(function() { + jointNames = MyAvatar.getJointNames(); + avatarScale = MyAvatar.getSensorToWorldScale(); + updateFlowData(true); + if (shown) { + manageClick(); + } + }, MS_AFTER_AVATAR_UPDATE); + }); + + MyAvatar.scaleChanged.connect(function() { + avatarScale = MyAvatar.getSensorToWorldScale(); + }); + + Script.update.connect(function() { + if (IS_ACTIVE) { + var groupData = flowData.physics; + var collisionData = flowData.collisions; + var threads = flowData.threads; + var groups = Object.keys(groupData); + var flowPositions = Array(jointNames.length); + var flowCollisionColors = Array(jointNames.length); + var collidingJoints = MyAvatar.getCollidingFlowJoints(); + for (var i = 0; i < groups.length; i++) { + var group = groups[i]; + var data = groupData[group]; + for (var j = 0; j < data.jointIndices.length; j++) { + var index = data.jointIndices[j]; + var name = jointNames[index]; + var position = MyAvatar.getJointPosition(index); + flowPositions[index] = position; + var colliding = collidingJoints.indexOf(index) > -1; + var color = { red: 255, green: 255, blue: 0 }; + if (colliding) { + color.green = 0; + } + flowCollisionColors[index] = color; + var radius = 2.0 * avatarScale * data.radius; + jointDebug.setDebugSphere(name + "_flow", position, radius, color); + } + } + var names = Object.keys(collisionData); + for (i = 0; i < names.length; i++) { + name = names[i]; + index = collisionData[name].jointIndex; + + var offset = Vec3.multiply(collisionData[name].offset, avatarScale); + radius = avatarScale * collisionData[name].radius; + position = MyAvatar.jointToWorldPoint(offset, index); + collisionDebug.setDebugSphere(name + "_col", position, 2 * radius, {red: 200, green: 10, blue: 50}); + } + var threadKeys = Object.keys(threads); + for (i = 0; i < threadKeys.length; i++) { + var thread = threads[threadKeys[i]]; + for (j = 1; j < thread.length; j++) { + var index1 = thread[j-1]; + var index2 = thread[j]; + if (flowPositions[index1] !== undefined && flowPositions[index2] !== undefined) { + var lineName = jointNames[index1] + "_line"; + color = flowCollisionColors[index1]; + var DEFAULT_LINE_WIDTH = 0.004; + var lineWidth = DEFAULT_LINE_WIDTH * avatarScale; + // We are creating two PolyLines with different normals, to make them more visible from the sides. + var LINE_NORMALS_1 = [{ x: 0, y: 0, z: 1 }, { x: 0, y: 0, z: 1 }]; + var LINE_NORMALS_2 = [{ x: 1, y: 0, z: 0 }, { x: 1, y: 0, z: 0 }]; + jointDebug.setDebugLine(lineName + "_1", LINE_NORMALS_1, flowPositions[index1], flowPositions[index2], color, lineWidth); + jointDebug.setDebugLine(lineName + "_2", LINE_NORMALS_2, flowPositions[index1], flowPositions[index2], color, lineWidth); + } + } + } + } + }); + + Script.scriptEnding.connect(function () { + collisionDebug.cleanup(); + jointDebug.cleanup(); + shutdownTabletApp(); + }); + }, MS_AFTER_LOADING); + +}()); diff --git a/applications/flow/polylineTexture.png b/applications/flow/polylineTexture.png new file mode 100644 index 0000000..e7996c0 Binary files /dev/null and b/applications/flow/polylineTexture.png differ diff --git a/applications/metadata.js b/applications/metadata.js index 2398a11..9cf9be5 100644 --- a/applications/metadata.js +++ b/applications/metadata.js @@ -341,6 +341,15 @@ var metadata = { "applications": "jsfile": "hmd3rdPerson/app-hmd3rdPerson.js", "icon": "hmd3rdPerson/icon_inactive_white.png", "caption": "3rd PERS" + }, + { + "isActive": true, + "directory": "flow", + "name": "Flow Bones", + "description": "Effortlessly tweak and tune avatar flow bones.", + "jsfile": "flow/flowAppCpp.js", + "icon": "flow/flow-i.svg", + "caption": "FLOW" } ] }; \ No newline at end of file