BatchTable.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. import Cartesian2 from '../Core/Cartesian2.js';
  2. import Cartesian3 from '../Core/Cartesian3.js';
  3. import Cartesian4 from '../Core/Cartesian4.js';
  4. import combine from '../Core/combine.js';
  5. import ComponentDatatype from '../Core/ComponentDatatype.js';
  6. import defined from '../Core/defined.js';
  7. import defineProperties from '../Core/defineProperties.js';
  8. import destroyObject from '../Core/destroyObject.js';
  9. import DeveloperError from '../Core/DeveloperError.js';
  10. import PixelFormat from '../Core/PixelFormat.js';
  11. import ContextLimits from '../Renderer/ContextLimits.js';
  12. import PixelDatatype from '../Renderer/PixelDatatype.js';
  13. import Sampler from '../Renderer/Sampler.js';
  14. import Texture from '../Renderer/Texture.js';
  15. import TextureMagnificationFilter from '../Renderer/TextureMagnificationFilter.js';
  16. import TextureMinificationFilter from '../Renderer/TextureMinificationFilter.js';
  17. /**
  18. * Creates a texture to look up per instance attributes for batched primitives. For example, store each primitive's pick color in the texture.
  19. *
  20. * @alias BatchTable
  21. * @constructor
  22. * @private
  23. *
  24. * @param {Context} context The context in which the batch table is created.
  25. * @param {Object[]} attributes An array of objects describing a per instance attribute. Each object contains a datatype, components per attributes, whether it is normalized and a function name
  26. * to retrieve the value in the vertex shader.
  27. * @param {Number} numberOfInstances The number of instances in a batch table.
  28. *
  29. * @example
  30. * // create the batch table
  31. * var attributes = [{
  32. * functionName : 'getShow',
  33. * componentDatatype : ComponentDatatype.UNSIGNED_BYTE,
  34. * componentsPerAttribute : 1
  35. * }, {
  36. * functionName : 'getPickColor',
  37. * componentDatatype : ComponentDatatype.UNSIGNED_BYTE,
  38. * componentsPerAttribute : 4,
  39. * normalize : true
  40. * }];
  41. * var batchTable = new BatchTable(context, attributes, 5);
  42. *
  43. * // when creating the draw commands, update the uniform map and the vertex shader
  44. * vertexShaderSource = batchTable.getVertexShaderCallback()(vertexShaderSource);
  45. * var shaderProgram = ShaderProgram.fromCache({
  46. * // ...
  47. * vertexShaderSource : vertexShaderSource,
  48. * });
  49. *
  50. * drawCommand.shaderProgram = shaderProgram;
  51. * drawCommand.uniformMap = batchTable.getUniformMapCallback()(uniformMap);
  52. *
  53. * // use the attribute function names in the shader to retrieve the instance values
  54. * // ...
  55. * attribute float batchId;
  56. *
  57. * void main() {
  58. * // ...
  59. * float show = getShow(batchId);
  60. * vec3 pickColor = getPickColor(batchId);
  61. * // ...
  62. * }
  63. */
  64. function BatchTable(context, attributes, numberOfInstances) {
  65. //>>includeStart('debug', pragmas.debug);
  66. if (!defined(context)) {
  67. throw new DeveloperError('context is required');
  68. }
  69. if (!defined(attributes)) {
  70. throw new DeveloperError('attributes is required');
  71. }
  72. if (!defined(numberOfInstances)) {
  73. throw new DeveloperError('numberOfInstances is required');
  74. }
  75. //>>includeEnd('debug');
  76. this._attributes = attributes;
  77. this._numberOfInstances = numberOfInstances;
  78. if (attributes.length === 0) {
  79. return;
  80. }
  81. // PERFORMANCE_IDEA: We may be able to arrange the attributes so they can be packing into fewer texels.
  82. // Right now, an attribute with one component uses an entire texel when 4 single component attributes can
  83. // be packed into a texel.
  84. //
  85. // Packing floats into unsigned byte textures makes the problem worse. A single component float attribute
  86. // will be packed into a single texel leaving 3 texels unused. 4 texels are reserved for each float attribute
  87. // regardless of how many components it has.
  88. var pixelDatatype = getDatatype(attributes);
  89. var textureFloatSupported = context.floatingPointTexture;
  90. var packFloats = pixelDatatype === PixelDatatype.FLOAT && !textureFloatSupported;
  91. var offsets = createOffsets(attributes, packFloats);
  92. var stride = getStride(offsets, attributes, packFloats);
  93. var maxNumberOfInstancesPerRow = Math.floor(ContextLimits.maximumTextureSize / stride);
  94. var instancesPerWidth = Math.min(numberOfInstances, maxNumberOfInstancesPerRow);
  95. var width = stride * instancesPerWidth;
  96. var height = Math.ceil(numberOfInstances / instancesPerWidth);
  97. var stepX = 1.0 / width;
  98. var centerX = stepX * 0.5;
  99. var stepY = 1.0 / height;
  100. var centerY = stepY * 0.5;
  101. this._textureDimensions = new Cartesian2(width, height);
  102. this._textureStep = new Cartesian4(stepX, centerX, stepY, centerY);
  103. this._pixelDatatype = !packFloats ? pixelDatatype : PixelDatatype.UNSIGNED_BYTE;
  104. this._packFloats = packFloats;
  105. this._offsets = offsets;
  106. this._stride = stride;
  107. this._texture = undefined;
  108. var batchLength = 4 * width * height;
  109. this._batchValues = pixelDatatype === PixelDatatype.FLOAT && !packFloats ? new Float32Array(batchLength) : new Uint8Array(batchLength);
  110. this._batchValuesDirty = false;
  111. }
  112. defineProperties(BatchTable.prototype, {
  113. /**
  114. * The attribute descriptions.
  115. * @memberOf BatchTable.prototype
  116. * @type {Object[]}
  117. * @readonly
  118. */
  119. attributes : {
  120. get : function() {
  121. return this._attributes;
  122. }
  123. },
  124. /**
  125. * The number of instances.
  126. * @memberOf BatchTable.prototype
  127. * @type {Number}
  128. * @readonly
  129. */
  130. numberOfInstances : {
  131. get : function () {
  132. return this._numberOfInstances;
  133. }
  134. }
  135. });
  136. function getDatatype(attributes) {
  137. var foundFloatDatatype = false;
  138. var length = attributes.length;
  139. for (var i = 0; i < length; ++i) {
  140. if (attributes[i].componentDatatype !== ComponentDatatype.UNSIGNED_BYTE) {
  141. foundFloatDatatype = true;
  142. break;
  143. }
  144. }
  145. return foundFloatDatatype ? PixelDatatype.FLOAT : PixelDatatype.UNSIGNED_BYTE;
  146. }
  147. function getAttributeType(attributes, attributeIndex) {
  148. var componentsPerAttribute = attributes[attributeIndex].componentsPerAttribute;
  149. if (componentsPerAttribute === 2) {
  150. return Cartesian2;
  151. } else if (componentsPerAttribute === 3) {
  152. return Cartesian3;
  153. } else if (componentsPerAttribute === 4) {
  154. return Cartesian4;
  155. }
  156. return Number;
  157. }
  158. function createOffsets(attributes, packFloats) {
  159. var offsets = new Array(attributes.length);
  160. var currentOffset = 0;
  161. var attributesLength = attributes.length;
  162. for (var i = 0; i < attributesLength; ++i) {
  163. var attribute = attributes[i];
  164. var componentDatatype = attribute.componentDatatype;
  165. offsets[i] = currentOffset;
  166. if (componentDatatype !== ComponentDatatype.UNSIGNED_BYTE && packFloats) {
  167. currentOffset += 4;
  168. } else {
  169. ++currentOffset;
  170. }
  171. }
  172. return offsets;
  173. }
  174. function getStride(offsets, attributes, packFloats) {
  175. var length = offsets.length;
  176. var lastOffset = offsets[length - 1];
  177. var lastAttribute = attributes[length - 1];
  178. var componentDatatype = lastAttribute.componentDatatype;
  179. if (componentDatatype !== ComponentDatatype.UNSIGNED_BYTE && packFloats) {
  180. return lastOffset + 4;
  181. }
  182. return lastOffset + 1;
  183. }
  184. var scratchPackedFloatCartesian4 = new Cartesian4();
  185. function getPackedFloat(array, index, result) {
  186. var packed = Cartesian4.unpack(array, index, scratchPackedFloatCartesian4);
  187. var x = Cartesian4.unpackFloat(packed);
  188. packed = Cartesian4.unpack(array, index + 4, scratchPackedFloatCartesian4);
  189. var y = Cartesian4.unpackFloat(packed);
  190. packed = Cartesian4.unpack(array, index + 8, scratchPackedFloatCartesian4);
  191. var z = Cartesian4.unpackFloat(packed);
  192. packed = Cartesian4.unpack(array, index + 12, scratchPackedFloatCartesian4);
  193. var w = Cartesian4.unpackFloat(packed);
  194. return Cartesian4.fromElements(x, y, z, w, result);
  195. }
  196. function setPackedAttribute(value, array, index) {
  197. var packed = Cartesian4.packFloat(value.x, scratchPackedFloatCartesian4);
  198. Cartesian4.pack(packed, array, index);
  199. packed = Cartesian4.packFloat(value.y, packed);
  200. Cartesian4.pack(packed, array, index + 4);
  201. packed = Cartesian4.packFloat(value.z, packed);
  202. Cartesian4.pack(packed, array, index + 8);
  203. packed = Cartesian4.packFloat(value.w, packed);
  204. Cartesian4.pack(packed, array, index + 12);
  205. }
  206. var scratchGetAttributeCartesian4 = new Cartesian4();
  207. /**
  208. * Gets the value of an attribute in the table.
  209. *
  210. * @param {Number} instanceIndex The index of the instance.
  211. * @param {Number} attributeIndex The index of the attribute.
  212. * @param {undefined|Cartesian2|Cartesian3|Cartesian4} [result] The object onto which to store the result. The type is dependent on the attribute's number of components.
  213. * @returns {Number|Cartesian2|Cartesian3|Cartesian4} The attribute value stored for the instance.
  214. *
  215. * @exception {DeveloperError} instanceIndex is out of range.
  216. * @exception {DeveloperError} attributeIndex is out of range.
  217. */
  218. BatchTable.prototype.getBatchedAttribute = function(instanceIndex, attributeIndex, result) {
  219. //>>includeStart('debug', pragmas.debug);
  220. if (instanceIndex < 0 || instanceIndex >= this._numberOfInstances) {
  221. throw new DeveloperError('instanceIndex is out of range.');
  222. }
  223. if (attributeIndex < 0 || attributeIndex >= this._attributes.length) {
  224. throw new DeveloperError('attributeIndex is out of range');
  225. }
  226. //>>includeEnd('debug');
  227. var attributes = this._attributes;
  228. var offset = this._offsets[attributeIndex];
  229. var stride = this._stride;
  230. var index = 4 * stride * instanceIndex + 4 * offset;
  231. var value;
  232. if (this._packFloats && attributes[attributeIndex].componentDatatype !== PixelDatatype.UNSIGNED_BYTE) {
  233. value = getPackedFloat(this._batchValues, index, scratchGetAttributeCartesian4);
  234. } else {
  235. value = Cartesian4.unpack(this._batchValues, index, scratchGetAttributeCartesian4);
  236. }
  237. var attributeType = getAttributeType(attributes, attributeIndex);
  238. if (defined(attributeType.fromCartesian4)) {
  239. return attributeType.fromCartesian4(value, result);
  240. } else if (defined(attributeType.clone)) {
  241. return attributeType.clone(value, result);
  242. }
  243. return value.x;
  244. };
  245. var setAttributeScratchValues = [undefined, undefined, new Cartesian2(), new Cartesian3(), new Cartesian4()];
  246. var setAttributeScratchCartesian4 = new Cartesian4();
  247. /**
  248. * Sets the value of an attribute in the table.
  249. *
  250. * @param {Number} instanceIndex The index of the instance.
  251. * @param {Number} attributeIndex The index of the attribute.
  252. * @param {Number|Cartesian2|Cartesian3|Cartesian4} value The value to be stored in the table. The type of value will depend on the number of components of the attribute.
  253. *
  254. * @exception {DeveloperError} instanceIndex is out of range.
  255. * @exception {DeveloperError} attributeIndex is out of range.
  256. */
  257. BatchTable.prototype.setBatchedAttribute = function(instanceIndex, attributeIndex, value) {
  258. //>>includeStart('debug', pragmas.debug);
  259. if (instanceIndex < 0 || instanceIndex >= this._numberOfInstances) {
  260. throw new DeveloperError('instanceIndex is out of range.');
  261. }
  262. if (attributeIndex < 0 || attributeIndex >= this._attributes.length) {
  263. throw new DeveloperError('attributeIndex is out of range');
  264. }
  265. if (!defined(value)) {
  266. throw new DeveloperError('value is required.');
  267. }
  268. //>>includeEnd('debug');
  269. var attributes = this._attributes;
  270. var result = setAttributeScratchValues[attributes[attributeIndex].componentsPerAttribute];
  271. var currentAttribute = this.getBatchedAttribute(instanceIndex, attributeIndex, result);
  272. var attributeType = getAttributeType(this._attributes, attributeIndex);
  273. var entriesEqual = defined(attributeType.equals) ? attributeType.equals(currentAttribute, value) : currentAttribute === value;
  274. if (entriesEqual) {
  275. return;
  276. }
  277. var attributeValue = setAttributeScratchCartesian4;
  278. attributeValue.x = defined(value.x) ? value.x : value;
  279. attributeValue.y = defined(value.y) ? value.y : 0.0;
  280. attributeValue.z = defined(value.z) ? value.z : 0.0;
  281. attributeValue.w = defined(value.w) ? value.w : 0.0;
  282. var offset = this._offsets[attributeIndex];
  283. var stride = this._stride;
  284. var index = 4 * stride * instanceIndex + 4 * offset;
  285. if (this._packFloats && attributes[attributeIndex].componentDatatype !== PixelDatatype.UNSIGNED_BYTE) {
  286. setPackedAttribute(attributeValue, this._batchValues, index);
  287. } else {
  288. Cartesian4.pack(attributeValue, this._batchValues, index);
  289. }
  290. this._batchValuesDirty = true;
  291. };
  292. function createTexture(batchTable, context) {
  293. var dimensions = batchTable._textureDimensions;
  294. batchTable._texture = new Texture({
  295. context : context,
  296. pixelFormat : PixelFormat.RGBA,
  297. pixelDatatype : batchTable._pixelDatatype,
  298. width : dimensions.x,
  299. height : dimensions.y,
  300. sampler : new Sampler({
  301. minificationFilter : TextureMinificationFilter.NEAREST,
  302. magnificationFilter : TextureMagnificationFilter.NEAREST
  303. }),
  304. flipY : false
  305. });
  306. }
  307. function updateTexture(batchTable) {
  308. var dimensions = batchTable._textureDimensions;
  309. batchTable._texture.copyFrom({
  310. width : dimensions.x,
  311. height : dimensions.y,
  312. arrayBufferView : batchTable._batchValues
  313. });
  314. }
  315. /**
  316. * Creates/updates the batch table texture.
  317. * @param {FrameState} frameState The frame state.
  318. *
  319. * @exception {RuntimeError} The floating point texture extension is required but not supported.
  320. */
  321. BatchTable.prototype.update = function(frameState) {
  322. if ((defined(this._texture) && !this._batchValuesDirty) || this._attributes.length === 0) {
  323. return;
  324. }
  325. this._batchValuesDirty = false;
  326. if (!defined(this._texture)) {
  327. createTexture(this, frameState.context);
  328. }
  329. updateTexture(this);
  330. };
  331. /**
  332. * Gets a function that will update a uniform map to contain values for looking up values in the batch table.
  333. *
  334. * @returns {BatchTable~updateUniformMapCallback} A callback for updating uniform maps.
  335. */
  336. BatchTable.prototype.getUniformMapCallback = function() {
  337. var that = this;
  338. return function(uniformMap) {
  339. if (that._attributes.length === 0) {
  340. return uniformMap;
  341. }
  342. var batchUniformMap = {
  343. batchTexture : function() {
  344. return that._texture;
  345. },
  346. batchTextureDimensions : function() {
  347. return that._textureDimensions;
  348. },
  349. batchTextureStep : function() {
  350. return that._textureStep;
  351. }
  352. };
  353. return combine(uniformMap, batchUniformMap);
  354. };
  355. };
  356. function getGlslComputeSt(batchTable) {
  357. var stride = batchTable._stride;
  358. // GLSL batchId is zero-based: [0, numberOfInstances - 1]
  359. if (batchTable._textureDimensions.y === 1) {
  360. return 'uniform vec4 batchTextureStep; \n' +
  361. 'vec2 computeSt(float batchId) \n' +
  362. '{ \n' +
  363. ' float stepX = batchTextureStep.x; \n' +
  364. ' float centerX = batchTextureStep.y; \n' +
  365. ' float numberOfAttributes = float('+ stride + '); \n' +
  366. ' return vec2(centerX + (batchId * numberOfAttributes * stepX), 0.5); \n' +
  367. '} \n';
  368. }
  369. return 'uniform vec4 batchTextureStep; \n' +
  370. 'uniform vec2 batchTextureDimensions; \n' +
  371. 'vec2 computeSt(float batchId) \n' +
  372. '{ \n' +
  373. ' float stepX = batchTextureStep.x; \n' +
  374. ' float centerX = batchTextureStep.y; \n' +
  375. ' float stepY = batchTextureStep.z; \n' +
  376. ' float centerY = batchTextureStep.w; \n' +
  377. ' float numberOfAttributes = float('+ stride + '); \n' +
  378. ' float xId = mod(batchId * numberOfAttributes, batchTextureDimensions.x); \n' +
  379. ' float yId = floor(batchId * numberOfAttributes / batchTextureDimensions.x); \n' +
  380. ' return vec2(centerX + (xId * stepX), centerY + (yId * stepY)); \n' +
  381. '} \n';
  382. }
  383. function getComponentType(componentsPerAttribute) {
  384. if (componentsPerAttribute === 1) {
  385. return 'float';
  386. }
  387. return 'vec' + componentsPerAttribute;
  388. }
  389. function getComponentSwizzle(componentsPerAttribute) {
  390. if (componentsPerAttribute === 1) {
  391. return '.x';
  392. } else if (componentsPerAttribute === 2) {
  393. return '.xy';
  394. } else if (componentsPerAttribute === 3) {
  395. return '.xyz';
  396. }
  397. return '';
  398. }
  399. function getGlslAttributeFunction(batchTable, attributeIndex) {
  400. var attributes = batchTable._attributes;
  401. var attribute = attributes[attributeIndex];
  402. var componentsPerAttribute = attribute.componentsPerAttribute;
  403. var functionName = attribute.functionName;
  404. var functionReturnType = getComponentType(componentsPerAttribute);
  405. var functionReturnValue = getComponentSwizzle(componentsPerAttribute);
  406. var offset = batchTable._offsets[attributeIndex];
  407. var glslFunction =
  408. functionReturnType + ' ' + functionName + '(float batchId) \n' +
  409. '{ \n' +
  410. ' vec2 st = computeSt(batchId); \n' +
  411. ' st.x += batchTextureStep.x * float(' + offset + '); \n';
  412. if (batchTable._packFloats && attribute.componentDatatype !== PixelDatatype.UNSIGNED_BYTE) {
  413. glslFunction += 'vec4 textureValue; \n' +
  414. 'textureValue.x = czm_unpackFloat(texture2D(batchTexture, st)); \n' +
  415. 'textureValue.y = czm_unpackFloat(texture2D(batchTexture, st + vec2(batchTextureStep.x, 0.0))); \n' +
  416. 'textureValue.z = czm_unpackFloat(texture2D(batchTexture, st + vec2(batchTextureStep.x * 2.0, 0.0))); \n' +
  417. 'textureValue.w = czm_unpackFloat(texture2D(batchTexture, st + vec2(batchTextureStep.x * 3.0, 0.0))); \n';
  418. } else {
  419. glslFunction += ' vec4 textureValue = texture2D(batchTexture, st); \n';
  420. }
  421. glslFunction += ' ' + functionReturnType + ' value = textureValue' + functionReturnValue + '; \n';
  422. if (batchTable._pixelDatatype === PixelDatatype.UNSIGNED_BYTE && attribute.componentDatatype === ComponentDatatype.UNSIGNED_BYTE && !attribute.normalize) {
  423. glslFunction += 'value *= 255.0; \n';
  424. } else if (batchTable._pixelDatatype === PixelDatatype.FLOAT && attribute.componentDatatype === ComponentDatatype.UNSIGNED_BYTE && attribute.normalize) {
  425. glslFunction += 'value /= 255.0; \n';
  426. }
  427. glslFunction +=
  428. ' return value; \n' +
  429. '} \n';
  430. return glslFunction;
  431. }
  432. /**
  433. * Gets a function that will update a vertex shader to contain functions for looking up values in the batch table.
  434. *
  435. * @returns {BatchTable~updateVertexShaderSourceCallback} A callback for updating a vertex shader source.
  436. */
  437. BatchTable.prototype.getVertexShaderCallback = function() {
  438. var attributes = this._attributes;
  439. if (attributes.length === 0) {
  440. return function(source) {
  441. return source;
  442. };
  443. }
  444. var batchTableShader = 'uniform sampler2D batchTexture; \n';
  445. batchTableShader += getGlslComputeSt(this) + '\n';
  446. var length = attributes.length;
  447. for (var i = 0; i < length; ++i) {
  448. batchTableShader += getGlslAttributeFunction(this, i);
  449. }
  450. return function(source) {
  451. var mainIndex = source.indexOf('void main');
  452. var beforeMain = source.substring(0, mainIndex);
  453. var afterMain = source.substring(mainIndex);
  454. return beforeMain + '\n' + batchTableShader + '\n' + afterMain;
  455. };
  456. };
  457. /**
  458. * Returns true if this object was destroyed; otherwise, false.
  459. * <br /><br />
  460. * If this object was destroyed, it should not be used; calling any function other than
  461. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
  462. *
  463. * @returns {Boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
  464. *
  465. * @see BatchTable#destroy
  466. */
  467. BatchTable.prototype.isDestroyed = function() {
  468. return false;
  469. };
  470. /**
  471. * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
  472. * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
  473. * <br /><br />
  474. * Once an object is destroyed, it should not be used; calling any function other than
  475. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
  476. * assign the return value (<code>undefined</code>) to the object as done in the example.
  477. *
  478. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  479. *
  480. * @see BatchTable#isDestroyed
  481. */
  482. BatchTable.prototype.destroy = function() {
  483. this._texture = this._texture && this._texture.destroy();
  484. return destroyObject(this);
  485. };
  486. /**
  487. * A callback for updating uniform maps.
  488. * @callback BatchTable~updateUniformMapCallback
  489. *
  490. * @param {Object} uniformMap The uniform map.
  491. * @returns {Object} The new uniform map with properties for retrieving values from the batch table.
  492. */
  493. /**
  494. * A callback for updating a vertex shader source.
  495. * @callback BatchTable~updateVertexShaderSourceCallback
  496. *
  497. * @param {String} vertexShaderSource The vertex shader source.
  498. * @returns {String} The new vertex shader source with the functions for retrieving batch table values injected.
  499. */
  500. export default BatchTable;