OctahedralProjectedCubeMap.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. import Cartesian3 from "../Core/Cartesian3.js";
  2. import ComponentDatatype from "../Core/ComponentDatatype.js";
  3. import defer from "../Core/defer.js";
  4. import defined from "../Core/defined.js";
  5. import destroyObject from "../Core/destroyObject.js";
  6. import IndexDatatype from "../Core/IndexDatatype.js";
  7. import loadKTX2 from "../Core/loadKTX2.js";
  8. import PixelFormat from "../Core/PixelFormat.js";
  9. import Buffer from "../Renderer/Buffer.js";
  10. import BufferUsage from "../Renderer/BufferUsage.js";
  11. import ComputeCommand from "../Renderer/ComputeCommand.js";
  12. import CubeMap from "../Renderer/CubeMap.js";
  13. import PixelDatatype from "../Renderer/PixelDatatype.js";
  14. import ShaderProgram from "../Renderer/ShaderProgram.js";
  15. import ShaderSource from "../Renderer/ShaderSource.js";
  16. import Texture from "../Renderer/Texture.js";
  17. import VertexArray from "../Renderer/VertexArray.js";
  18. import OctahedralProjectionAtlasFS from "../Shaders/OctahedralProjectionAtlasFS.js";
  19. import OctahedralProjectionFS from "../Shaders/OctahedralProjectionFS.js";
  20. import OctahedralProjectionVS from "../Shaders/OctahedralProjectionVS.js";
  21. /**
  22. * Packs all mip levels of a cube map into a 2D texture atlas.
  23. *
  24. * Octahedral projection is a way of putting the cube maps onto a 2D texture
  25. * with minimal distortion and easy look up.
  26. * See Chapter 16 of WebGL Insights "HDR Image-Based Lighting on the Web" by Jeff Russell
  27. * and "Octahedron Environment Maps" for reference.
  28. *
  29. * @private
  30. */
  31. function OctahedralProjectedCubeMap(url) {
  32. this._url = url;
  33. this._cubeMapBuffers = undefined;
  34. this._cubeMaps = undefined;
  35. this._texture = undefined;
  36. this._mipTextures = undefined;
  37. this._va = undefined;
  38. this._sp = undefined;
  39. this._maximumMipmapLevel = undefined;
  40. this._loading = false;
  41. this._ready = false;
  42. this._readyPromise = defer();
  43. }
  44. Object.defineProperties(OctahedralProjectedCubeMap.prototype, {
  45. /**
  46. * The url to the KTX2 file containing the specular environment map and convoluted mipmaps.
  47. * @memberof OctahedralProjectedCubeMap.prototype
  48. * @type {String}
  49. * @readonly
  50. */
  51. url: {
  52. get: function () {
  53. return this._url;
  54. },
  55. },
  56. /**
  57. * A texture containing all the packed convolutions.
  58. * @memberof OctahedralProjectedCubeMap.prototype
  59. * @type {Texture}
  60. * @readonly
  61. */
  62. texture: {
  63. get: function () {
  64. return this._texture;
  65. },
  66. },
  67. /**
  68. * The maximum number of mip levels.
  69. * @memberOf OctahedralProjectedCubeMap.prototype
  70. * @type {Number}
  71. * @readonly
  72. */
  73. maximumMipmapLevel: {
  74. get: function () {
  75. return this._maximumMipmapLevel;
  76. },
  77. },
  78. /**
  79. * Determines if the texture atlas is complete and ready to use.
  80. * @memberof OctahedralProjectedCubeMap.prototype
  81. * @type {Boolean}
  82. * @readonly
  83. */
  84. ready: {
  85. get: function () {
  86. return this._ready;
  87. },
  88. },
  89. /**
  90. * Gets a promise that resolves when the texture atlas is ready to use.
  91. * @memberof OctahedralProjectedCubeMap.prototype
  92. * @type {Promise<void>}
  93. * @readonly
  94. */
  95. readyPromise: {
  96. get: function () {
  97. return this._readyPromise.promise;
  98. },
  99. },
  100. });
  101. OctahedralProjectedCubeMap.isSupported = function (context) {
  102. return (
  103. (context.colorBufferHalfFloat && context.halfFloatingPointTexture) ||
  104. (context.floatingPointTexture && context.colorBufferFloat)
  105. );
  106. };
  107. // These vertices are based on figure 1 from "Octahedron Environment Maps".
  108. const v1 = new Cartesian3(1.0, 0.0, 0.0);
  109. const v2 = new Cartesian3(0.0, 0.0, 1.0);
  110. const v3 = new Cartesian3(-1.0, 0.0, 0.0);
  111. const v4 = new Cartesian3(0.0, 0.0, -1.0);
  112. const v5 = new Cartesian3(0.0, 1.0, 0.0);
  113. const v6 = new Cartesian3(0.0, -1.0, 0.0);
  114. // top left, left, top, center, right, top right, bottom, bottom left, bottom right
  115. const cubeMapCoordinates = [v5, v3, v2, v6, v1, v5, v4, v5, v5];
  116. const length = cubeMapCoordinates.length;
  117. const flatCubeMapCoordinates = new Float32Array(length * 3);
  118. let offset = 0;
  119. for (let i = 0; i < length; ++i, offset += 3) {
  120. Cartesian3.pack(cubeMapCoordinates[i], flatCubeMapCoordinates, offset);
  121. }
  122. const flatPositions = new Float32Array([
  123. -1.0,
  124. 1.0, // top left
  125. -1.0,
  126. 0.0, // left
  127. 0.0,
  128. 1.0, // top
  129. 0.0,
  130. 0.0, // center
  131. 1.0,
  132. 0.0, // right
  133. 1.0,
  134. 1.0, // top right
  135. 0.0,
  136. -1.0, // bottom
  137. -1.0,
  138. -1.0, // bottom left
  139. 1.0,
  140. -1.0, // bottom right
  141. ]);
  142. const indices = new Uint16Array([
  143. 0,
  144. 1,
  145. 2, // top left, left, top,
  146. 2,
  147. 3,
  148. 1, // top, center, left,
  149. 7,
  150. 6,
  151. 1, // bottom left, bottom, left,
  152. 3,
  153. 6,
  154. 1, // center, bottom, left,
  155. 2,
  156. 5,
  157. 4, // top, top right, right,
  158. 3,
  159. 4,
  160. 2, // center, right, top,
  161. 4,
  162. 8,
  163. 6, // right, bottom right, bottom,
  164. 3,
  165. 4,
  166. 6, //center, right, bottom
  167. ]);
  168. function createVertexArray(context) {
  169. const positionBuffer = Buffer.createVertexBuffer({
  170. context: context,
  171. typedArray: flatPositions,
  172. usage: BufferUsage.STATIC_DRAW,
  173. });
  174. const cubeMapCoordinatesBuffer = Buffer.createVertexBuffer({
  175. context: context,
  176. typedArray: flatCubeMapCoordinates,
  177. usage: BufferUsage.STATIC_DRAW,
  178. });
  179. const indexBuffer = Buffer.createIndexBuffer({
  180. context: context,
  181. typedArray: indices,
  182. usage: BufferUsage.STATIC_DRAW,
  183. indexDatatype: IndexDatatype.UNSIGNED_SHORT,
  184. });
  185. const attributes = [
  186. {
  187. index: 0,
  188. vertexBuffer: positionBuffer,
  189. componentsPerAttribute: 2,
  190. componentDatatype: ComponentDatatype.FLOAT,
  191. },
  192. {
  193. index: 1,
  194. vertexBuffer: cubeMapCoordinatesBuffer,
  195. componentsPerAttribute: 3,
  196. componentDatatype: ComponentDatatype.FLOAT,
  197. },
  198. ];
  199. return new VertexArray({
  200. context: context,
  201. attributes: attributes,
  202. indexBuffer: indexBuffer,
  203. });
  204. }
  205. function createUniformTexture(texture) {
  206. return function () {
  207. return texture;
  208. };
  209. }
  210. function cleanupResources(map) {
  211. map._va = map._va && map._va.destroy();
  212. map._sp = map._sp && map._sp.destroy();
  213. let i;
  214. let length;
  215. const cubeMaps = map._cubeMaps;
  216. if (defined(cubeMaps)) {
  217. length = cubeMaps.length;
  218. for (i = 0; i < length; ++i) {
  219. cubeMaps[i].destroy();
  220. }
  221. }
  222. const mipTextures = map._mipTextures;
  223. if (defined(mipTextures)) {
  224. length = mipTextures.length;
  225. for (i = 0; i < length; ++i) {
  226. mipTextures[i].destroy();
  227. }
  228. }
  229. map._va = undefined;
  230. map._sp = undefined;
  231. map._cubeMaps = undefined;
  232. map._cubeMapBuffers = undefined;
  233. map._mipTextures = undefined;
  234. }
  235. /**
  236. * Creates compute commands to generate octahedral projections of each cube map
  237. * and then renders them to an atlas.
  238. * <p>
  239. * Only needs to be called twice. The first call queues the compute commands to generate the atlas.
  240. * The second call cleans up unused resources. Every call afterwards is a no-op.
  241. * </p>
  242. *
  243. * @param {FrameState} frameState The frame state.
  244. *
  245. * @private
  246. */
  247. OctahedralProjectedCubeMap.prototype.update = function (frameState) {
  248. const context = frameState.context;
  249. if (!OctahedralProjectedCubeMap.isSupported(context)) {
  250. return;
  251. }
  252. if (defined(this._texture) && defined(this._va)) {
  253. cleanupResources(this);
  254. }
  255. if (defined(this._texture)) {
  256. return;
  257. }
  258. if (!defined(this._texture) && !this._loading) {
  259. const cachedTexture = context.textureCache.getTexture(this._url);
  260. if (defined(cachedTexture)) {
  261. cleanupResources(this);
  262. this._texture = cachedTexture;
  263. this._maximumMipmapLevel = this._texture.maximumMipmapLevel;
  264. this._ready = true;
  265. this._readyPromise.resolve();
  266. return;
  267. }
  268. }
  269. const cubeMapBuffers = this._cubeMapBuffers;
  270. if (!defined(cubeMapBuffers) && !this._loading) {
  271. const that = this;
  272. loadKTX2(this._url)
  273. .then(function (buffers) {
  274. that._cubeMapBuffers = buffers;
  275. that._loading = false;
  276. })
  277. .catch(function (e) {
  278. that._readyPromise.reject(e);
  279. });
  280. this._loading = true;
  281. }
  282. if (!defined(this._cubeMapBuffers)) {
  283. return;
  284. }
  285. const defines = [];
  286. // Datatype is defined if it is a normalized type (i.e. ..._UNORM, ..._SFLOAT)
  287. let pixelDatatype = cubeMapBuffers[0].positiveX.pixelDatatype;
  288. if (!defined(pixelDatatype)) {
  289. pixelDatatype = context.halfFloatingPointTexture
  290. ? PixelDatatype.HALF_FLOAT
  291. : PixelDatatype.FLOAT;
  292. } else {
  293. defines.push("RGBA_NORMALIZED");
  294. }
  295. const pixelFormat = PixelFormat.RGBA;
  296. const fs = new ShaderSource({
  297. defines: defines,
  298. sources: [OctahedralProjectionFS],
  299. });
  300. this._va = createVertexArray(context);
  301. this._sp = ShaderProgram.fromCache({
  302. context: context,
  303. vertexShaderSource: OctahedralProjectionVS,
  304. fragmentShaderSource: fs,
  305. attributeLocations: {
  306. position: 0,
  307. cubeMapCoordinates: 1,
  308. },
  309. });
  310. // We only need up to 6 mip levels to avoid artifacts.
  311. const length = Math.min(cubeMapBuffers.length, 6);
  312. this._maximumMipmapLevel = length - 1;
  313. const cubeMaps = (this._cubeMaps = new Array(length));
  314. const mipTextures = (this._mipTextures = new Array(length));
  315. const originalSize = cubeMapBuffers[0].positiveX.width * 2.0;
  316. const uniformMap = {
  317. originalSize: function () {
  318. return originalSize;
  319. },
  320. };
  321. // First we project each cubemap onto a flat octahedron, and write that to a texture.
  322. for (let i = 0; i < length; ++i) {
  323. // Swap +Y/-Y faces since the octahedral projection expects this order.
  324. const positiveY = cubeMapBuffers[i].positiveY;
  325. cubeMapBuffers[i].positiveY = cubeMapBuffers[i].negativeY;
  326. cubeMapBuffers[i].negativeY = positiveY;
  327. const cubeMap = (cubeMaps[i] = new CubeMap({
  328. context: context,
  329. source: cubeMapBuffers[i],
  330. pixelDatatype: pixelDatatype,
  331. }));
  332. const size = cubeMaps[i].width * 2;
  333. const mipTexture = (mipTextures[i] = new Texture({
  334. context: context,
  335. width: size,
  336. height: size,
  337. pixelDatatype: pixelDatatype,
  338. pixelFormat: pixelFormat,
  339. }));
  340. const command = new ComputeCommand({
  341. vertexArray: this._va,
  342. shaderProgram: this._sp,
  343. uniformMap: {
  344. cubeMap: createUniformTexture(cubeMap),
  345. },
  346. outputTexture: mipTexture,
  347. persists: true,
  348. owner: this,
  349. });
  350. frameState.commandList.push(command);
  351. uniformMap[`texture${i}`] = createUniformTexture(mipTexture);
  352. }
  353. this._texture = new Texture({
  354. context: context,
  355. width: originalSize * 1.5 + 2.0, // We add a 1 pixel border to avoid linear sampling artifacts.
  356. height: originalSize,
  357. pixelDatatype: pixelDatatype,
  358. pixelFormat: pixelFormat,
  359. });
  360. this._texture.maximumMipmapLevel = this._maximumMipmapLevel;
  361. context.textureCache.addTexture(this._url, this._texture);
  362. const atlasCommand = new ComputeCommand({
  363. fragmentShaderSource: OctahedralProjectionAtlasFS,
  364. uniformMap: uniformMap,
  365. outputTexture: this._texture,
  366. persists: false,
  367. owner: this,
  368. });
  369. frameState.commandList.push(atlasCommand);
  370. this._ready = true;
  371. this._readyPromise.resolve();
  372. };
  373. /**
  374. * Returns true if this object was destroyed; otherwise, false.
  375. * <p>
  376. * If this object was destroyed, it should not be used; calling any function other than
  377. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
  378. * </p>
  379. *
  380. * @returns {Boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
  381. *
  382. * @see OctahedralProjectedCubeMap#destroy
  383. */
  384. OctahedralProjectedCubeMap.prototype.isDestroyed = function () {
  385. return false;
  386. };
  387. /**
  388. * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
  389. * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
  390. * <p>
  391. * Once an object is destroyed, it should not be used; calling any function other than
  392. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
  393. * assign the return value (<code>undefined</code>) to the object as done in the example.
  394. * </p>
  395. *
  396. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  397. *
  398. * @see OctahedralProjectedCubeMap#isDestroyed
  399. */
  400. OctahedralProjectedCubeMap.prototype.destroy = function () {
  401. cleanupResources(this);
  402. this._texture = this._texture && this._texture.destroy();
  403. return destroyObject(this);
  404. };
  405. export default OctahedralProjectedCubeMap;