智慧水务管理系统 - 精河县供水工程综合管理平台

parseStructuralMetadata.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. import Check from "../Core/Check.js";
  2. import Frozen from "../Core/Frozen.js";
  3. import defined from "../Core/defined.js";
  4. import PropertyTable from "./PropertyTable.js";
  5. import PropertyTexture from "./PropertyTexture.js";
  6. import PropertyAttribute from "./PropertyAttribute.js";
  7. import StructuralMetadata from "./StructuralMetadata.js";
  8. import MetadataTable from "./MetadataTable.js";
  9. import Sampler from "../Renderer/Sampler.js";
  10. import Texture from "../Renderer/Texture.js";
  11. import PixelFormat from "../Core/PixelFormat.js";
  12. import PixelDatatype from "../Renderer/PixelDatatype.js";
  13. import RuntimeError from "../Core/RuntimeError.js";
  14. import oneTimeWarning from "../Core/oneTimeWarning.js";
  15. import TextureWrap from "../Renderer/TextureWrap.js";
  16. import TextureMagnificationFilter from "../Renderer/TextureMagnificationFilter.js";
  17. import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js";
  18. import ContextLimits from "../Renderer/ContextLimits.js";
  19. import MetadataComponentType from "./MetadataComponentType.js";
  20. import MetadataType from "./MetadataType.js";
  21. /**
  22. * Parse the <code>EXT_structural_metadata</code> glTF extension to create a
  23. * structural metadata object.
  24. *
  25. * @param {object} options Object with the following properties:
  26. * @param {object} options.extension The extension JSON object.
  27. * @param {MetadataSchema} options.schema The parsed schema.
  28. * @param {Object<string, Uint8Array>} [options.bufferViews] An object mapping bufferView IDs to Uint8Array objects.
  29. * @param {Object<string, Texture>} [options.textures] An object mapping texture IDs to {@link Texture} objects.
  30. * @param {Context} [options.context] The current rendering context.
  31. * @return {StructuralMetadata} A structural metadata object
  32. * @private
  33. * @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
  34. */
  35. function parseStructuralMetadata(options) {
  36. options = options ?? Frozen.EMPTY_OBJECT;
  37. const extension = options.extension;
  38. // The calling code is responsible for loading the schema.
  39. // This keeps metadata parsing synchronous.
  40. const schema = options.schema;
  41. //>>includeStart('debug', pragmas.debug);
  42. Check.typeOf.object("options.extension", extension);
  43. Check.typeOf.object("options.schema", schema);
  44. //>>includeEnd('debug');
  45. const propertyTables = [];
  46. if (defined(extension.propertyTables)) {
  47. for (let i = 0; i < extension.propertyTables.length; i++) {
  48. const propertyTable = extension.propertyTables[i];
  49. const classDefinition = schema.classes[propertyTable.class];
  50. const propertyTableTexture = createTextureForPropertyTable(
  51. propertyTable,
  52. options.bufferViews,
  53. classDefinition,
  54. options.context,
  55. );
  56. const metadataTable = new MetadataTable({
  57. count: propertyTable.count,
  58. properties: propertyTable.properties,
  59. class: classDefinition,
  60. bufferViews: options.bufferViews,
  61. });
  62. propertyTables.push(
  63. new PropertyTable({
  64. id: i,
  65. name: propertyTable.name,
  66. count: propertyTable.count,
  67. metadataTable: metadataTable,
  68. extras: propertyTable.extras,
  69. extensions: propertyTable.extensions,
  70. texture: propertyTableTexture,
  71. }),
  72. );
  73. }
  74. }
  75. const propertyTextures = [];
  76. if (defined(extension.propertyTextures)) {
  77. for (let i = 0; i < extension.propertyTextures.length; i++) {
  78. const propertyTexture = extension.propertyTextures[i];
  79. propertyTextures.push(
  80. new PropertyTexture({
  81. id: i,
  82. name: propertyTexture.name,
  83. propertyTexture: propertyTexture,
  84. class: schema.classes[propertyTexture.class],
  85. textures: options.textures,
  86. }),
  87. );
  88. }
  89. }
  90. const propertyAttributes = [];
  91. if (defined(extension.propertyAttributes)) {
  92. for (let i = 0; i < extension.propertyAttributes.length; i++) {
  93. const propertyAttribute = extension.propertyAttributes[i];
  94. propertyAttributes.push(
  95. new PropertyAttribute({
  96. id: i,
  97. name: propertyAttribute.name,
  98. class: schema.classes[propertyAttribute.class],
  99. propertyAttribute: propertyAttribute,
  100. }),
  101. );
  102. }
  103. }
  104. return new StructuralMetadata({
  105. schema: schema,
  106. propertyTables: propertyTables,
  107. propertyTextures: propertyTextures,
  108. propertyAttributes: propertyAttributes,
  109. statistics: extension.statistics,
  110. extras: extension.extras,
  111. extensions: extension.extensions,
  112. });
  113. }
  114. // Always use four channels for property table textures.
  115. const NUM_CHANNELS = 4;
  116. /**
  117. * Creates a texture from a set of property table properties (those which are GPU compatible).
  118. * Each row of the texture is a property, with each column corresponding to a given feature.
  119. *
  120. * @param {PropertyTable} propertyTable The property table.
  121. * @param {Object<string, Uint8Array>} bufferViews An object mapping bufferView IDs to Uint8Array objects for the given property table.
  122. * @param {MetadataClass} classDefinition Class defined in the schema
  123. * @param {Context} context The rendering context.
  124. * @returns {Texture|undefined} The created texture, or <code>undefined</code> if no properties are GPU compatible.
  125. *
  126. * @private
  127. */
  128. function createTextureForPropertyTable(
  129. propertyTable,
  130. bufferViews,
  131. classDefinition,
  132. context,
  133. ) {
  134. const properties = propertyTable.properties;
  135. if (!defined(properties)) {
  136. return undefined;
  137. }
  138. const numFeatures = propertyTable.count;
  139. let gpuCompatiblePropertyInfo;
  140. try {
  141. gpuCompatiblePropertyInfo = collectGpuCompatiblePropertyInfo(
  142. properties,
  143. bufferViews,
  144. classDefinition,
  145. numFeatures,
  146. );
  147. } catch (error) {
  148. console.warn(
  149. `Failed to create texture for property table "${propertyTable.name}": ${error.message}`,
  150. );
  151. return undefined;
  152. }
  153. const numGpuCompatibleProperties = gpuCompatiblePropertyInfo.length;
  154. if (numGpuCompatibleProperties === 0) {
  155. return undefined;
  156. }
  157. // In the future, we could use multiple textures if we would exceed the maximum texture size.
  158. if (
  159. numFeatures > ContextLimits.maximumTextureSize ||
  160. numGpuCompatibleProperties > ContextLimits.maximumTextureSize
  161. ) {
  162. oneTimeWarning(
  163. "PropertyTableTextureExceedsMaximumSize",
  164. `Cannot create a texture for the property table "${propertyTable.name}" because it exceeds the maximum texture size of ${ContextLimits.maximumTextureSize}.`,
  165. );
  166. return undefined;
  167. }
  168. const packedBufferView = packPropertyTablePropertiesIntoRGBA8(
  169. gpuCompatiblePropertyInfo,
  170. numFeatures,
  171. );
  172. // Create a sampler fit for sampling raw data without mipmapping / filtering etc.
  173. const sampler = new Sampler({
  174. wrapS: TextureWrap.CLAMP_TO_EDGE,
  175. wrapT: TextureWrap.CLAMP_TO_EDGE,
  176. minificationFilter: TextureMinificationFilter.NEAREST,
  177. magnificationFilter: TextureMagnificationFilter.NEAREST,
  178. });
  179. return Texture.create({
  180. context: context,
  181. pixelFormat: PixelFormat.RGBA,
  182. pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
  183. sampler: sampler,
  184. flipY: false,
  185. source: {
  186. width: numFeatures,
  187. height: numGpuCompatibleProperties,
  188. arrayBufferView: packedBufferView,
  189. },
  190. });
  191. }
  192. function collectGpuCompatiblePropertyInfo(
  193. properties,
  194. bufferViews,
  195. classDefinition,
  196. numFeatures,
  197. ) {
  198. const propertyInfos = [];
  199. const classProperties = classDefinition.properties;
  200. // It's possible for a primitive in a tileset to only use a subset of the class properties defined in the schema.
  201. // For instance, the Design Tiler merges classes together by default, and omits properties in primitives that aren't used.
  202. // To make the default values available to the GPU, and to avoid compiling different versions of the shader for each primitive,
  203. // we iterate over _all_ class properties here - not just the properties in the property table.
  204. for (const [propertyId, classProperty] of Object.entries(classProperties)) {
  205. // Certain properties like strings, dynamic-sized arrays, and 64-bit types cannot be represented natively on the GPU.
  206. if (!classProperty.isGpuCompatible(NUM_CHANNELS)) {
  207. continue;
  208. }
  209. const property = properties[propertyId];
  210. const bufferView = defined(property)
  211. ? bufferViews[property.values]
  212. : createNoDataBufferView(classProperty, numFeatures);
  213. const bufferViewLength = bufferView.length;
  214. const bytesPerElement = classProperty.cpuBytesPerElement();
  215. const numBufferElements = bufferViewLength / bytesPerElement;
  216. if (numBufferElements !== numFeatures) {
  217. throw new RuntimeError(
  218. `Property with ID: "${propertyId}" has (${numBufferElements}), which does not match number of features in the property table: (${numFeatures}).`,
  219. );
  220. }
  221. propertyInfos.push({
  222. view: bufferView,
  223. classProperty: classProperty,
  224. });
  225. }
  226. return propertyInfos;
  227. }
  228. /**
  229. * When a property is part of a tileset class schema but not used in a property table,
  230. * we create a buffer view filled with the property's noData value.
  231. *
  232. * @param {MetadataClassProperty} classProperty The class property definition.
  233. * @param {number} numFeatures The number of features in the property table.
  234. *
  235. * @returns {Uint8Array} A buffer view filled with the property's noData value.
  236. *
  237. * @private
  238. */
  239. function createNoDataBufferView(classProperty, numFeatures) {
  240. // noData can be a number, an array of numbers (e.g. for vecN types), or a nested array of numbers (e.g. for arrays of vecN types).
  241. let noData = classProperty.noData;
  242. const metadataComponentCount = MetadataType.getComponentCount(
  243. classProperty.type,
  244. );
  245. const metadataArrayLength = classProperty.isArray
  246. ? classProperty.arrayLength
  247. : 1;
  248. // Special case: noData enum values are specified as strings, so we need to convert them to numbers here.
  249. if (classProperty.type === MetadataType.ENUM) {
  250. const enumDefinition = classProperty.enumType;
  251. noData = enumDefinition.valuesByName[noData];
  252. }
  253. // Wrap noData in an array (up to two times) so we can treat it uniformly in the loop below.
  254. if (metadataComponentCount === 1) {
  255. noData = [noData];
  256. }
  257. if (metadataArrayLength === 1) {
  258. noData = [noData];
  259. }
  260. const bytesPerElement = classProperty.cpuBytesPerElement();
  261. const bytesPerComponent = MetadataComponentType.getSizeInBytes(
  262. classProperty.valueType,
  263. );
  264. const buffer = new ArrayBuffer(bytesPerElement * numFeatures);
  265. const view = new DataView(buffer);
  266. const accessors = MetadataComponentType.getDataViewAccessors(
  267. view,
  268. classProperty.valueType,
  269. );
  270. for (let i = 0; i < numFeatures; i++) {
  271. for (let j = 0; j < metadataArrayLength; j++) {
  272. for (let k = 0; k < metadataComponentCount; k++) {
  273. const componentIdx = j * metadataComponentCount + k;
  274. accessors.set(
  275. bytesPerElement * i + componentIdx * bytesPerComponent,
  276. noData[j][k],
  277. );
  278. }
  279. }
  280. }
  281. return new Uint8Array(buffer);
  282. }
  283. // Make one big buffer view to load into the texture
  284. // Since each texel is always 4 bytes (RGBA8 format), elements less than 4 bytes need to be padded (respecting little-endian order).
  285. // Exception: single-component 64-bit types can be downcast to 32-bit for GPU compatibility (with potential loss of precision / range).
  286. function packPropertyTablePropertiesIntoRGBA8(propertyInfos, numFeatures) {
  287. const numGpuCompatibleProperties = propertyInfos.length;
  288. const packedBufferView = new Uint8Array(
  289. numGpuCompatibleProperties * numFeatures * NUM_CHANNELS,
  290. );
  291. const packedDataView = new DataView(
  292. packedBufferView.buffer,
  293. packedBufferView.byteOffset,
  294. packedBufferView.byteLength,
  295. );
  296. for (
  297. let propertyIndex = 0;
  298. propertyIndex < numGpuCompatibleProperties;
  299. propertyIndex++
  300. ) {
  301. const propertyInfo = propertyInfos[propertyIndex];
  302. const classProperty = propertyInfo.classProperty;
  303. const rowOffset = propertyIndex * numFeatures * NUM_CHANNELS;
  304. const sourceType = classProperty.valueType;
  305. const packedType = MetadataComponentType.gpuComponentType(sourceType);
  306. // E.g. When the source component type is INT64, we first downcast each element to INT32 before packing into the GPU buffer.
  307. if (sourceType !== packedType) {
  308. downcastAndPackProperty(propertyInfo, packedDataView, rowOffset);
  309. continue;
  310. }
  311. packProperty(propertyInfo, packedBufferView, rowOffset);
  312. }
  313. return packedBufferView;
  314. }
  315. function packProperty(propertyInfo, packedBufferView, rowOffset) {
  316. const bufferView = propertyInfo.view;
  317. const bytesPerElement = propertyInfo.classProperty.cpuBytesPerElement();
  318. const numElements = bufferView.length / bytesPerElement;
  319. for (let elementIndex = 0; elementIndex < numElements; elementIndex++) {
  320. const sourceOffset = elementIndex * bytesPerElement;
  321. const destinationOffset = rowOffset + elementIndex * NUM_CHANNELS;
  322. packedBufferView.set(
  323. bufferView.subarray(sourceOffset, sourceOffset + bytesPerElement),
  324. destinationOffset,
  325. );
  326. }
  327. }
  328. // This is the slow path - rather than doing a straight copy, we need to interpret and downcast each element before packing it into the GPU buffer.
  329. // Note: this function does not handle properties with multiple components per element (or arrays). While not complete, this is OK because
  330. // multi-component properties with 64-bit types - even when downcast to 32 bits - cannot fit into a single RGBA8 texel.
  331. function downcastAndPackProperty(propertyInfo, packedDataView, rowOffset) {
  332. const classProperty = propertyInfo.classProperty;
  333. const bufferView = propertyInfo.view;
  334. const sourceType = classProperty.valueType;
  335. const packedType = MetadataComponentType.gpuComponentType(sourceType);
  336. const bytesPerElement = classProperty.cpuBytesPerElement();
  337. const numElements = bufferView.length / bytesPerElement;
  338. const sourceDataView = new DataView(
  339. bufferView.buffer,
  340. bufferView.byteOffset,
  341. bufferView.byteLength,
  342. );
  343. const sourceAccessors = MetadataComponentType.getDataViewAccessors(
  344. sourceDataView,
  345. sourceType,
  346. );
  347. const packedAccessors = MetadataComponentType.getDataViewAccessors(
  348. packedDataView,
  349. packedType,
  350. );
  351. const downcastFunction = MetadataComponentType.downcastFunction(sourceType);
  352. for (let elementIndex = 0; elementIndex < numElements; elementIndex++) {
  353. const sourceElementOffset = elementIndex * bytesPerElement;
  354. const destinationElementOffset = rowOffset + elementIndex * NUM_CHANNELS;
  355. const value = sourceAccessors.get(sourceElementOffset);
  356. packedAccessors.set(destinationElementOffset, downcastFunction(value));
  357. }
  358. }
  359. export default parseStructuralMetadata;