|
| 1 | +<!DOCTYPE html><html lang="en"><head><title></title></head> |
| 2 | +<style>canvas{ display:block; } body, html { padding:0px; margin:0px; width:100%; height:100%; }</style> |
| 3 | +<body><script src="../../../import-map.js"></script><script type="module"> |
| 4 | +// #region IMPORTS |
| 5 | + import useThreeWebGL2, { THREE, useDarkScene, useVisualDebug } from '@lib/useThreeWebGL2.js'; |
| 6 | + import * as Util from '@lib/util.js'; |
| 7 | + // import { GLTFLoader } from 'three/GLTFLoader.js'; |
| 8 | + |
| 9 | + // import HotKeys from '@lib/misc/HotKeys.js'; |
| 10 | + import KeyboardInput from '@lib/input/KeyboardInput.js'; |
| 11 | + import Cursor3DMaterial from '@lib/shader/Cursor3DMaterial.js'; |
| 12 | + |
| 13 | + import Vec3 from '@lib/maths/Vec3.js'; |
| 14 | + import Quat from '@lib/maths/Quat.js'; |
| 15 | + import Radian from '@lib/maths/Radian.js'; |
| 16 | + // import FSemiImplicitEuler from '@lib/maths/springs/FSemiImplicitEuler.js'; |
| 17 | + import FImplicitEuler from '@lib/maths/springs/FImplicitEuler.js'; |
| 18 | + |
| 19 | + import { Pane } from '@tp/tweakpane/tweakpane-4.0.4.min.js'; |
| 20 | +// #endregion |
| 21 | + |
| 22 | +// #region MAIN |
| 23 | +let App = useDarkScene( useThreeWebGL2() ); |
| 24 | +let Debug = {}; |
| 25 | +let Ref = {}; |
| 26 | + |
| 27 | +window.addEventListener( 'load', async ()=>{ |
| 28 | + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 29 | + App.sphericalLook( 45, 20, 7, [0,0,0] ); |
| 30 | + Debug = await useVisualDebug( App ); |
| 31 | + |
| 32 | + Ref.ki = new KeyboardInput(); |
| 33 | + // Ref.hk = new HotKeys().reg( 'x', ()=>{ |
| 34 | + // Debug.reset(); |
| 35 | + // Ref.ctrl.update( 0.01, App.camera ); |
| 36 | + // }); |
| 37 | + |
| 38 | + Ref.cursor = Cursor3DMaterial.createMesh( {factor:0} ); |
| 39 | + App.scene.add( Ref.cursor ); |
| 40 | + |
| 41 | + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 42 | + // const url = '../../../res'; |
| 43 | + // const dl = await Promise.all([ //allSettled |
| 44 | + // loadChar( {path:`${url}/models/mannequin.gltf`} ), |
| 45 | + // ]); |
| 46 | + |
| 47 | + // // --------------------------- |
| 48 | + // const { tf, skel } = dl[0]; |
| 49 | + // App.scene.add( tf.scene ); |
| 50 | + // |
| 51 | + // |
| 52 | + Ref.ctrl = new Controller(); |
| 53 | + Ref.ctrl.model = Ref.cursor; |
| 54 | + |
| 55 | + Ref.ctrl.update( 0.01, App.camera, {x:0, y:0} ); |
| 56 | + |
| 57 | + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 58 | + App.createRenderLoop( onPreRender ).start(); |
| 59 | + appendGithubLink( false ); |
| 60 | + buildUI(); |
| 61 | +}); |
| 62 | + |
| 63 | +function onPreRender( dt, et ){ |
| 64 | + Debug.reset(); |
| 65 | + Ref.ctrl.update( dt, App.camera, Ref.ki.getWASD() ); |
| 66 | +} |
| 67 | + |
| 68 | +async function buildUI(){ |
| 69 | + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 70 | + const p = new Pane( ); |
| 71 | + const Data = { |
| 72 | + Move: 'Use WASD Keys' |
| 73 | + }; |
| 74 | + |
| 75 | + const f = p.addFolder({ title: 'Instructions', expanded: true }); |
| 76 | + f.addBinding( Data, 'Move', { readonly: true, }); |
| 77 | + |
| 78 | + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 79 | + Ref.pane = p; |
| 80 | +} |
| 81 | +// #endregion |
| 82 | + |
| 83 | +// #region LOADING |
| 84 | + async function loadChar( props={} ){ |
| 85 | + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 86 | + // LOAD |
| 87 | + const opt = { skel:true, matswap:null, onSkinned: null, tpose:false, ...props }; |
| 88 | + const tf = await new GLTFLoader().loadAsync( opt.path ); |
| 89 | + |
| 90 | + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 91 | + let skel = null |
| 92 | + let mat = null; |
| 93 | + for( const m of Util.traverseFind( tf.scene, o=> (o.type === 'SkinnedMesh') ) ){ |
| 94 | + // ------------------------------------ |
| 95 | + switch( typeof opt.matswop ){ |
| 96 | + case 'string': |
| 97 | + switch( opt.matswop ){ |
| 98 | + case 'toon': m.material = new THREE.MeshToonMaterial( { map: m.material.map, normalMap: m.material.normalMap } ); break; |
| 99 | + } |
| 100 | + break; |
| 101 | + } |
| 102 | + |
| 103 | + // ------------------------------------ |
| 104 | + if( opt.onSkinned ) opt.onSkinned( m ); |
| 105 | + |
| 106 | + // ------------------------------------ |
| 107 | + if( !skel ) skel = m.skeleton; // First skeleton |
| 108 | + } |
| 109 | + |
| 110 | + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 111 | + return { tf, skel }; |
| 112 | + } |
| 113 | +// #endregion |
| 114 | + |
| 115 | +class Controller{ |
| 116 | +// #region MAIN |
| 117 | + model = null; // Root of model to apply rotation & translation |
| 118 | + maxSpeed = 5; // Make Speed of movement |
| 119 | + |
| 120 | + vel = new FImplicitEuler( { osc:1 } ); // Normalized velocity |
| 121 | + dir = new FImplicitEuler( { osc:2 } ); // Radian angle of rotation |
| 122 | + |
| 123 | + fwd = new Vec3(); // View Forward Direction |
| 124 | + rit = new Vec3(); // View Right Direction |
| 125 | + look = new Vec3(); // Character Look Direction |
| 126 | + constructor(){} |
| 127 | +// #endregion |
| 128 | + |
| 129 | +// #region CALC |
| 130 | + updateCharDir( ip ){ |
| 131 | + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 132 | + // No Input, slow down to a stop |
| 133 | + if( ip.x === 0 && ip.y === 0 ){ |
| 134 | + this.vel.target = 0; |
| 135 | + return; |
| 136 | + } |
| 137 | + |
| 138 | + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 139 | + // Compute rotation angle & look direction from INPUT |
| 140 | + const fwd = new Vec3().fromScale( this.fwd, ip.y ); |
| 141 | + const rit = new Vec3().fromScale( this.rit, ip.x ); |
| 142 | + this.look.fromAdd( fwd, rit ).norm(); |
| 143 | + |
| 144 | + // Compute angle of the look direction from world forward |
| 145 | + let rad = Vec3.angle( [0,0,1], this.look ); |
| 146 | + if( Vec3.dot( [1,0,0], this.look ) < 0 ) rad = -rad; |
| 147 | + |
| 148 | + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 149 | + // Fix angle to ignore boundary for smooth shortest path rotation |
| 150 | + this.dir.target = Radian.continuousTarget( this.dir.value, rad ); |
| 151 | + this.vel.target = 1; // Speed up |
| 152 | + } |
| 153 | + |
| 154 | + updateViewAxis( camera ){ |
| 155 | + // Compute the XZ plane direction in relation to camera view |
| 156 | + const q = new Quat().copyObj( camera.quaternion ); |
| 157 | + this.fwd.fromQuat( q, [0,0,1] ).negate().sy(0).norm(); |
| 158 | + this.rit.fromCross( this.fwd, [0,1,0] ).norm(); |
| 159 | + |
| 160 | + Debug.ln.add( [0,0,0], this.fwd, 0x00ffff ); |
| 161 | + Debug.ln.add( [0,0,0], this.rit, 0xffff00 ); |
| 162 | + } |
| 163 | +// #endregion |
| 164 | + |
| 165 | +// #region RENDER LOOP |
| 166 | + update( dt, camera, ip ){ |
| 167 | + this.updateViewAxis( camera ); // Compute View Directions |
| 168 | + this.updateCharDir( ip ); // Look direction for rotation & translation |
| 169 | + |
| 170 | + this.dir.update( dt ); // Run Float Spring |
| 171 | + this.vel.update( dt ); |
| 172 | + |
| 173 | + // Make model face same direction as camera |
| 174 | + const rot = new Quat().fromAxisAngle( [0,1,0], this.dir.value ); |
| 175 | + this.model.quaternion.fromArray( rot ); |
| 176 | + |
| 177 | + // 4Debugging |
| 178 | + const dir = new Vec3().fromQuat( rot, [0,0,1] ).norm(); |
| 179 | + Debug.ln.add( [0,0,0], dir, 0xff00ff ); |
| 180 | + Debug.ln.add( [0,0,0], this.look, 0xffffff ); |
| 181 | + |
| 182 | + // Move modal toward the direction it is looking |
| 183 | + if( ! this.vel.isDone || this.vel.target > 0 ){ |
| 184 | + const move = new Vec3().fromScale( this.look, this.maxSpeed * dt * this.vel.value ); |
| 185 | + this.model.position.x += move[0]; |
| 186 | + this.model.position.z += move[2]; |
| 187 | + } |
| 188 | + } |
| 189 | +// #endregion |
| 190 | +} |
| 191 | + |
| 192 | +</script></body></html> |
0 commit comments