Skip to content

Commit 257a683

Browse files
committed
Added speakTo feature #125
1 parent ff6e369 commit 257a683

2 files changed

Lines changed: 114 additions & 8 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,3 +906,14 @@ headAvatarOnly.armature.position.set(1,0,0);
906906
headStandalone.scene.add( headAvatarOnly.armature );
907907
```
908908
909+
If you want one TalkingHead avatar to speak to another, set its
910+
`speakTo` property. The value can be another TalkingHead
911+
instance, any 3D object, or a world position. For example, to make
912+
two avatars talk to each other:
913+
914+
```javascript
915+
headAvatarOnly.speakTo = headStandalone;
916+
headStandalone.speakTo = headAvatarOnly;
917+
```
918+
919+
If you want the avatar to address the user again, set `speakTo` value to `null`.

modules/talkinghead.mjs

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3816,16 +3816,111 @@ class TalkingHead {
38163816
*/
38173817
lookAtCamera(t) {
38183818

3819-
if ( this.avatar.hasOwnProperty('avatarIgnoreCamera') ) {
3820-
if ( this.avatar.avatarIgnoreCamera ) {
3819+
// Calculate the target
3820+
let target;
3821+
if ( this.speakTo ) {
3822+
target = new THREE.Vector3();
3823+
if ( this.speakTo.objectLeftEye?.isObject3D ) {
3824+
3825+
// Target eyes
3826+
const o = this.speakTo.armature.objectHead;
3827+
this.speakTo.objectLeftEye.updateMatrixWorld(true);
3828+
this.speakTo.objectRightEye.updateMatrixWorld(true);
3829+
v.setFromMatrixPosition(this.speakTo.objectLeftEye.matrixWorld);
3830+
w.setFromMatrixPosition(this.speakTo.objectRightEye.matrixWorld);
3831+
target.addVectors(v,w).divideScalar( 2 );
3832+
3833+
} else if ( this.speakTo.isObject3D ) {
3834+
this.speakTo.getWorldPosition(target);
3835+
} else if ( this.speakTo.isVector3 ) {
3836+
target.set( this.speakTo );
3837+
} else if ( this.speakTo.x && this.speakTo.y && this.speakTo.z ) {
3838+
target.set( this.speakTo.x, this.speakTo.y, this.speakTo.z );
3839+
}
3840+
}
3841+
3842+
// If we don't have a target, look ahead or to the screen
3843+
if ( !target ) {
3844+
if ( this.avatar.hasOwnProperty('avatarIgnoreCamera') ) {
3845+
if ( this.avatar.avatarIgnoreCamera ) {
3846+
this.lookAhead(t);
3847+
return;
3848+
}
3849+
} else if ( this.opt.avatarIgnoreCamera ) {
38213850
this.lookAhead(t);
3822-
} else {
3823-
this.lookAt( null, null, t );
3851+
return;
38243852
}
3825-
} else if ( this.opt.avatarIgnoreCamera ) {
3826-
this.lookAhead(t);
3827-
} else {
3828-
this.lookAt( null, null, t );
3853+
this.lookAt(null,null,t);
3854+
return;
3855+
}
3856+
3857+
// TODO: Improve the logic, if possible
3858+
3859+
// Eyes position and head world rotation
3860+
this.objectLeftEye.updateMatrixWorld(true);
3861+
this.objectRightEye.updateMatrixWorld(true);
3862+
v.setFromMatrixPosition(this.objectLeftEye.matrixWorld);
3863+
w.setFromMatrixPosition(this.objectRightEye.matrixWorld);
3864+
v.add(w).divideScalar( 2 );
3865+
q.copy( this.armature.quaternion );
3866+
q.multiply( this.poseTarget.props['Hips.quaternion'] );
3867+
q.multiply( this.poseTarget.props['Spine.quaternion'] );
3868+
q.multiply( this.poseTarget.props['Spine1.quaternion'] );
3869+
q.multiply( this.poseTarget.props['Spine2.quaternion'] );
3870+
q.multiply( this.poseTarget.props['Neck.quaternion'] );
3871+
q.multiply( this.poseTarget.props['Head.quaternion'] );
3872+
3873+
// Direction from object to speakto target
3874+
const dir = new THREE.Vector3().subVectors(target, v).normalize();
3875+
3876+
// Remove roll: compute yaw + pitch only
3877+
const yaw = Math.atan2(dir.x, dir.z); // rotation around Y
3878+
const pitch = Math.asin(-dir.y); // rotation around X
3879+
const roll = 0; // force to 0
3880+
3881+
// Desired rotation with Z locked
3882+
e.set(pitch, yaw, roll, 'YXZ');
3883+
const desiredQ = new THREE.Quaternion().setFromEuler(e);
3884+
3885+
// Rotation difference
3886+
const deltaQ = new THREE.Quaternion().copy(desiredQ).multiply(q.clone().invert());
3887+
3888+
// Convert to Euler (Z will be ~0 by construction)
3889+
e.setFromQuaternion(deltaQ, 'YXZ');
3890+
let rx = e.x / (40/24) + 0.2; // Refer to setValue(bodyRotateX)
3891+
let ry = e.y / (9/4); // Refer to setValue(bodyRotateY)
3892+
let rotx = Math.min(0.6,Math.max(-0.3,rx));
3893+
let roty = Math.min(0.8,Math.max(-0.8,ry));
3894+
3895+
// Randomize head/eyes ratio
3896+
let drotx = (Math.random() - 0.5) / 4;
3897+
let droty = (Math.random() - 0.5) / 4;
3898+
3899+
if ( t ) {
3900+
3901+
// Remove old, if any
3902+
let old = this.animQueue.findIndex( y => y.template.name === 'lookat' );
3903+
if ( old !== -1 ) {
3904+
this.animQueue.splice(old, 1);
3905+
}
3906+
3907+
// Add new anim
3908+
const templateLookAt = {
3909+
name: 'lookat',
3910+
dt: [750,t],
3911+
vs: {
3912+
bodyRotateX: [ rotx + drotx ],
3913+
bodyRotateY: [ roty + droty ],
3914+
eyesRotateX: [ - 3 * drotx + 0.1 ],
3915+
eyesRotateY: [ - 5 * droty ],
3916+
browInnerUp: [[0,0.7]],
3917+
mouthLeft: [[0,0.7]],
3918+
mouthRight: [[0,0.7]],
3919+
eyeContact: [0],
3920+
headMove: [0]
3921+
}
3922+
};
3923+
this.animQueue.push( this.animFactory( templateLookAt ) );
38293924
}
38303925

38313926
}

0 commit comments

Comments
 (0)