OctahedralProjectedCubeMap.js 12 KB

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