index.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  1. import { OrbitControls } from '../examples/resource/js/OrbitControls.js';
  2. import { CameraRig, FreeMovementControls } from '../examples/resource/js/three-story-controls.js' //'../../examples/resource/js/three-story-controls.js'
  3. import Stats from '../examples/resource/js/stats.module.js';
  4. import StatsWidget from '../examples/resource/js/stats-widget.js' //'@probe.gl/stats-widget';
  5. import { load } from '@loaders.gl/core';
  6. import { CesiumIonLoader, Tiles3DLoader } from '@loaders.gl/3d-tiles';
  7. import { Tileset3D, TILE_TYPE, TILE_CONTENT_STATE } from '@loaders.gl/tiles';
  8. import { CullingVolume, Plane } from '@math.gl/culling';
  9. import { _PerspectiveFrustum as PerspectiveFrustum} from '@math.gl/culling';
  10. import { Matrix4 as MathGLMatrix4 } from '@math.gl/core';
  11. import * as Util from './util';
  12. import {
  13. Scene,
  14. Clock,
  15. sRGBEncoding,
  16. Object3D,
  17. Group,
  18. Matrix4,
  19. Vector3,
  20. Vector2,
  21. Mesh,
  22. BufferGeometry,
  23. MeshStandardMaterial,
  24. LineSegments,
  25. Material,
  26. Float32BufferAttribute,
  27. ShaderMaterial,
  28. MeshBasicMaterial,
  29. Uint8BufferAttribute,
  30. BufferAttribute,
  31. Points,
  32. Camera,
  33. PerspectiveCamera,
  34. WebGLRenderer,
  35. Texture,
  36. Euler
  37. } from '../examples/resource/js/three.js';
  38. import { GLTFLoader } from '../examples/resource/js/GLTFLoader.js';
  39. import { DRACOLoader } from '../examples/resource/js/DRACOLoader.js';
  40. import { KTX2Loader } from '../examples/resource/js/KTX2Loader.js';
  41. import { Gradients } from './gradients';
  42. import { PointCloudFS, PointCloudVS } from './shaders';
  43. import { LoaderProps, LoaderOptions, Runtime, PointCloudColoring, Shading, GeoCoord, GeoTransform } from './types';
  44. const gradient = Gradients.RAINBOW;
  45. const gradientTexture = typeof document != 'undefined' ? Util.generateGradientTexture(gradient) : null;
  46. const grayscale = Gradients.GRAYSCALE;
  47. const grayscaleTexture = typeof document != 'undefined' ? Util.generateGradientTexture(grayscale) : null;
  48. const defaultOptions: LoaderOptions = {
  49. throttleRequests: true,
  50. maxRequests: 64,
  51. updateInterval: 0.1,
  52. maxConcurrency: 1,
  53. maximumScreenSpaceError: 16,
  54. maximumMemoryUsage: 32,
  55. viewDistanceScale: 1.0,
  56. skipLevelOfDetail: false,
  57. updateTransforms: true,
  58. shading: Shading.FlatTexture,
  59. transparent: false,
  60. pointCloudColoring: PointCloudColoring.White,
  61. pointSize: 1.0,
  62. worker: true,
  63. wireframe: false,
  64. debug: false,
  65. basisTranscoderPath: null,
  66. dracoDecoderPath: null,
  67. material: null,
  68. computeNormals: false,
  69. shaderCallback: null,
  70. geoTransform: GeoTransform.Reset,
  71. preloadTilesCount: null
  72. };
  73. /** 3D Tiles Loader */
  74. class Loader3DTiles {
  75. /**
  76. * Loads a tileset of 3D Tiles according to the given {@link LoaderProps}
  77. * @public
  78. *
  79. * @param props - Properties for this load call {@link LoaderProps}.
  80. * @returns An object containing the 3D Model to be added to the scene
  81. * and a runtime engine to be updated every frame.
  82. */
  83. public static async load (props: LoaderProps): Promise<{ model: Object3D; runtime: Runtime }> {
  84. const options = { ...defaultOptions, ...props.options };
  85. const { url } = props;
  86. const UPDATE_INTERVAL = options.updateInterval;
  87. const MAX_DEPTH_FOR_ORIENTATION = 5;
  88. const loadersGLOptions: {[key: string]: unknown} = {};
  89. if (options.cesiumIONToken) {
  90. loadersGLOptions['cesium-ion'] = {
  91. accessToken: options.cesiumIONToken,
  92. };
  93. const metadata = await CesiumIonLoader.preload(url, loadersGLOptions);
  94. loadersGLOptions['fetch'] = { headers: metadata.headers };
  95. }
  96. if (props.loadingManager) {
  97. props.loadingManager.itemStart(url);
  98. }
  99. const tilesetJson = await load(url, Tiles3DLoader, {
  100. ...loadersGLOptions,
  101. });
  102. const renderMap = {};
  103. const boxMap = {};
  104. const unloadQueue = [];
  105. const root = new Group();
  106. const tileBoxes = new Group();
  107. if (!options.debug) {
  108. tileBoxes.visible = false;
  109. } else {
  110. // TODO: Need to have a parent root with no transform and then a conent root with transform
  111. //root.add(tileBoxes)
  112. }
  113. const pointcloudUniforms = {
  114. pointSize: { type: 'f', value: options.pointSize },
  115. gradient: { type: 't', value: gradientTexture },
  116. grayscale: { type: 't', value: grayscaleTexture },
  117. rootCenter: { type: 'vec3', value: new Vector3() },
  118. rootNormal: { type: 'vec3', value: new Vector3() },
  119. coloring: { type: 'i', value: options.pointCloudColoring },
  120. hideGround: { type: 'b', value: true },
  121. elevationRange: { type: 'vec2', value: new Vector2(0, 400) },
  122. maxIntensity: { type: 'f', value: 1.0 },
  123. intensityContrast: { type: 'f', value: 1.0 },
  124. alpha: { type: 'f', value: 1.0 },
  125. };
  126. const pointcloudMaterial = new ShaderMaterial({
  127. uniforms: pointcloudUniforms,
  128. vertexShader: PointCloudVS,
  129. fragmentShader: PointCloudFS,
  130. transparent: options.transparent,
  131. vertexColors: true
  132. });
  133. let cameraReference = null;
  134. let rendererReference = null;
  135. const gltfLoader = new GLTFLoader();
  136. let ktx2Loader = undefined;
  137. let dracoLoader = undefined;
  138. if (options.basisTranscoderPath) {
  139. ktx2Loader = new KTX2Loader();
  140. ktx2Loader.detectSupport(props.renderer);
  141. ktx2Loader.setTranscoderPath(options.basisTranscoderPath + '/');
  142. ktx2Loader.setWorkerLimit(1);
  143. gltfLoader.setKTX2Loader(ktx2Loader);
  144. }
  145. if (options.dracoDecoderPath) {
  146. dracoLoader = new DRACOLoader();
  147. dracoLoader.setDecoderPath(options.dracoDecoderPath + '/');
  148. dracoLoader.setWorkerLimit(options.maxConcurrency);
  149. gltfLoader.setDRACOLoader(dracoLoader);
  150. }
  151. const unlitMaterial = new MeshBasicMaterial({transparent: options.transparent});
  152. const tileOptions = {
  153. maximumMemoryUsage: options.maximumMemoryUsage,
  154. maximumScreenSpaceError: options.maximumScreenSpaceError,
  155. viewDistanceScale: options.viewDistanceScale,
  156. skipLevelOfDetail: options.skipLevelOfDetail,
  157. updateTransforms: options.updateTransforms,
  158. throttleRequests: options.throttleRequests,
  159. maxRequests: options.maxRequests,
  160. contentLoader: async (tile) => {
  161. let tileContent = null;
  162. switch (tile.type) {
  163. case TILE_TYPE.POINTCLOUD: {
  164. tileContent = createPointNodes(tile, pointcloudMaterial, options, rootTransformInverse);
  165. break;
  166. }
  167. case TILE_TYPE.SCENEGRAPH:
  168. case TILE_TYPE.MESH: {
  169. tileContent = await createGLTFNodes(gltfLoader, tile, unlitMaterial, options, rootTransformInverse);
  170. break;
  171. }
  172. default:
  173. break;
  174. }
  175. if (tileContent) {
  176. tileContent.visible = false;
  177. renderMap[tile.id] = tileContent;
  178. root.add(renderMap[tile.id]);
  179. if (options.debug) {
  180. const box = Util.loadersBoundingBoxToMesh(tile);
  181. tileBoxes.add(box);
  182. boxMap[tile.id] = box;
  183. }
  184. }
  185. },
  186. onTileLoad: async (tile) => {
  187. if (tileset) {
  188. if (!orientationDetected && tile?.depth <= MAX_DEPTH_FOR_ORIENTATION) {
  189. detectOrientation(tile);
  190. }
  191. tileset._frameNumber++;
  192. tilesetUpdate(tileset, renderMap, rendererReference, cameraReference);
  193. }
  194. },
  195. onTileUnload: (tile) => {
  196. unloadQueue.push(tile);
  197. },
  198. onTileError: (tile, message) => {
  199. console.error('Tile error', tile.id, message);
  200. },
  201. };
  202. const tileset = new Tileset3D(tilesetJson, {
  203. ...tileOptions,
  204. loadOptions: {
  205. ...loadersGLOptions,
  206. maxConcurrency: options.maxConcurrency,
  207. worker: options.worker,
  208. gltf: {
  209. loadImages: false,
  210. },
  211. '3d-tiles': {
  212. loadGLTF: false
  213. },
  214. },
  215. });
  216. //
  217. // transformations
  218. const threeMat = new Matrix4();
  219. const tileTrasnform = new Matrix4();
  220. const rootCenter = new Vector3();
  221. let orientationDetected = false;
  222. if (tileset.root.boundingVolume) {
  223. if (tileset.root.header.boundingVolume.region) {
  224. // TODO: Handle region type bounding volumes
  225. // https://github.com/visgl/loaders.gl/issues/1994
  226. console.warn("Cannot apply a model matrix to bounding volumes of type region. Tileset stays in original geo-coordinates.")
  227. options.geoTransform = GeoTransform.WGS84Cartesian;
  228. }
  229. tileTrasnform.setPosition(
  230. tileset.root.boundingVolume.center[0],
  231. tileset.root.boundingVolume.center[1],
  232. tileset.root.boundingVolume.center[2]
  233. )
  234. } else {
  235. console.warn("Bounding volume not found, no transformations applied")
  236. }
  237. if (options.debug) {
  238. const box = Util.loadersBoundingBoxToMesh(tileset.root);
  239. tileBoxes.add(box);
  240. boxMap[tileset.root.id] = box;
  241. }
  242. let disposeFlag = false;
  243. let loadingEnded = false;
  244. pointcloudUniforms.rootCenter.value.copy(rootCenter);
  245. pointcloudUniforms.rootNormal.value.copy(new Vector3(0, 0, 1).normalize());
  246. // Extra stats
  247. tileset.stats.get('Loader concurrency').count = options.maxConcurrency
  248. tileset.stats.get('Maximum SSE').count = options.maximumScreenSpaceError;
  249. tileset.stats.get('Maximum mem usage').count = options.maximumMemoryUsage;
  250. let timer = 0;
  251. let lastCameraTransform: Matrix4 = null;
  252. let lastCameraAspect = null;
  253. const lastCameraPosition = new Vector3(Infinity, Infinity, Infinity);
  254. let sseDenominator = null;
  255. root.updateMatrixWorld(true);
  256. const lastRootTransform:Matrix4 = new Matrix4().copy(root.matrixWorld)
  257. const rootTransformInverse = new Matrix4().copy(lastRootTransform).invert();
  258. detectOrientation(tileset.root);
  259. updateResetTransform();
  260. if (options.debug) {
  261. boxMap[tileset.root.id].applyMatrix4(threeMat);
  262. tileBoxes.matrixWorld.copy(root.matrixWorld);
  263. }
  264. if (options.geoTransform == GeoTransform.Mercator) {
  265. const coords = Util.datumsToSpherical(
  266. tileset.cartographicCenter[1],
  267. tileset.cartographicCenter[0]
  268. )
  269. rootCenter.set(
  270. coords.x,
  271. 0,
  272. -coords.y
  273. );
  274. root.position.copy(rootCenter);
  275. root.rotation.set(-Math.PI / 2, 0, 0);
  276. root.updateMatrixWorld(true);
  277. } else if (options.geoTransform == GeoTransform.WGS84Cartesian) {
  278. root.applyMatrix4(tileTrasnform);
  279. root.updateMatrixWorld(true);
  280. rootCenter.copy(root.position);
  281. }
  282. function detectOrientation(tile) {
  283. if (!tile.boundingVolume.halfAxes) {
  284. return;
  285. }
  286. const halfAxes = tile.boundingVolume.halfAxes;
  287. const orientationMatrix = new Matrix4()
  288. .extractRotation(Util.getMatrix4FromHalfAxes(halfAxes))
  289. .premultiply(new Matrix4().extractRotation(rootTransformInverse));
  290. const rotation = new Euler().setFromRotationMatrix(orientationMatrix);
  291. if (!rotation.equals(new Euler())) {
  292. orientationDetected = true;
  293. const pos = new Vector3(
  294. tileTrasnform.elements[12],
  295. tileTrasnform.elements[13],
  296. tileTrasnform.elements[14])
  297. ;
  298. tileTrasnform.extractRotation(orientationMatrix);
  299. tileTrasnform.setPosition(pos);
  300. updateResetTransform();
  301. }
  302. }
  303. function updateResetTransform() {
  304. if (options.geoTransform != GeoTransform.WGS84Cartesian) {
  305. // Reset the current model matrix and apply our own transformation
  306. threeMat.copy(tileTrasnform).invert();
  307. threeMat.premultiply(lastRootTransform);
  308. threeMat.copy(lastRootTransform).multiply(new Matrix4().copy(tileTrasnform).invert());
  309. tileset.modelMatrix = new MathGLMatrix4(threeMat.toArray());
  310. }
  311. }
  312. function tilesetUpdate(tileset, renderMap, renderer, camera) {
  313. if (disposeFlag) {
  314. return;
  315. }
  316. // Assumes camera fov, near and far are not changing
  317. if (!sseDenominator || camera.aspect != lastCameraAspect) {
  318. const loadersFrustum = new PerspectiveFrustum({
  319. fov: (camera.fov / 180) * Math.PI,
  320. aspectRatio: camera.aspect,
  321. near: camera.near,
  322. far: camera.far,
  323. });
  324. sseDenominator = loadersFrustum.sseDenominator;
  325. lastCameraAspect = camera.aspect;
  326. if (options.debug) {
  327. console.log('Updated sse denonimator:', sseDenominator);
  328. }
  329. }
  330. const frustum = Util.getCameraFrustum(camera);
  331. const planes = frustum.planes.map((plane) => new Plane(plane.normal.toArray(), plane.constant));
  332. const cullingVolume = new CullingVolume(planes);
  333. const rendererSize = new Vector2();
  334. renderer.getSize(rendererSize);
  335. const frameState = {
  336. camera: {
  337. position: lastCameraPosition.toArray(),
  338. },
  339. height: rendererSize.y,
  340. frameNumber: tileset._frameNumber,
  341. sseDenominator: sseDenominator,
  342. cullingVolume: cullingVolume,
  343. viewport: {
  344. id: 0,
  345. },
  346. };
  347. tileset._cache.reset();
  348. tileset._traverser.traverse(tileset.root, frameState, tileset.options);
  349. for (const tile of tileset.tiles) {
  350. if (tile.selected) {
  351. if (!renderMap[tile.id]) {
  352. console.error('TILE SELECTED BUT NOT LOADED!!', tile.id);
  353. } else {
  354. // Make sure it's visible
  355. renderMap[tile.id].visible = true;
  356. }
  357. } else {
  358. if (renderMap[tile.id]) {
  359. renderMap[tile.id].visible = false;
  360. }
  361. }
  362. }
  363. while (unloadQueue.length > 0) {
  364. const tile = unloadQueue.pop();
  365. if (renderMap[tile.id] && tile.contentState == TILE_CONTENT_STATE.UNLOADED) {
  366. root.remove(renderMap[tile.id]);
  367. disposeNode(renderMap[tile.id]);
  368. delete renderMap[tile.id];
  369. }
  370. if (boxMap[tile.id]) {
  371. disposeNode(boxMap[tile.id]);
  372. tileBoxes.remove(boxMap[tile.id]);
  373. delete boxMap[tile.id];
  374. }
  375. }
  376. const tilesLoaded = tileset.stats.get('Tiles Loaded').count;
  377. const tilesLoading = tileset.stats.get('Tiles Loading').count;
  378. if (props.onProgress) {
  379. props.onProgress(
  380. tilesLoaded,
  381. tilesLoaded + tilesLoading
  382. );
  383. }
  384. if (props.loadingManager && !loadingEnded) {
  385. if (tilesLoading == 0 &&
  386. (
  387. options.preloadTilesCount == null ||
  388. tilesLoaded >= options.preloadTilesCount)
  389. ) {
  390. loadingEnded = true;
  391. props.loadingManager.itemEnd(props.url);
  392. }
  393. }
  394. return frameState;
  395. }
  396. return {
  397. model: root,
  398. runtime: {
  399. getTileset: () => {
  400. return tileset;
  401. },
  402. getStats: () => {
  403. return tileset.stats;
  404. },
  405. showTiles: (visible) => {
  406. tileBoxes.visible = visible;
  407. },
  408. setWireframe: (wireframe) => {
  409. options.wireframe = wireframe;
  410. root.traverse((object) => {
  411. if (object instanceof Mesh) {
  412. object.material.wireframe = wireframe;
  413. }
  414. });
  415. },
  416. setDebug: (debug) => {
  417. options.debug = debug;
  418. tileBoxes.visible = debug;
  419. },
  420. setShading: (shading) => {
  421. options.shading = shading;
  422. },
  423. getTileBoxes: () => {
  424. return tileBoxes;
  425. },
  426. setViewDistanceScale: (scale) => {
  427. tileset.options.viewDistanceScale = scale;
  428. tileset._frameNumber++;
  429. tilesetUpdate(tileset, renderMap, rendererReference, cameraReference);
  430. },
  431. setHideGround: (enabled) => {
  432. pointcloudUniforms.hideGround.value = enabled;
  433. },
  434. setPointCloudColoring: (selection) => {
  435. pointcloudUniforms.coloring.value = selection;
  436. },
  437. setElevationRange: (range) => {
  438. pointcloudUniforms.elevationRange.value.set(range[0], range[1]);
  439. },
  440. setMaxIntensity: (intensity) => {
  441. pointcloudUniforms.maxIntensity.value = intensity;
  442. },
  443. setIntensityContrast: (contrast) => {
  444. pointcloudUniforms.intensityContrast.value = contrast;
  445. },
  446. setPointAlpha: (alpha) => {
  447. pointcloudUniforms.alpha.value = alpha;
  448. },
  449. getLatLongHeightFromPosition: (position) => {
  450. const cartographicPosition = tileset.ellipsoid.cartesianToCartographic(
  451. new Vector3().copy(position).applyMatrix4(new Matrix4().copy(threeMat).invert()).toArray(),
  452. );
  453. return {
  454. lat: cartographicPosition[1],
  455. long: cartographicPosition[0],
  456. height: cartographicPosition[2],
  457. };
  458. },
  459. getPositionFromLatLongHeight: (coord) => {
  460. const cartesianPosition = tileset.ellipsoid.cartographicToCartesian([coord.long, coord.lat, coord.height]);
  461. return new Vector3(...cartesianPosition).applyMatrix4(threeMat);
  462. },
  463. getCameraFrustum: (camera: Camera) => {
  464. const frustum = Util.getCameraFrustum(camera);
  465. const meshes = frustum.planes
  466. .map((plane) => new Plane(plane.normal.toArray(), plane.constant))
  467. .map((loadersPlane) => Util.loadersPlaneToMesh(loadersPlane));
  468. const model = new Group();
  469. for (const mesh of meshes) model.add(mesh);
  470. return model;
  471. },
  472. update: function (dt: number, renderer: WebGLRenderer, camera: Camera) {
  473. cameraReference = camera;
  474. rendererReference = renderer;
  475. timer += dt;
  476. if (tileset && timer >= UPDATE_INTERVAL) {
  477. if (!lastRootTransform.equals(root.matrixWorld)) {
  478. timer = 0;
  479. lastRootTransform.copy(root.matrixWorld);
  480. updateResetTransform();
  481. const rootCenter = new Vector3().setFromMatrixPosition(lastRootTransform);
  482. pointcloudUniforms.rootCenter.value.copy(rootCenter);
  483. pointcloudUniforms.rootNormal.value.copy(new Vector3(0, 0, 1).applyMatrix4(lastRootTransform).normalize());
  484. rootTransformInverse.copy(lastRootTransform).invert();
  485. if (options.debug) {
  486. boxMap[tileset.root.id].matrixWorld.copy(threeMat);
  487. boxMap[tileset.root.id].applyMatrix4(lastRootTransform);
  488. }
  489. }
  490. if (lastCameraTransform == null) {
  491. lastCameraTransform = new Matrix4().copy(camera.matrixWorld);
  492. } else {
  493. const cameraChanged: boolean =
  494. !camera.matrixWorld.equals(lastCameraTransform) ||
  495. !((<PerspectiveCamera>camera).aspect == lastCameraAspect);
  496. if (cameraChanged) {
  497. timer = 0;
  498. tileset._frameNumber++;
  499. camera.getWorldPosition(lastCameraPosition);
  500. lastCameraTransform.copy(camera.matrixWorld);
  501. tilesetUpdate(tileset, renderMap, renderer, camera);
  502. }
  503. }
  504. }
  505. },
  506. dispose: function () {
  507. disposeFlag = true;
  508. tileset._destroy();
  509. while (root.children.length > 0) {
  510. const obj = root.children[0];
  511. disposeNode(obj);
  512. root.remove(obj);
  513. }
  514. while (tileBoxes.children.length > 0) {
  515. const obj = tileBoxes.children[0] as LineSegments;
  516. tileBoxes.remove(obj);
  517. obj.geometry.dispose();
  518. (<Material>obj.material).dispose();
  519. }
  520. if (ktx2Loader) {
  521. ktx2Loader.dispose();
  522. }
  523. if (dracoLoader) {
  524. dracoLoader.dispose();
  525. }
  526. },
  527. },
  528. };
  529. }
  530. }
  531. async function createGLTFNodes(gltfLoader, tile, unlitMaterial, options, rootTransformInverse): Promise<Object3D> {
  532. return new Promise((resolve, reject) => {
  533. const rotateX = new Matrix4().makeRotationAxis(new Vector3(1, 0, 0), Math.PI / 2);
  534. const shouldRotate = tile.tileset.asset?.gltfUpAxis !== "Z";
  535. // The computed trasnform already contains the root's transform, so we have to invert it
  536. const contentTransform = new Matrix4().fromArray(tile.computedTransform).premultiply(rootTransformInverse);
  537. if (shouldRotate) {
  538. contentTransform.multiply(rotateX); // convert from GLTF Y-up to Z-up
  539. }
  540. gltfLoader.parse(
  541. tile.content.gltfArrayBuffer,
  542. tile.contentUrl ? tile.contentUrl.substr(0,tile.contentUrl.lastIndexOf('/') + 1) : '',
  543. (gltf) => {
  544. const tileContent = gltf.scenes[0] as Group;
  545. tileContent.applyMatrix4(contentTransform);
  546. tileContent.traverse((object) => {
  547. if (object.type == "Mesh") {
  548. const mesh = object as Mesh;
  549. const originalMaterial = (mesh.material as MeshStandardMaterial);
  550. const originalMap = originalMaterial.map;
  551. if (options.material) {
  552. mesh.material = options.material.clone();
  553. originalMaterial.dispose();
  554. } else if (options.shading == Shading.FlatTexture) {
  555. mesh.material = unlitMaterial.clone();
  556. originalMaterial.dispose();
  557. }
  558. if (options.shading != Shading.ShadedNoTexture) {
  559. if ((mesh.material as Material).type == "ShaderMaterial") {
  560. (mesh.material as ShaderMaterial).uniforms.map = { value: originalMap };
  561. } else {
  562. (mesh.material as MeshStandardMaterial).map = originalMap;
  563. }
  564. } else {
  565. if (originalMap) {
  566. originalMap.dispose();
  567. }
  568. (mesh.material as MeshStandardMaterial).map = null;
  569. }
  570. if (options.shaderCallback) {
  571. mesh.onBeforeRender = options.shaderCallback;
  572. }
  573. (mesh.material as MeshStandardMaterial | MeshBasicMaterial).wireframe = options.wireframe;
  574. if (options.computeNormals) {
  575. mesh.geometry.computeVertexNormals();
  576. }
  577. }
  578. });
  579. resolve(tileContent);
  580. },
  581. (e) => {
  582. reject(new Error(`error parsing gltf in tile ${tile.id}: ${e}`));
  583. },
  584. );
  585. });
  586. }
  587. function createPointNodes(tile, pointcloudMaterial, options, rootTransformInverse) {
  588. const d = {
  589. rtc_center: tile.content.rtcCenter, // eslint-disable-line camelcase
  590. points: tile.content.attributes.positions,
  591. intensities: tile.content.attributes.intensity,
  592. classifications: tile.content.attributes.classification,
  593. rgb: null,
  594. rgba: null,
  595. };
  596. const { colors } = tile.content.attributes;
  597. if (colors && colors.size === 3) {
  598. d.rgb = colors.value;
  599. }
  600. if (colors && colors.size === 4) {
  601. d.rgba = colors.value;
  602. }
  603. const geometry = new BufferGeometry();
  604. geometry.setAttribute('position', new Float32BufferAttribute(d.points, 3));
  605. const contentTransform = new Matrix4().fromArray(tile.computedTransform).premultiply(rootTransformInverse);
  606. if (d.rgba) {
  607. geometry.setAttribute('color', new Float32BufferAttribute(d.rgba, 4));
  608. } else if (d.rgb) {
  609. geometry.setAttribute('color', new Uint8BufferAttribute(d.rgb, 3, true));
  610. }
  611. if (d.intensities) {
  612. geometry.setAttribute(
  613. 'intensity',
  614. // Handles both 16bit or 8bit intensity values
  615. new BufferAttribute(d.intensities, 1, true)
  616. );
  617. }
  618. if (d.classifications) {
  619. geometry.setAttribute('classification', new Uint8BufferAttribute(d.classifications, 1, false));
  620. }
  621. const tileContent = new Points(geometry, options.material || pointcloudMaterial);
  622. if (d.rtc_center) {
  623. const c = d.rtc_center;
  624. contentTransform.multiply(new Matrix4().makeTranslation(c[0], c[1], c[2]));
  625. }
  626. tileContent.applyMatrix4(contentTransform);
  627. return tileContent;
  628. }
  629. function disposeMaterial(material) {
  630. if ((material as ShaderMaterial)?.uniforms?.map) {
  631. ((material as ShaderMaterial)?.uniforms?.map.value as Texture)?.dispose();
  632. }
  633. else if (material.map) {
  634. (material.map as Texture)?.dispose();
  635. }
  636. material.dispose();
  637. }
  638. function disposeNode(node) {
  639. node.traverse((object) => {
  640. if (object.isMesh) {
  641. object.geometry.dispose();
  642. if (object.material.isMaterial) {
  643. disposeMaterial(object.material);
  644. } else {
  645. // an array of materials
  646. for (const material of object.material) {
  647. disposeMaterial(material);
  648. }
  649. }
  650. }
  651. });
  652. for (let i = node.children.length - 1; i >= 0; i--) {
  653. const obj = node.children[i];
  654. node.remove(obj);
  655. }
  656. }
  657. export { Loader3DTiles, PointCloudColoring, Shading, Runtime, GeoCoord, GeoTransform, LoaderOptions, LoaderProps };
  658. //------------------------------------------------------test
  659. const queryParams = new URLSearchParams(document.location.search);
  660. const canvasParent = document.querySelector('#canvas-parent') as HTMLElement;
  661. const statsParent = document.querySelector('#stats-widget') as HTMLElement
  662. const scene = new Scene();
  663. const camera = new PerspectiveCamera(
  664. 35,
  665. 1,
  666. 0.01,
  667. 1000,
  668. );
  669. const renderer = new WebGLRenderer();
  670. renderer.outputEncoding = sRGBEncoding;
  671. const clock = new Clock()
  672. const rig = new CameraRig(camera, scene)
  673. let controls = undefined;
  674. if (queryParams.get('orbit')) {
  675. controls = new OrbitControls( camera, canvasParent);
  676. controls.listenToKeyEvents( document.body );
  677. camera.position.set(0,200,0);
  678. controls.update();
  679. } else {
  680. controls = new FreeMovementControls(rig, {
  681. panDegreeFactor: 2,
  682. wheelScaleFactor: 0.01,
  683. keyboardScaleFactor: 0.015,
  684. keyboardDampFactor: 0
  685. });
  686. controls.enable();
  687. }
  688. canvasParent.appendChild(renderer.domElement);
  689. const threeJsStats = new Stats();
  690. threeJsStats.domElement.style.position = 'absolute';
  691. threeJsStats.domElement.style.top = '10px';
  692. threeJsStats.domElement.style.left = '10px';
  693. canvasParent.appendChild( threeJsStats.domElement );
  694. let tilesRuntime = undefined;
  695. let statsRuntime = undefined;
  696. if (queryParams.get('tilesetUrl')) {
  697. (document.querySelector('#example-desc') as HTMLElement).style.display = 'none';
  698. }
  699. loadTileset();
  700. async function loadTileset() {
  701. const result = await Loader3DTiles.load(
  702. {
  703. url:
  704. queryParams.get('tilesetUrl') ??
  705. //'https://int.nyt.com/data/3dscenes/ONA360/TILESET/0731_FREEMAN_ALLEY_10M_A_36x8K__10K-PN_50P_DB/tileset_tileset.json',
  706. 'https://testgis.4dage.com/LVBADUI_qp/tileset.json',
  707. renderer: renderer,
  708. options: {
  709. dracoDecoderPath: 'https://cdn.jsdelivr.net/npm/three@0.137.0/examples/js/libs/draco',
  710. basisTranscoderPath: 'https://cdn.jsdelivr.net/npm/three@0.137.0/examples/js/libs/basis',
  711. maximumScreenSpaceError: 48 //queryParams.get('sse') ?? 48,
  712. }
  713. }
  714. );
  715. const {model, runtime} = result;
  716. model.rotation.set(-Math.PI / 2, 0, Math.PI / 2);
  717. if (!queryParams.get('tilesetUrl')) {
  718. model.position.set(-1, 4, -16);
  719. }
  720. tilesRuntime = runtime;
  721. scene.add(model);
  722. statsRuntime = new StatsWidget(runtime.getStats(), {container: statsParent });
  723. statsParent.style.visibility = 'visible';
  724. }
  725. function render(t) {
  726. const dt = clock.getDelta()
  727. controls.update(t);
  728. if (tilesRuntime) {
  729. tilesRuntime.update(dt, renderer, camera);
  730. }
  731. if (statsRuntime) {
  732. statsRuntime.update();
  733. }
  734. renderer.render(scene, camera);
  735. threeJsStats.update();
  736. window.requestAnimationFrame(render);
  737. }
  738. onWindowResize();
  739. function onWindowResize() {
  740. renderer.setSize(canvasParent.clientWidth, canvasParent.clientHeight);
  741. camera.aspect = canvasParent.clientWidth / canvasParent.clientHeight;
  742. camera.updateProjectionMatrix();
  743. }
  744. window.addEventListener('resize', onWindowResize)
  745. render(undefined);