WebXRLightEstimation.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import { WebGLHardwareTexture } from "../../Engines/WebGL/webGLHardwareTexture.js";
  2. import { InternalTexture, InternalTextureSource } from "../../Materials/Textures/internalTexture.js";
  3. import { Observable } from "../../Misc/observable.js";
  4. import { Tools } from "../../Misc/tools.js";
  5. import { WebXRFeatureName, WebXRFeaturesManager } from "../webXRFeaturesManager.js";
  6. import { WebXRAbstractFeature } from "./WebXRAbstractFeature.js";
  7. import { Color3 } from "../../Maths/math.color.js";
  8. import { Vector3 } from "../../Maths/math.vector.js";
  9. import { DirectionalLight } from "../../Lights/directionalLight.js";
  10. import { BaseTexture } from "../../Materials/Textures/baseTexture.js";
  11. import { SphericalHarmonics, SphericalPolynomial } from "../../Maths/sphericalPolynomial.js";
  12. import { LightConstants } from "../../Lights/lightConstants.js";
  13. import { HDRFiltering } from "../../Materials/Textures/Filtering/hdrFiltering.js";
  14. /**
  15. * Light Estimation Feature
  16. *
  17. * @since 5.0.0
  18. */
  19. export class WebXRLightEstimation extends WebXRAbstractFeature {
  20. /**
  21. * Creates a new instance of the light estimation feature
  22. * @param _xrSessionManager an instance of WebXRSessionManager
  23. * @param options options to use when constructing this feature
  24. */
  25. constructor(_xrSessionManager,
  26. /**
  27. * options to use when constructing this feature
  28. */
  29. options) {
  30. super(_xrSessionManager);
  31. this.options = options;
  32. this._canvasContext = null;
  33. this._reflectionCubeMap = null;
  34. this._xrLightEstimate = null;
  35. this._xrLightProbe = null;
  36. this._xrWebGLBinding = null;
  37. this._lightDirection = Vector3.Up().negateInPlace();
  38. this._lightColor = Color3.White();
  39. this._intensity = 1;
  40. this._sphericalHarmonics = new SphericalHarmonics();
  41. this._cubeMapPollTime = Date.now();
  42. this._lightEstimationPollTime = Date.now();
  43. /**
  44. * ARCore's reflection cube map size is 16x16.
  45. * Once other systems support this feature we will need to change this to be dynamic.
  46. * see https://github.com/immersive-web/lighting-estimation/blob/main/lighting-estimation-explainer.md#cube-map-open-questions
  47. */
  48. this._reflectionCubeMapTextureSize = 16;
  49. /**
  50. * If createDirectionalLightSource is set to true this light source will be created automatically.
  51. * Otherwise this can be set with an external directional light source.
  52. * This light will be updated whenever the light estimation values change.
  53. */
  54. this.directionalLight = null;
  55. /**
  56. * This observable will notify when the reflection cube map is updated.
  57. */
  58. this.onReflectionCubeMapUpdatedObservable = new Observable();
  59. /**
  60. * Event Listener for "reflectionchange" events.
  61. */
  62. this._updateReflectionCubeMap = () => {
  63. if (!this._xrLightProbe) {
  64. return;
  65. }
  66. // check poll time, do not update if it has not been long enough
  67. if (this.options.cubeMapPollInterval) {
  68. const now = Date.now();
  69. if (now - this._cubeMapPollTime < this.options.cubeMapPollInterval) {
  70. return;
  71. }
  72. this._cubeMapPollTime = now;
  73. }
  74. const lp = this._getXRGLBinding().getReflectionCubeMap(this._xrLightProbe);
  75. if (lp && this._reflectionCubeMap) {
  76. if (!this._reflectionCubeMap._texture) {
  77. const internalTexture = new InternalTexture(this._xrSessionManager.scene.getEngine(), InternalTextureSource.Unknown);
  78. internalTexture.isCube = true;
  79. internalTexture.invertY = false;
  80. internalTexture._useSRGBBuffer = this.options.reflectionFormat === "srgba8";
  81. internalTexture.format = 5;
  82. internalTexture.generateMipMaps = true;
  83. internalTexture.type = this.options.reflectionFormat !== "srgba8" ? 2 : 0;
  84. internalTexture.samplingMode = 3;
  85. internalTexture.width = this._reflectionCubeMapTextureSize;
  86. internalTexture.height = this._reflectionCubeMapTextureSize;
  87. internalTexture._cachedWrapU = 1;
  88. internalTexture._cachedWrapV = 1;
  89. internalTexture._hardwareTexture = new WebGLHardwareTexture(lp, this._getCanvasContext());
  90. this._reflectionCubeMap._texture = internalTexture;
  91. }
  92. else {
  93. this._reflectionCubeMap._texture._hardwareTexture?.set(lp);
  94. this._reflectionCubeMap._texture.getEngine().resetTextureCache();
  95. }
  96. this._reflectionCubeMap._texture.isReady = true;
  97. if (!this.options.disablePreFiltering) {
  98. this._xrLightProbe.removeEventListener("reflectionchange", this._updateReflectionCubeMap);
  99. this._hdrFilter.prefilter(this._reflectionCubeMap).then(() => {
  100. this._xrSessionManager.scene.markAllMaterialsAsDirty(1);
  101. this.onReflectionCubeMapUpdatedObservable.notifyObservers(this._reflectionCubeMap);
  102. this._xrLightProbe.addEventListener("reflectionchange", this._updateReflectionCubeMap);
  103. });
  104. }
  105. else {
  106. this._xrSessionManager.scene.markAllMaterialsAsDirty(1);
  107. this.onReflectionCubeMapUpdatedObservable.notifyObservers(this._reflectionCubeMap);
  108. }
  109. }
  110. };
  111. this.xrNativeFeatureName = "light-estimation";
  112. if (this.options.createDirectionalLightSource) {
  113. this.directionalLight = new DirectionalLight("light estimation directional", this._lightDirection, this._xrSessionManager.scene);
  114. this.directionalLight.position = new Vector3(0, 8, 0);
  115. // intensity will be set later
  116. this.directionalLight.intensity = 0;
  117. this.directionalLight.falloffType = LightConstants.FALLOFF_GLTF;
  118. }
  119. this._hdrFilter = new HDRFiltering(this._xrSessionManager.scene.getEngine());
  120. // https://immersive-web.github.io/lighting-estimation/
  121. Tools.Warn("light-estimation is an experimental and unstable feature.");
  122. }
  123. /**
  124. * While the estimated cube map is expected to update over time to better reflect the user's environment as they move around those changes are unlikely to happen with every XRFrame.
  125. * Since creating and processing the cube map is potentially expensive, especially if mip maps are needed, you can listen to the onReflectionCubeMapUpdatedObservable to determine
  126. * when it has been updated.
  127. */
  128. get reflectionCubeMapTexture() {
  129. return this._reflectionCubeMap;
  130. }
  131. /**
  132. * The most recent light estimate. Available starting on the first frame where the device provides a light probe.
  133. */
  134. get xrLightingEstimate() {
  135. if (this._xrLightEstimate) {
  136. return {
  137. lightColor: this._lightColor,
  138. lightDirection: this._lightDirection,
  139. lightIntensity: this._intensity,
  140. sphericalHarmonics: this._sphericalHarmonics,
  141. };
  142. }
  143. return this._xrLightEstimate;
  144. }
  145. _getCanvasContext() {
  146. if (this._canvasContext === null) {
  147. this._canvasContext = this._xrSessionManager.scene.getEngine()._gl;
  148. }
  149. return this._canvasContext;
  150. }
  151. _getXRGLBinding() {
  152. if (this._xrWebGLBinding === null) {
  153. const context = this._getCanvasContext();
  154. this._xrWebGLBinding = new XRWebGLBinding(this._xrSessionManager.session, context);
  155. }
  156. return this._xrWebGLBinding;
  157. }
  158. /**
  159. * attach this feature
  160. * Will usually be called by the features manager
  161. *
  162. * @returns true if successful.
  163. */
  164. attach() {
  165. if (!super.attach()) {
  166. return false;
  167. }
  168. const reflectionFormat = this.options.reflectionFormat ?? (this._xrSessionManager.session.preferredReflectionFormat || "srgba8");
  169. this.options.reflectionFormat = reflectionFormat;
  170. this._xrSessionManager.session
  171. .requestLightProbe({
  172. reflectionFormat,
  173. })
  174. .then((xrLightProbe) => {
  175. this._xrLightProbe = xrLightProbe;
  176. if (!this.options.disableCubeMapReflection) {
  177. if (!this._reflectionCubeMap) {
  178. this._reflectionCubeMap = new BaseTexture(this._xrSessionManager.scene);
  179. this._reflectionCubeMap._isCube = true;
  180. this._reflectionCubeMap.coordinatesMode = 3;
  181. if (this.options.setSceneEnvironmentTexture) {
  182. this._xrSessionManager.scene.environmentTexture = this._reflectionCubeMap;
  183. }
  184. }
  185. this._xrLightProbe.addEventListener("reflectionchange", this._updateReflectionCubeMap);
  186. }
  187. });
  188. return true;
  189. }
  190. /**
  191. * detach this feature.
  192. * Will usually be called by the features manager
  193. *
  194. * @returns true if successful.
  195. */
  196. detach() {
  197. const detached = super.detach();
  198. if (this._xrLightProbe !== null && !this.options.disableCubeMapReflection) {
  199. this._xrLightProbe.removeEventListener("reflectionchange", this._updateReflectionCubeMap);
  200. this._xrLightProbe = null;
  201. }
  202. this._canvasContext = null;
  203. this._xrLightEstimate = null;
  204. // When the session ends (on detach) we must clear our XRWebGLBinging instance, which references the ended session.
  205. this._xrWebGLBinding = null;
  206. return detached;
  207. }
  208. /**
  209. * Dispose this feature and all of the resources attached
  210. */
  211. dispose() {
  212. super.dispose();
  213. this.onReflectionCubeMapUpdatedObservable.clear();
  214. if (this.directionalLight) {
  215. this.directionalLight.dispose();
  216. this.directionalLight = null;
  217. }
  218. if (this._reflectionCubeMap !== null) {
  219. if (this._reflectionCubeMap._texture) {
  220. this._reflectionCubeMap._texture.dispose();
  221. }
  222. this._reflectionCubeMap.dispose();
  223. this._reflectionCubeMap = null;
  224. }
  225. }
  226. _onXRFrame(_xrFrame) {
  227. if (this._xrLightProbe !== null) {
  228. if (this.options.lightEstimationPollInterval) {
  229. const now = Date.now();
  230. if (now - this._lightEstimationPollTime < this.options.lightEstimationPollInterval) {
  231. return;
  232. }
  233. this._lightEstimationPollTime = now;
  234. }
  235. this._xrLightEstimate = _xrFrame.getLightEstimate(this._xrLightProbe);
  236. if (this._xrLightEstimate) {
  237. this._intensity = Math.max(1.0, this._xrLightEstimate.primaryLightIntensity.x, this._xrLightEstimate.primaryLightIntensity.y, this._xrLightEstimate.primaryLightIntensity.z);
  238. const rhsFactor = this._xrSessionManager.scene.useRightHandedSystem ? 1.0 : -1.0;
  239. // recreate the vector caches, so that the last one provided to the user will persist
  240. if (this.options.disableVectorReuse) {
  241. this._lightDirection = new Vector3();
  242. this._lightColor = new Color3();
  243. if (this.directionalLight) {
  244. this.directionalLight.direction = this._lightDirection;
  245. this.directionalLight.diffuse = this._lightColor;
  246. }
  247. }
  248. this._lightDirection.copyFromFloats(this._xrLightEstimate.primaryLightDirection.x, this._xrLightEstimate.primaryLightDirection.y, this._xrLightEstimate.primaryLightDirection.z * rhsFactor);
  249. this._lightColor.copyFromFloats(this._xrLightEstimate.primaryLightIntensity.x / this._intensity, this._xrLightEstimate.primaryLightIntensity.y / this._intensity, this._xrLightEstimate.primaryLightIntensity.z / this._intensity);
  250. this._sphericalHarmonics.updateFromFloatsArray(this._xrLightEstimate.sphericalHarmonicsCoefficients);
  251. if (this._reflectionCubeMap && !this.options.disableSphericalPolynomial) {
  252. this._reflectionCubeMap.sphericalPolynomial = this._reflectionCubeMap.sphericalPolynomial || new SphericalPolynomial();
  253. this._reflectionCubeMap.sphericalPolynomial?.updateFromHarmonics(this._sphericalHarmonics);
  254. }
  255. // direction from instead of direction to
  256. this._lightDirection.negateInPlace();
  257. // set the values after calculating them
  258. if (this.directionalLight) {
  259. this.directionalLight.direction.copyFrom(this._lightDirection);
  260. this.directionalLight.intensity = Math.min(this._intensity, 1.0);
  261. this.directionalLight.diffuse.copyFrom(this._lightColor);
  262. }
  263. }
  264. }
  265. }
  266. }
  267. /**
  268. * The module's name
  269. */
  270. WebXRLightEstimation.Name = WebXRFeatureName.LIGHT_ESTIMATION;
  271. /**
  272. * The (Babylon) version of this module.
  273. * This is an integer representing the implementation version.
  274. * This number does not correspond to the WebXR specs version
  275. */
  276. WebXRLightEstimation.Version = 1;
  277. // register the plugin
  278. WebXRFeaturesManager.AddWebXRFeature(WebXRLightEstimation.Name, (xrSessionManager, options) => {
  279. return () => new WebXRLightEstimation(xrSessionManager, options);
  280. }, WebXRLightEstimation.Version, false);
  281. //# sourceMappingURL=WebXRLightEstimation.js.map