function retargetClip( target, source, clip, options = {} ) {

	options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false;

	// Calculate the fps from the source clip based on the track with the most frames, unless fps is already provided.
	options.fps = options.fps !== undefined ? options.fps : ( Math.max( ...clip.tracks.map( track => track.times.length ) ) / clip.duration );
	options.names = options.names || [];

	if ( ! source.isObject3D ) {

		source = getHelperFromSkeleton( source );

	}

	const numFrames = Math.round( clip.duration * ( options.fps / 1000 ) * 1000 ),
		delta = clip.duration / ( numFrames - 1 ),
		convertedTracks = [],
		mixer = new AnimationMixer( source ),
		bones = getBones( target.skeleton ),
		boneDatas = [];

	let positionOffset,
		bone, boneTo, boneData,
		name;

	mixer.clipAction( clip ).play();

	// trim

	let start = 0, end = numFrames;

	if ( options.trim !== undefined ) {

		start = Math.round( options.trim[ 0 ] * options.fps );
		end = Math.min( Math.round( options.trim[ 1 ] * options.fps ), numFrames ) - start;

		mixer.update( options.trim[ 0 ] );

	} else {

		mixer.update( 0 );

	}

	source.updateMatrixWorld();

	//

	for ( let frame = 0; frame < end; ++ frame ) {

		const time = frame * delta;

		retarget( target, source, options );

		for ( let j = 0; j < bones.length; ++ j ) {

			bone = bones[ j ];
			name = getBoneName( bone, options ) || bone.name;
			boneTo = getBoneByName( name, source.skeleton );

			if ( boneTo ) {

				boneData = boneDatas[ j ] = boneDatas[ j ] || { bone: bone };

				if ( options.hip === name ) {

					if ( ! boneData.pos ) {

						boneData.pos = {
							times: new Float32Array( end ),
							values: new Float32Array( end * 3 )
						};

					}

					if ( options.useFirstFramePosition ) {

						if ( frame === 0 ) {

							positionOffset = bone.position.clone();

						}

						bone.position.sub( positionOffset );

					}

					boneData.pos.times[ frame ] = time;

					bone.position.toArray( boneData.pos.values, frame * 3 );

				}

				if ( ! boneData.quat ) {

					boneData.quat = {
						times: new Float32Array( end ),
						values: new Float32Array( end * 4 )
					};

				}

				boneData.quat.times[ frame ] = time;

				bone.quaternion.toArray( boneData.quat.values, frame * 4 );

			}

		}

		if ( frame === end - 2 ) {

			// last mixer update before final loop iteration
			// make sure we do not go over or equal to clip duration
			mixer.update( delta - 0.0000001 );

		} else {

			mixer.update( delta );

		}

		source.updateMatrixWorld();

	}

	for ( let i = 0; i < boneDatas.length; ++ i ) {

		boneData = boneDatas[ i ];

		if ( boneData ) {

			if ( boneData.pos ) {

				convertedTracks.push( new VectorKeyframeTrack(
					'.bones[' + boneData.bone.name + '].position',
					boneData.pos.times,
					boneData.pos.values
				) );

			}

			convertedTracks.push( new QuaternionKeyframeTrack(
				'.bones[' + boneData.bone.name + '].quaternion',
				boneData.quat.times,
				boneData.quat.values
			) );

		}

	}

	mixer.uncacheAction( clip );

	return new AnimationClip( clip.name, - 1, convertedTracks );

}
Retargets the animation clip of the source object to the
target 3D object.

@param {Object3D} target - The target 3D object.
@param {Object3D} source - The source 3D object.
@param {AnimationClip} clip - The animation clip.
@param {module:SkeletonUtils~RetargetOptions} options - The options.
@return {AnimationClip} The retargeted animation clip.