PntsLoader.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. import AttributeCompression from "../../Core/AttributeCompression.js";
  2. import Cartesian3 from "../../Core/Cartesian3.js";
  3. import Color from "../../Core/Color.js";
  4. import Check from "../../Core/Check.js";
  5. import ComponentDatatype from "../../Core/ComponentDatatype.js";
  6. import defaultValue from "../../Core/defaultValue.js";
  7. import defer from "../../Core/defer.js";
  8. import defined from "../../Core/defined.js";
  9. import Matrix4 from "../../Core/Matrix4.js";
  10. import PrimitiveType from "../../Core/PrimitiveType.js";
  11. import MersenneTwister from "../../ThirdParty/mersenne-twister.js";
  12. import Buffer from "../../Renderer/Buffer.js";
  13. import BufferUsage from "../../Renderer/BufferUsage.js";
  14. import AlphaMode from "../AlphaMode.js";
  15. import AttributeType from "../AttributeType.js";
  16. import Axis from "../Axis.js";
  17. import parseBatchTable from "../parseBatchTable.js";
  18. import DracoLoader from "../DracoLoader.js";
  19. import StructuralMetadata from "../StructuralMetadata.js";
  20. import ResourceLoader from "../ResourceLoader.js";
  21. import MetadataClass from "../MetadataClass.js";
  22. import ModelComponents from "../ModelComponents.js";
  23. import PntsParser from "../PntsParser.js";
  24. import PropertyTable from "../PropertyTable.js";
  25. import ResourceLoaderState from "../ResourceLoaderState.js";
  26. import VertexAttributeSemantic from "../VertexAttributeSemantic.js";
  27. const Components = ModelComponents.Components;
  28. const Scene = ModelComponents.Scene;
  29. const Node = ModelComponents.Node;
  30. const Primitive = ModelComponents.Primitive;
  31. const Attribute = ModelComponents.Attribute;
  32. const Quantization = ModelComponents.Quantization;
  33. const FeatureIdAttribute = ModelComponents.FeatureIdAttribute;
  34. const Material = ModelComponents.Material;
  35. const MetallicRoughness = ModelComponents.MetallicRoughness;
  36. /**
  37. * Loads a .pnts point cloud and transcodes it into a {@link ModelComponents}
  38. *
  39. * @alias PntsLoader
  40. * @constructor
  41. * @augments ResourceLoader
  42. * @private
  43. *
  44. * @param {Object} options An object containing the following properties
  45. * @param {ArrayBuffer} options.arrayBuffer The array buffer of the pnts contents
  46. * @param {Number} [options.byteOffset] The byte offset to the beginning of the pnts contents in the array buffer
  47. */
  48. export default function PntsLoader(options) {
  49. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  50. const arrayBuffer = options.arrayBuffer;
  51. const byteOffset = defaultValue(options.byteOffset, 0);
  52. //>>includeStart('debug', pragmas.debug);
  53. Check.typeOf.object("options.arrayBuffer", arrayBuffer);
  54. //>>includeEnd('debug');
  55. this._arrayBuffer = arrayBuffer;
  56. this._byteOffset = byteOffset;
  57. this._parsedContent = undefined;
  58. this._decodePromise = undefined;
  59. this._decodedAttributes = undefined;
  60. this._promise = defer();
  61. this._state = ResourceLoaderState.UNLOADED;
  62. this._buffers = [];
  63. // The batch table object contains a json and a binary component access using keys of the same name.
  64. this._components = undefined;
  65. this._transform = Matrix4.IDENTITY;
  66. }
  67. if (defined(Object.create)) {
  68. PntsLoader.prototype = Object.create(ResourceLoader.prototype);
  69. PntsLoader.prototype.constructor = PntsLoader;
  70. }
  71. Object.defineProperties(PntsLoader.prototype, {
  72. /**
  73. * A promise that resolves to the resource when the resource is ready.
  74. *
  75. * @memberof PntsLoader.prototype
  76. *
  77. * @type {Promise.<PntsLoader>}
  78. * @readonly
  79. * @private
  80. */
  81. promise: {
  82. get: function () {
  83. return this._promise.promise;
  84. },
  85. },
  86. /**
  87. * The cache key of the resource
  88. *
  89. * @memberof PntsLoader.prototype
  90. *
  91. * @type {String}
  92. * @readonly
  93. * @private
  94. */
  95. cacheKey: {
  96. get: function () {
  97. return undefined;
  98. },
  99. },
  100. /**
  101. * The loaded components.
  102. *
  103. * @memberof PntsLoader.prototype
  104. *
  105. * @type {ModelComponents.Components}
  106. * @readonly
  107. * @private
  108. */
  109. components: {
  110. get: function () {
  111. return this._components;
  112. },
  113. },
  114. /**
  115. * A world-space transform to apply to the primitives.
  116. * See {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/PointCloud#global-semantics}
  117. *
  118. * @memberof PntsLoader.prototype
  119. *
  120. * @type {Matrix4}
  121. * @readonly
  122. * @private
  123. */
  124. transform: {
  125. get: function () {
  126. return this._transform;
  127. },
  128. },
  129. });
  130. /**
  131. * Loads the resource.
  132. * @private
  133. */
  134. PntsLoader.prototype.load = function () {
  135. this._parsedContent = PntsParser.parse(this._arrayBuffer, this._byteOffset);
  136. this._state = ResourceLoaderState.PROCESSING;
  137. };
  138. PntsLoader.prototype.process = function (frameState) {
  139. if (this._state === ResourceLoaderState.PROCESSING) {
  140. if (!defined(this._decodePromise)) {
  141. decodeDraco(this, frameState.context);
  142. }
  143. }
  144. };
  145. function decodeDraco(loader, context) {
  146. const parsedContent = loader._parsedContent;
  147. const draco = parsedContent.draco;
  148. let decodePromise;
  149. if (!defined(draco)) {
  150. // The draco extension wasn't present,
  151. decodePromise = Promise.resolve();
  152. } else {
  153. decodePromise = DracoLoader.decodePointCloud(draco, context);
  154. }
  155. if (!defined(decodePromise)) {
  156. // Could not schedule Draco decoding this frame.
  157. return;
  158. }
  159. loader._decodePromise = decodePromise;
  160. decodePromise
  161. .then(function (decodeDracoResult) {
  162. if (loader.isDestroyed()) {
  163. return;
  164. }
  165. if (defined(decodeDracoResult)) {
  166. processDracoAttributes(loader, draco, decodeDracoResult);
  167. }
  168. makeComponents(loader, context);
  169. loader._state = ResourceLoaderState.READY;
  170. loader._promise.resolve(loader);
  171. })
  172. .catch(function (error) {
  173. loader.unload();
  174. loader._state = ResourceLoaderState.FAILED;
  175. const errorMessage = "Failed to load Draco";
  176. loader._promise.reject(loader.getError(errorMessage, error));
  177. });
  178. }
  179. function processDracoAttributes(loader, draco, result) {
  180. loader._state = ResourceLoaderState.READY;
  181. const parsedContent = loader._parsedContent;
  182. let attribute;
  183. if (defined(result.POSITION)) {
  184. attribute = {
  185. name: "POSITION",
  186. semantic: VertexAttributeSemantic.POSITION,
  187. typedArray: result.POSITION.array,
  188. componentDatatype: ComponentDatatype.FLOAT,
  189. type: AttributeType.VEC3,
  190. isQuantized: false,
  191. };
  192. if (defined(result.POSITION.data.quantization)) {
  193. // Draco quantization range == quantized volume scale - size in meters of the quantized volume
  194. // Internal quantized range is the range of values of the quantized data, e.g. 255 for 8-bit, 1023 for 10-bit, etc
  195. const quantization = result.POSITION.data.quantization;
  196. const range = quantization.range;
  197. const quantizedVolumeScale = Cartesian3.fromElements(range, range, range);
  198. const quantizedVolumeOffset = Cartesian3.unpack(quantization.minValues);
  199. const quantizedRange = (1 << quantization.quantizationBits) - 1.0;
  200. attribute.isQuantized = true;
  201. attribute.quantizedRange = quantizedRange;
  202. attribute.quantizedVolumeOffset = quantizedVolumeOffset;
  203. attribute.quantizedVolumeScale = quantizedVolumeScale;
  204. attribute.quantizedComponentDatatype = ComponentDatatype.UNSIGNED_SHORT;
  205. attribute.quantizedType = AttributeType.VEC3;
  206. }
  207. parsedContent.positions = attribute;
  208. }
  209. if (defined(result.NORMAL)) {
  210. attribute = {
  211. name: "NORMAL",
  212. semantic: VertexAttributeSemantic.NORMAL,
  213. typedArray: result.NORMAL.array,
  214. componentDatatype: ComponentDatatype.FLOAT,
  215. type: AttributeType.VEC3,
  216. isQuantized: false,
  217. octEncoded: false,
  218. octEncodedZXY: false,
  219. };
  220. if (defined(result.NORMAL.data.quantization)) {
  221. const octEncodedRange =
  222. (1 << result.NORMAL.data.quantization.quantizationBits) - 1.0;
  223. attribute.quantizedRange = octEncodedRange;
  224. attribute.octEncoded = true;
  225. attribute.octEncodedZXY = true;
  226. attribute.quantizedComponentDatatype = ComponentDatatype.UNSIGNED_BYTE;
  227. attribute.quantizedType = AttributeType.VEC2;
  228. }
  229. parsedContent.normals = attribute;
  230. }
  231. if (defined(result.RGBA)) {
  232. parsedContent.colors = {
  233. name: "COLOR",
  234. semantic: VertexAttributeSemantic.COLOR,
  235. setIndex: 0,
  236. typedArray: result.RGBA.array,
  237. componentDatatype: ComponentDatatype.UNSIGNED_BYTE,
  238. type: AttributeType.VEC4,
  239. normalized: true,
  240. isTranslucent: true,
  241. };
  242. } else if (defined(result.RGB)) {
  243. parsedContent.colors = {
  244. name: "COLOR",
  245. semantic: VertexAttributeSemantic.COLOR,
  246. setIndex: 0,
  247. typedArray: result.RGB.array,
  248. componentDatatype: ComponentDatatype.UNSIGNED_BYTE,
  249. type: AttributeType.VEC3,
  250. normalized: true,
  251. isTranslucent: false,
  252. };
  253. }
  254. // Transcode Batch ID (3D Tiles 1.0) -> Feature ID (3D Tiles Next)
  255. if (defined(result.BATCH_ID)) {
  256. const batchIds = result.BATCH_ID.array;
  257. parsedContent.batchIds = {
  258. name: "_FEATURE_ID",
  259. semantic: VertexAttributeSemantic.FEATURE_ID,
  260. setIndex: 0,
  261. typedArray: batchIds,
  262. componentDatatype: ComponentDatatype.fromTypedArray(batchIds),
  263. type: AttributeType.SCALAR,
  264. };
  265. }
  266. let styleableProperties = parsedContent.styleableProperties;
  267. const batchTableProperties = draco.batchTableProperties;
  268. for (const name in batchTableProperties) {
  269. if (batchTableProperties.hasOwnProperty(name)) {
  270. const property = result[name];
  271. if (!defined(styleableProperties)) {
  272. styleableProperties = {};
  273. }
  274. styleableProperties[name] = {
  275. typedArray: property.array,
  276. componentCount: property.data.componentsPerAttribute,
  277. };
  278. }
  279. }
  280. parsedContent.styleableProperties = styleableProperties;
  281. }
  282. function makeAttribute(loader, attributeInfo, context) {
  283. let typedArray = attributeInfo.typedArray;
  284. let quantization;
  285. if (attributeInfo.octEncoded) {
  286. quantization = new Quantization();
  287. quantization.octEncoded = attributeInfo.octEncoded;
  288. quantization.octEncodedZXY = attributeInfo.octEncodedZXY;
  289. quantization.normalizationRange = attributeInfo.quantizedRange;
  290. quantization.type = attributeInfo.quantizedType;
  291. quantization.componentDatatype = attributeInfo.quantizedComponentDatatype;
  292. }
  293. if (attributeInfo.isQuantized) {
  294. quantization = new Quantization();
  295. const normalizationRange = attributeInfo.quantizedRange;
  296. quantization.normalizationRange = normalizationRange;
  297. // volume offset sometimes requires 64-bit precision so this is handled
  298. // in the components.transform matrix.
  299. quantization.quantizedVolumeOffset = Cartesian3.ZERO;
  300. const quantizedVolumeDimensions = attributeInfo.quantizedVolumeScale;
  301. quantization.quantizedVolumeDimensions = quantizedVolumeDimensions;
  302. quantization.quantizedVolumeStepSize = Cartesian3.divideByScalar(
  303. quantizedVolumeDimensions,
  304. normalizationRange,
  305. new Cartesian3()
  306. );
  307. quantization.componentDatatype = attributeInfo.quantizedComponentDatatype;
  308. quantization.type = attributeInfo.quantizedType;
  309. }
  310. const attribute = new Attribute();
  311. attribute.name = attributeInfo.name;
  312. attribute.semantic = attributeInfo.semantic;
  313. attribute.setIndex = attributeInfo.setIndex;
  314. attribute.componentDatatype = attributeInfo.componentDatatype;
  315. attribute.type = attributeInfo.type;
  316. attribute.normalized = defaultValue(attributeInfo.normalized, false);
  317. attribute.min = attributeInfo.min;
  318. attribute.max = attributeInfo.max;
  319. attribute.quantization = quantization;
  320. if (attributeInfo.isRGB565) {
  321. typedArray = AttributeCompression.decodeRGB565(typedArray);
  322. }
  323. if (defined(attributeInfo.constantColor)) {
  324. const packedColor = new Array(4);
  325. attribute.constant = Color.pack(attributeInfo.constantColor, packedColor);
  326. } else {
  327. const buffer = Buffer.createVertexBuffer({
  328. typedArray: typedArray,
  329. context: context,
  330. usage: BufferUsage.STATIC_DRAW,
  331. });
  332. buffer.vertexArrayDestroyable = false;
  333. loader._buffers.push(buffer);
  334. attribute.buffer = buffer;
  335. }
  336. return attribute;
  337. }
  338. let randomNumberGenerator;
  339. let randomValues;
  340. function getRandomValues(samplesLength) {
  341. // Use same random values across all runs
  342. if (!defined(randomValues)) {
  343. // Use MersenneTwister directly to avoid interfering with CesiumMath.nextRandomNumber()
  344. // See https://github.com/CesiumGS/cesium/issues/9730
  345. randomNumberGenerator = new MersenneTwister(0);
  346. randomValues = new Array(samplesLength);
  347. for (let i = 0; i < samplesLength; ++i) {
  348. randomValues[i] = randomNumberGenerator.random();
  349. }
  350. }
  351. return randomValues;
  352. }
  353. const scratchMin = new Cartesian3();
  354. const scratchMax = new Cartesian3();
  355. const scratchPosition = new Cartesian3();
  356. function computeApproximateExtrema(positions) {
  357. const positionsArray = positions.typedArray;
  358. const maximumSamplesLength = 20;
  359. const pointsLength = positionsArray.length / 3;
  360. const samplesLength = Math.min(pointsLength, maximumSamplesLength);
  361. const randomValues = getRandomValues(maximumSamplesLength);
  362. const maxValue = Number.MAX_VALUE;
  363. const minValue = -Number.MAX_VALUE;
  364. let min = Cartesian3.fromElements(maxValue, maxValue, maxValue, scratchMin);
  365. let max = Cartesian3.fromElements(minValue, minValue, minValue, scratchMax);
  366. let i;
  367. let index;
  368. let position;
  369. if (positions.isQuantized) {
  370. // The quantized volume offset is not used here since it will become part of
  371. // the model matrix.
  372. min = Cartesian3.ZERO;
  373. max = positions.quantizedVolumeScale;
  374. } else {
  375. for (i = 0; i < samplesLength; ++i) {
  376. index = Math.floor(randomValues[i] * pointsLength);
  377. position = Cartesian3.unpack(positionsArray, index * 3, scratchPosition);
  378. Cartesian3.minimumByComponent(min, position, min);
  379. Cartesian3.maximumByComponent(max, position, max);
  380. }
  381. }
  382. positions.min = Cartesian3.clone(min);
  383. positions.max = Cartesian3.clone(max);
  384. }
  385. // By default, point clouds are rendered as dark gray.
  386. const defaultColorAttribute = {
  387. name: VertexAttributeSemantic.COLOR,
  388. semantic: VertexAttributeSemantic.COLOR,
  389. setIndex: 0,
  390. constantColor: Color.DARKGRAY,
  391. componentDatatype: ComponentDatatype.FLOAT,
  392. type: AttributeType.VEC4,
  393. isQuantized: false,
  394. isTranslucent: false,
  395. };
  396. function makeAttributes(loader, parsedContent, context) {
  397. const attributes = [];
  398. let attribute;
  399. const positions = parsedContent.positions;
  400. if (defined(positions)) {
  401. computeApproximateExtrema(positions);
  402. attribute = makeAttribute(loader, positions, context);
  403. attribute.count = parsedContent.pointsLength;
  404. attributes.push(attribute);
  405. }
  406. if (defined(parsedContent.normals)) {
  407. attribute = makeAttribute(loader, parsedContent.normals, context);
  408. attributes.push(attribute);
  409. }
  410. if (defined(parsedContent.colors)) {
  411. attribute = makeAttribute(loader, parsedContent.colors, context);
  412. attributes.push(attribute);
  413. } else {
  414. attribute = makeAttribute(loader, defaultColorAttribute, context);
  415. attributes.push(attribute);
  416. }
  417. if (defined(parsedContent.batchIds)) {
  418. attribute = makeAttribute(loader, parsedContent.batchIds, context);
  419. attributes.push(attribute);
  420. }
  421. return attributes;
  422. }
  423. function makeStructuralMetadata(parsedContent) {
  424. const batchLength = parsedContent.batchLength;
  425. const pointsLength = parsedContent.pointsLength;
  426. const batchTableBinary = parsedContent.batchTableBinary;
  427. if (defined(batchTableBinary)) {
  428. const count = defaultValue(batchLength, pointsLength);
  429. return parseBatchTable({
  430. count: count,
  431. batchTable: parsedContent.batchTableJson,
  432. binaryBody: batchTableBinary,
  433. });
  434. }
  435. // If batch table is not defined, create a property table without any properties.
  436. const emptyPropertyTable = new PropertyTable({
  437. name: MetadataClass.BATCH_TABLE_CLASS_NAME,
  438. count: pointsLength,
  439. });
  440. return new StructuralMetadata({
  441. schema: {},
  442. propertyTables: [emptyPropertyTable],
  443. });
  444. }
  445. function makeComponents(loader, context) {
  446. const parsedContent = loader._parsedContent;
  447. const metallicRoughness = new MetallicRoughness();
  448. metallicRoughness.metallicFactor = 0;
  449. metallicRoughness.roughnessFactor = 0.9;
  450. const material = new Material();
  451. material.metallicRoughness = metallicRoughness;
  452. const colors = parsedContent.colors;
  453. if (defined(colors) && colors.isTranslucent) {
  454. material.alphaMode = AlphaMode.BLEND;
  455. }
  456. // Render point clouds as unlit, unless normals are present, in which case
  457. // render as a PBR material.
  458. const isUnlit = !defined(parsedContent.normals);
  459. material.unlit = isUnlit;
  460. const primitive = new Primitive();
  461. primitive.attributes = makeAttributes(loader, parsedContent, context);
  462. primitive.primitiveType = PrimitiveType.POINTS;
  463. primitive.material = material;
  464. if (defined(parsedContent.batchIds)) {
  465. const featureIdAttribute = new FeatureIdAttribute();
  466. featureIdAttribute.propertyTableId = 0;
  467. featureIdAttribute.setIndex = 0;
  468. featureIdAttribute.positionalLabel = "featureId_0";
  469. primitive.featureIds.push(featureIdAttribute);
  470. }
  471. const node = new Node();
  472. node.index = 0;
  473. node.primitives = [primitive];
  474. const scene = new Scene();
  475. scene.nodes = [node];
  476. scene.upAxis = Axis.Z;
  477. scene.forwardAxis = Axis.X;
  478. const components = new Components();
  479. components.scene = scene;
  480. components.nodes = [node];
  481. components.structuralMetadata = makeStructuralMetadata(parsedContent);
  482. if (defined(parsedContent.rtcCenter)) {
  483. components.transform = Matrix4.multiplyByTranslation(
  484. components.transform,
  485. parsedContent.rtcCenter,
  486. components.transform
  487. );
  488. }
  489. const positions = parsedContent.positions;
  490. if (defined(positions) && positions.isQuantized) {
  491. // The volume offset is sometimes in ECEF, so this is applied here rather
  492. // than the dequantization shader to avoid jitter
  493. components.transform = Matrix4.multiplyByTranslation(
  494. components.transform,
  495. positions.quantizedVolumeOffset,
  496. components.transform
  497. );
  498. }
  499. loader._components = components;
  500. // Free the parsed content so we don't hold onto the large typed arrays.
  501. loader._parsedContent = undefined;
  502. }
  503. PntsLoader.prototype.unload = function () {
  504. const buffers = this._buffers;
  505. for (let i = 0; i < buffers.length; i++) {
  506. buffers[i].destroy();
  507. }
  508. buffers.length = 0;
  509. this._components = undefined;
  510. this._parsedContent = undefined;
  511. };