diff --git a/src/assets/avatars/Bot_SkinnedWithAnim.glb b/src/assets/avatars/Bot_SkinnedWithAnim.glb new file mode 100644 index 0000000000000000000000000000000000000000..192bbed19788cb4c5cb71a3f23da428173766948 Binary files /dev/null and b/src/assets/avatars/Bot_SkinnedWithAnim.glb differ diff --git a/src/components/body-controller.js b/src/components/body-controller.js deleted file mode 100644 index 05afec55951aff8ba8108bd6ee95e8212d9c8b95..0000000000000000000000000000000000000000 --- a/src/components/body-controller.js +++ /dev/null @@ -1,49 +0,0 @@ -AFRAME.registerComponent("body-controller", { - schema: { - camera: { type: "selector", default: "[camera]" }, - rotationSpeed: { default: 0.07 }, - eyeNeckOffset: { type: "vec3" }, - neckHeight: { type: "number" } - }, - - init() { - this.targetAngleEuler = new THREE.Euler(); - this.targetAngle = new THREE.Quaternion(); - this.cameraPositionMatrix = new THREE.Matrix4(); - const offset = this.data.eyeNeckOffset; - this.eyeNeckTransformMatrix = new THREE.Matrix4().makeTranslation( - offset.x, - offset.y, - offset.z - ); - this.neckTransformMatrix = new THREE.Matrix4().makeTranslation( - 0, - this.data.neckHeight, - 0 - ); - this.bodyPositionMatrix = new THREE.Matrix4(); - this.bodyPositionVector = new THREE.Vector3(); - }, - - tick(time, dt) { - const object3D = this.el.object3D; - const cameraObject3D = this.data.camera.object3D; - - // Set Rotation - this.targetAngleEuler.set(0, cameraObject3D.rotation.y, 0); - this.targetAngle.setFromEuler(this.targetAngleEuler); - object3D.quaternion.slerp(this.targetAngle, this.data.rotationSpeed); - const object3DRotation = object3D.rotation; - this.el.setAttribute("rotation", { - x: object3DRotation.x * THREE.Math.RAD2DEG, - y: object3DRotation.y * THREE.Math.RAD2DEG, - z: object3DRotation.z * THREE.Math.RAD2DEG - }); - - //Set Position - this.bodyPositionMatrix.copy(cameraObject3D.matrix); - this.bodyPositionMatrix.multiply(this.eyeNeckTransformMatrix); - this.bodyPositionVector.setFromMatrixPosition(this.bodyPositionMatrix); - this.el.setAttribute("position", this.bodyPositionVector); - } -}); diff --git a/src/components/bone-visibility.js b/src/components/bone-visibility.js index 17933edcd6ef9d218fd7f048322cb356fbf22d52..5c2e2f2f235f3665f0e9366c3af31be4de5a3d13 100644 --- a/src/components/bone-visibility.js +++ b/src/components/bone-visibility.js @@ -1,13 +1,17 @@ AFRAME.registerComponent("bone-visibility", { + schema: { + type: "selector" + }, tick() { - const visible = this.el.getAttribute("visible"); + const targetEl = this.data; + const visible = this.getAttribute("visible"); if (this.lastVisible !== visible) { if (visible) { - this.el.object3D.scale.set(1, 1, 1); + this.targetEl.object3D.scale.set(1, 1, 1); } else { // Three.js doesn't like updating matrices with 0 scale, so we set it to a near zero number. - this.el.object3D.scale.set(0.00000001, 0.00000001, 0.00000001); + this.targetEl.object3D.scale.set(0.00000001, 0.00000001, 0.00000001); } this.lastVisible = visible; diff --git a/src/components/ik-controller.js b/src/components/ik-controller.js new file mode 100644 index 0000000000000000000000000000000000000000..58627a064b8cd03a73abc0cc22d3e66e3f98916b --- /dev/null +++ b/src/components/ik-controller.js @@ -0,0 +1,193 @@ +const { Vector3, Quaternion, Matrix4, Euler } = THREE; +const RAD2DEG = THREE.Math.RAD2DEG; + +const degRotation = { x: 0, y: 0, z: 0 }; +function setEntityFromMatrix(entity, matrix) { + const object3D = entity.object3D; + object3D.position.setFromMatrixPosition(matrix); + object3D.rotation.setFromRotationMatrix(matrix); + entity.setAttribute("position", object3D.position); + const { x, y, z } = object3D.rotation; + degRotation.x = x * RAD2DEG; + degRotation.y = y * RAD2DEG; + degRotation.z = z * RAD2DEG; + entity.setAttribute("rotation", degRotation); +} + +function updateEntityPosition(entity) { + const object3D = entity.object3D; + entity.setAttribute("position", object3D.position); +} + +function updateEntityRotation(entity) { + const object3D = entity.object3D; + const { x, y, z } = object3D.rotation; + degRotation.x = x * RAD2DEG; + degRotation.y = y * RAD2DEG; + degRotation.z = z * RAD2DEG; + entity.setAttribute("rotation", degRotation); +} + +AFRAME.registerComponent("ik-root", { + schema: { + camera: { type: "string", default: ".camera" }, + leftController: { type: "string", default: ".left-controller" }, + rightController: { type: "string", default: ".right-controller" } + }, + update(oldData) { + let updated = false; + + if (this.data.camera !== oldData.camera) { + this.camera = this.el.querySelector(this.data.camera); + updated = true; + } + + if (this.data.leftController !== oldData.leftController) { + this.leftController = this.el.querySelector(this.data.leftController); + updated = true; + } + + if (this.data.rightController !== oldData.rightController) { + this.rightController = this.el.querySelector(this.data.rightController); + updated = true; + } + + if (updated) { + this.el.querySelector("[ik-controller]").components["ik-controller"].updateIkRoot(this); + } + } +}); + +AFRAME.registerComponent("ik-controller", { + schema: { + leftEye: { type: "string", default: ".LeftEye" }, + rightEye: { type: "string", default: ".RightEye" }, + middleEye: { type: "string", default: ".middle-eye" }, + head: { type: "string", default: ".Head" }, + neck: { type: "string", default: ".Neck" }, + leftHand: { type: "string", default: ".LeftHand" }, + rightHand: { type: "string", default: ".RightHand" }, + chest: { type: "string", default: ".Chest" }, + hips: { type: "string", default: ".Hips" }, + rotationSpeed: { default: 5 }, + debug: { type: "boolean", default: true } + }, + + init() { + this.flipY = new Matrix4().makeRotationY(Math.PI); + + this.cameraForward = new Matrix4(); + this.headTransform = new Matrix4(); + this.hipsPosition = new Vector3(); + + this.curTransform = new Matrix4(); + + this.iHipsToHeadVector = new Vector3(); + + this.iMiddleEyeToHead = new Matrix4(); + this.iHeadToHip = new Matrix4(); + + this.cameraYRotation = new Euler(); + this.cameraYQuaternion = new Quaternion(); + + this.hipsQuaternion = new Quaternion(); + this.headQuaternion = new Quaternion(); + }, + + update(oldData) { + if (this.data.leftEye !== oldData.leftEye) { + this.leftEye = this.el.querySelector(this.data.leftEye); + } + + if (this.data.rightEye !== oldData.rightEye) { + this.rightEye = this.el.querySelector(this.data.rightEye); + } + + if (this.data.middleEye !== oldData.middleEye) { + this.middleEye = this.el.querySelector(this.data.middleEye); + } + + if (this.data.head !== oldData.head) { + this.head = this.el.querySelector(this.data.head); + } + + if (this.data.neck !== oldData.neck) { + this.neck = this.el.querySelector(this.data.neck); + } + + if (this.data.leftHand !== oldData.leftHand) { + this.leftHand = this.el.querySelector(this.data.leftHand); + } + + if (this.data.rightHand !== oldData.rightHand) { + this.rightHand = this.el.querySelector(this.data.rightHand); + } + + if (this.data.chest !== oldData.chest) { + this.chest = this.el.querySelector(this.data.chest); + } + + if (this.data.hips !== oldData.hips) { + this.hips = this.el.querySelector(this.data.hips); + } + + // Set middleEye's position to be right in the middle of the left and right eyes. + const middleEyePosition = this.middleEye.object3D.position; + middleEyePosition.addVectors(this.leftEye.object3D.position, this.rightEye.object3D.position); + middleEyePosition.divideScalar(2); + this.middleEye.object3D.updateMatrix(); + this.iMiddleEyeToHead.getInverse(this.middleEye.object3D.matrix); + + this.iHipsToHeadVector + .addVectors(this.chest.object3D.position, this.neck.object3D.position) + .add(this.head.object3D.position) + .negate(); + }, + + updateIkRoot(ikRoot) { + this.ikRoot = ikRoot; + }, + + tick(time, dt) { + if (!this.ikRoot) { + return; + } + + const { camera, leftController, rightController } = this.ikRoot; + const { + hips, + head, + cameraForward, + headTransform, + iMiddleEyeToHead, + iHipsToHeadVector, + flipY, + hipsPosition, + cameraYRotation, + cameraYQuaternion, + hipsQuaternion, + headQuaternion + } = this; + + // Camera faces the -Z direction. Flip it along the Y axis so that it is +Z. + camera.object3D.updateMatrix(); + cameraForward.multiplyMatrices(camera.object3D.matrix, flipY); + + headTransform.multiplyMatrices(cameraForward, iMiddleEyeToHead); + hipsPosition.setFromMatrixPosition(headTransform).add(iHipsToHeadVector); + hips.setAttribute("position", hipsPosition); + + cameraYRotation.setFromRotationMatrix(cameraForward, "YXZ"); + cameraYRotation.x = 0; + cameraYRotation.z = 0; + cameraYQuaternion.setFromEuler(cameraYRotation); + Quaternion.slerp(hips.object3D.quaternion, cameraYQuaternion, hipsQuaternion, this.data.rotationSpeed * dt / 1000); + hips.object3D.quaternion.copy(hipsQuaternion); + updateEntityRotation(hips); + + headQuaternion.setFromRotationMatrix(headTransform).premultiply(hipsQuaternion.inverse()); + + head.object3D.quaternion.copy(headQuaternion); + updateEntityRotation(head); + } +}); diff --git a/src/network-schemas.js b/src/network-schemas.js index e211090073a2ee8201d810a2f9dd673aebd30581..73c00c0310b0e005d28a9ebe933de08e18e7f9a7 100644 --- a/src/network-schemas.js +++ b/src/network-schemas.js @@ -5,35 +5,35 @@ function registerNetworkSchemas() { "position", "rotation", { - selector: ".Head", + selector: ".camera", component: "position" }, { - selector: ".Head", + selector: ".camera", component: "rotation" }, { - selector: ".LeftHand", + selector: ".left-controller", component: "position" }, { - selector: ".LeftHand", + selector: ".left-controller", component: "rotation" }, { - selector: ".LeftHand", + selector: ".left-controller", component: "visible" }, { - selector: ".RightHand", + selector: ".right-controller", component: "position" }, { - selector: ".RightHand", + selector: ".right-controller", component: "rotation" }, { - selector: ".RightHand", + selector: ".right-controller", component: "visible" }, { diff --git a/src/room.js b/src/room.js index 7b789b4b59f253ae7b78ee366ac9d01dff9819d9..d5fc0957706aa7f42d49da3a9c0988c5fe763d90 100644 --- a/src/room.js +++ b/src/room.js @@ -26,7 +26,7 @@ import "./components/nametag-transform"; import "./components/bone-mute-state-indicator"; import "./components/2d-mute-state-indicator"; import "./components/virtual-gamepad-controls"; -import "./components/body-controller"; +import "./components/ik-controller"; import "./components/hand-controls2"; import "./components/character-controller"; import "./components/haptic-feedback"; diff --git a/templates/room.hbs b/templates/room.hbs index 087a81f8ef09841301ee79c34fa64856398de798..8137c23aa705f7858ff21d4e6c272b8e2e9def25 100644 --- a/templates/room.hbs +++ b/templates/room.hbs @@ -44,7 +44,7 @@ light="defaultLightsEnabled: false"> <a-assets> - <a-asset-item id="bot-skinned-mesh" response-type="arraybuffer" src="{{asset "assets/avatars/Bot_Skinned.glb" }}"></a-asset-item> + <a-asset-item id="bot-skinned-mesh" response-type="arraybuffer" src="{{asset "assets/avatars/Bot_SkinnedWithAnim.glb" }}"></a-asset-item> <a-asset-item id="watch-model" response-type="arraybuffer" src="{{asset "assets/hud/watch.glb"}}"></a-asset-item> <a-asset-item id="meeting-space1-mesh" response-type="arraybuffer" src="{{asset "assets/environments/MeetingSpace1_mesh.glb"}}"></a-asset-item> @@ -60,109 +60,99 @@ <a-entity class="video" geometry="primitive: plane;" material="side: double" networked-video-player></a-entity> </template> - <template id="remote-avatar-template"> - <a-gltf-entity src="#bot-skinned-mesh" inflate="true"> - <template data-selector=".Head"> - <a-entity - networked-audio-source - networked-audio-analyser - matcolor-audio-feedback="objectName: Head_Mesh" - scale-audio-feedback - personal-space-invader - animation-mixer - > + <template id="remote-avatar-template"> + <a-entity> + <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller > + <template data-selector=".Head"> <a-entity - class="nametag" - nametag-transform="follow: .Head" - text="side: double; align: center; color: #ddd" - position="0 2.5 0" - scale="6 6 6" - ></a-entity> - </a-entity> - </template> - - <template data-selector=".Body"> - <a-entity body-controller="eyeNeckOffset: 0 -0.11 0.09; neckHeight: 0.05" ></a-entity> - </template> - - <template selector=".LeftHand"> - <a-entity personal-space-invader bone-visibility ></a-entity> - </template> - - <template data-selector=".RightHand"> - <a-entity personal-space-invader bone-visibility ></a-entity> - </template> - </a-gltf-entity> + networked-audio-source + networked-audio-analyser + matcolor-audio-feedback="objectName: Head_Mesh" + scale-audio-feedback + personal-space-invader + animation-mixer + > + <a-entity class="middle-eye"></a-entity> + <a-entity + class="nametag" + nametag-transform="follow: .Head" + text="side: double; align: center; color: #ddd" + position="0 50 0" + scale="600 600 600" + ></a-entity> + </a-entity> + </template> + + <template selector=".LeftHand"> + <a-entity personal-space-invader ></a-entity> + </template> + + <template data-selector=".RightHand"> + <a-entity personal-space-invader ></a-entity> + </template> + </a-gltf-entity> + </a-entity> </template> + </a-assets> <!-- Player Rig --> - <a-gltf-entity + <a-entity id="player-rig" - src="#bot-skinned-mesh" - inflate="true" networked="template: #remote-avatar-template; attachLocalTemplate: false;" spawn-controller="radius: 4;" wasd-to-analog2d - character-controller="pivot: #head" + character-controller="pivot: #player-camera" + ik-root > - <template data-selector=".Head"> - <a-entity - id="head" - camera="userHeight: 1.6" - camera-scale - personal-space-bubble - look-controls - visible="false" - > - <a-entity - class="nametag" - nametag-transform="follow: .Head" - text="side: double; align: center; color: #ddd" - position="0 2.5 0" - scale="6 6 6" - ></a-entity> - </a-entity> - </template> - - <template data-selector=".Chest"> - <a-entity - id="body" - body-controller="eyeNeckOffset: 0 -0.11 0.09; neckHeight: 0.05" - ></a-entity> - </template> - - <template data-selector=".LeftHand"> - <a-entity - id="left-hand" - hand-controls2="left" - tracked-controls - haptic-feedback - teleport-controls="cameraRig: #player-rig; teleportOrigin: #head; button: action_teleport_" - bone-visibility - > - <!--<a-entity - id="watch" - cached-gltf-model="#watch-model" - bone-mute-state-indicator - position="-0.003 0.009 0.085" - rotation="-79.12547150756669 -160.1417037390651 -100.1530225888679" - scale="1.5 1.5 1.5" - >--> + <a-entity + id="player-camera" + class="camera" + camera="userHeight: 1.6" + personal-space-bubble + look-controls + ></a-entity> + + <a-entity + id="player-left-controller" + class="left-controller" + hand-controls2="left" + tracked-controls + teleport-controls="cameraRig: #player-rig; teleportOrigin: #player-camera; button: action_teleport_" + haptic-feedback + ></a-entity> + + <a-entity + id="player-right-controller" + class="right-controller" + hand-controls2="right" + tracked-controls + teleport-controls="cameraRig: #player-rig; teleportOrigin: #player-camera; button: action_teleport_" + haptic-feedback + ></a-entity> + + <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller> + <template data-selector=".Head"> + <a-entity visible="false"> + <a-entity class="nametag" text ></a-entity> + <a-entity class="middle-eye"></a-entity> </a-entity> - </a-entity> - </template> + </template> - <template data-selector=".RightHand"> - <a-entity - id="right-hand" - hand-controls2="right" - haptic-feedback - teleport-controls="cameraRig: #player-rig; teleportOrigin: #head; button: action_teleport_;" - bone-visibility - ></a-entity> - </template> - </a-gltf-entity> + <template data-selector=".LeftHand"> + <a-entity> + <a-entity + id="watch" + cached-gltf-model="#watch-model" + bone-mute-state-indicator + position="-0.003 0.009 0.085" + rotation="-79.12547150756669 -160.1417037390651 -100.1530225888679" + scale="1.5 1.5 1.5" + ></a-entity> + </a-entity> + </template> + </a-gltf-entity> + </a-entity> <!-- Environment --> <a-entity