You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							275 lines
						
					
					
						
							6.4 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							275 lines
						
					
					
						
							6.4 KiB
						
					
					
				| <!DOCTYPE html> | |
| <html lang="en"> | |
| 	<head> | |
| 		<title>three.js webgl - skinning and morphing</title> | |
| 		<meta charset="utf-8"> | |
| 		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> | |
| 		<link type="text/css" rel="stylesheet" href="main.css"> | |
| 		<style> | |
| 			body { | |
| 				color: #222; | |
| 			} | |
| 
 | |
| 			a { | |
| 				color: #2fa1d6; | |
| 			} | |
| 
 | |
| 			p { | |
| 				max-width: 600px; | |
| 				margin-left: auto; | |
| 				margin-right: auto; | |
| 				padding: 0 2em; | |
| 			} | |
| 		</style> | |
| 	</head> | |
| 
 | |
| 	<body> | |
| 		<div id="info"> | |
| 			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - skinning and morphing<br /> | |
| 			<p> | |
| 				The animation system allows clips to be played individually, looped, or crossfaded with other clips. This example shows a character looping in one of several base animation states, then transitioning smoothly to one-time actions. Facial expressions are controlled independently with morph targets. | |
| 			</p> | |
| 			Model by | |
| 			<a href="https://www.patreon.com/quaternius" target="_blank" rel="noopener">Tomás Laulhé</a>, | |
| 			modifications by <a href="https://donmccurdy.com/" target="_blank" rel="noopener">Don McCurdy</a>. CC0.<br /> | |
| 		</div> | |
| 
 | |
| 		<!-- Import maps polyfill --> | |
| 		<!-- Remove this when import maps will be widely supported --> | |
| 		<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script> | |
| 
 | |
| 		<script type="importmap"> | |
| 			{ | |
| 				"imports": { | |
| 					"three": "../build/three.module.js", | |
| 					"three/addons/": "./jsm/" | |
| 				} | |
| 			} | |
| 		</script> | |
| 
 | |
| 		<script type="module"> | |
| 
 | |
| 			import * as THREE from 'three'; | |
| 
 | |
| 			import Stats from 'three/addons/libs/stats.module.js'; | |
| 			import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; | |
| 
 | |
| 			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; | |
| 
 | |
| 			let container, stats, clock, gui, mixer, actions, activeAction, previousAction; | |
| 			let camera, scene, renderer, model, face; | |
| 
 | |
| 			const api = { state: 'Walking' }; | |
| 
 | |
| 			init(); | |
| 			animate(); | |
| 
 | |
| 			function init() { | |
| 
 | |
| 				container = document.createElement( 'div' ); | |
| 				document.body.appendChild( container ); | |
| 
 | |
| 				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.25, 100 ); | |
| 				camera.position.set( - 5, 3, 10 ); | |
| 				camera.lookAt( 0, 2, 0 ); | |
| 
 | |
| 				scene = new THREE.Scene(); | |
| 				scene.background = new THREE.Color( 0xe0e0e0 ); | |
| 				scene.fog = new THREE.Fog( 0xe0e0e0, 20, 100 ); | |
| 
 | |
| 				clock = new THREE.Clock(); | |
| 
 | |
| 				// lights | |
|  | |
| 				const hemiLight = new THREE.HemisphereLight( 0xffffff, 0x444444 ); | |
| 				hemiLight.position.set( 0, 20, 0 ); | |
| 				scene.add( hemiLight ); | |
| 
 | |
| 				const dirLight = new THREE.DirectionalLight( 0xffffff ); | |
| 				dirLight.position.set( 0, 20, 10 ); | |
| 				scene.add( dirLight ); | |
| 
 | |
| 				// ground | |
|  | |
| 				const mesh = new THREE.Mesh( new THREE.PlaneGeometry( 2000, 2000 ), new THREE.MeshPhongMaterial( { color: 0x999999, depthWrite: false } ) ); | |
| 				mesh.rotation.x = - Math.PI / 2; | |
| 				scene.add( mesh ); | |
| 
 | |
| 				const grid = new THREE.GridHelper( 200, 40, 0x000000, 0x000000 ); | |
| 				grid.material.opacity = 0.2; | |
| 				grid.material.transparent = true; | |
| 				scene.add( grid ); | |
| 
 | |
| 				// model | |
|  | |
| 				const loader = new GLTFLoader(); | |
| 				loader.load( 'models/gltf/RobotExpressive/RobotExpressive.glb', function ( gltf ) { | |
| 
 | |
| 					model = gltf.scene; | |
| 					scene.add( model ); | |
| 
 | |
| 					createGUI( model, gltf.animations ); | |
| 
 | |
| 				}, undefined, function ( e ) { | |
| 
 | |
| 					console.error( e ); | |
| 
 | |
| 				} ); | |
| 
 | |
| 				renderer = new THREE.WebGLRenderer( { antialias: true } ); | |
| 				renderer.setPixelRatio( window.devicePixelRatio ); | |
| 				renderer.setSize( window.innerWidth, window.innerHeight ); | |
| 				renderer.outputEncoding = THREE.sRGBEncoding; | |
| 				container.appendChild( renderer.domElement ); | |
| 
 | |
| 				window.addEventListener( 'resize', onWindowResize ); | |
| 
 | |
| 				// stats | |
| 				stats = new Stats(); | |
| 				container.appendChild( stats.dom ); | |
| 
 | |
| 			} | |
| 
 | |
| 			function createGUI( model, animations ) { | |
| 
 | |
| 				const states = [ 'Idle', 'Walking', 'Running', 'Dance', 'Death', 'Sitting', 'Standing' ]; | |
| 				const emotes = [ 'Jump', 'Yes', 'No', 'Wave', 'Punch', 'ThumbsUp' ]; | |
| 
 | |
| 				gui = new GUI(); | |
| 
 | |
| 				mixer = new THREE.AnimationMixer( model ); | |
| 
 | |
| 				actions = {}; | |
| 
 | |
| 				for ( let i = 0; i < animations.length; i ++ ) { | |
| 
 | |
| 					const clip = animations[ i ]; | |
| 					const action = mixer.clipAction( clip ); | |
| 					actions[ clip.name ] = action; | |
| 
 | |
| 					if ( emotes.indexOf( clip.name ) >= 0 || states.indexOf( clip.name ) >= 4 ) { | |
| 
 | |
| 						action.clampWhenFinished = true; | |
| 						action.loop = THREE.LoopOnce; | |
| 
 | |
| 					} | |
| 
 | |
| 				} | |
| 
 | |
| 				// states | |
|  | |
| 				const statesFolder = gui.addFolder( 'States' ); | |
| 
 | |
| 				const clipCtrl = statesFolder.add( api, 'state' ).options( states ); | |
| 
 | |
| 				clipCtrl.onChange( function () { | |
| 
 | |
| 					fadeToAction( api.state, 0.5 ); | |
| 
 | |
| 				} ); | |
| 
 | |
| 				statesFolder.open(); | |
| 
 | |
| 				// emotes | |
|  | |
| 				const emoteFolder = gui.addFolder( 'Emotes' ); | |
| 
 | |
| 				function createEmoteCallback( name ) { | |
| 
 | |
| 					api[ name ] = function () { | |
| 
 | |
| 						fadeToAction( name, 0.2 ); | |
| 
 | |
| 						mixer.addEventListener( 'finished', restoreState ); | |
| 
 | |
| 					}; | |
| 
 | |
| 					emoteFolder.add( api, name ); | |
| 
 | |
| 				} | |
| 
 | |
| 				function restoreState() { | |
| 
 | |
| 					mixer.removeEventListener( 'finished', restoreState ); | |
| 
 | |
| 					fadeToAction( api.state, 0.2 ); | |
| 
 | |
| 				} | |
| 
 | |
| 				for ( let i = 0; i < emotes.length; i ++ ) { | |
| 
 | |
| 					createEmoteCallback( emotes[ i ] ); | |
| 
 | |
| 				} | |
| 
 | |
| 				emoteFolder.open(); | |
| 
 | |
| 				// expressions | |
|  | |
| 				face = model.getObjectByName( 'Head_4' ); | |
| 
 | |
| 				const expressions = Object.keys( face.morphTargetDictionary ); | |
| 				const expressionFolder = gui.addFolder( 'Expressions' ); | |
| 
 | |
| 				for ( let i = 0; i < expressions.length; i ++ ) { | |
| 
 | |
| 					expressionFolder.add( face.morphTargetInfluences, i, 0, 1, 0.01 ).name( expressions[ i ] ); | |
| 
 | |
| 				} | |
| 
 | |
| 				activeAction = actions[ 'Walking' ]; | |
| 				activeAction.play(); | |
| 
 | |
| 				expressionFolder.open(); | |
| 
 | |
| 			} | |
| 
 | |
| 			function fadeToAction( name, duration ) { | |
| 
 | |
| 				previousAction = activeAction; | |
| 				activeAction = actions[ name ]; | |
| 
 | |
| 				if ( previousAction !== activeAction ) { | |
| 
 | |
| 					previousAction.fadeOut( duration ); | |
| 
 | |
| 				} | |
| 
 | |
| 				activeAction | |
| 					.reset() | |
| 					.setEffectiveTimeScale( 1 ) | |
| 					.setEffectiveWeight( 1 ) | |
| 					.fadeIn( duration ) | |
| 					.play(); | |
| 
 | |
| 			} | |
| 
 | |
| 			function onWindowResize() { | |
| 
 | |
| 				camera.aspect = window.innerWidth / window.innerHeight; | |
| 				camera.updateProjectionMatrix(); | |
| 
 | |
| 				renderer.setSize( window.innerWidth, window.innerHeight ); | |
| 
 | |
| 			} | |
| 
 | |
| 			// | |
|  | |
| 			function animate() { | |
| 
 | |
| 				const dt = clock.getDelta(); | |
| 
 | |
| 				if ( mixer ) mixer.update( dt ); | |
| 
 | |
| 				requestAnimationFrame( animate ); | |
| 
 | |
| 				renderer.render( scene, camera ); | |
| 
 | |
| 				stats.update(); | |
| 
 | |
| 			} | |
| 
 | |
| 		</script> | |
| 
 | |
| 	</body> | |
| </html>
 | |
| 
 |