diff --git a/examples/index.js b/examples/index.js index ebdb79600..7c2fdf331 100644 --- a/examples/index.js +++ b/examples/index.js @@ -102,6 +102,12 @@ function onHashChange() { return; } + const hasMatch = items.some(({ itemDOM }) => `dist/${itemDOM.title}` === hashPath); + if (!hasMatch) { + clickItem(items[0].itemDOM); + return; + } + iframe.src = hashPath + ".html"; items.forEach(({ itemDOM }) => { diff --git a/examples/package.json b/examples/package.json index 7af584944..68787fbfe 100644 --- a/examples/package.json +++ b/examples/package.json @@ -25,6 +25,7 @@ "@galacean/engine-ui": "workspace:*" }, "devDependencies": { + "@vitejs/plugin-basic-ssl": "^2.1.4", "dat.gui": "^0.7.9", "vite": "^4.4.4" } diff --git a/examples/src/webxr-light-estimation.ts b/examples/src/webxr-light-estimation.ts index bb8eedb0e..049106d56 100644 --- a/examples/src/webxr-light-estimation.ts +++ b/examples/src/webxr-light-estimation.ts @@ -5,6 +5,7 @@ */ import { Camera, + Color, DirectLight, DiffuseMode, Entity, @@ -12,121 +13,302 @@ import { PBRMaterial, PrimitiveMesh, Script, + TextureCube, + TextureFormat, Vector3, WebGLEngine } from "@galacean/engine"; import { WebXRDevice } from "@galacean/engine-xr-webxr"; -import { XRLightEstimation, XRSessionMode, XRTrackedInputDevice } from "@galacean/engine-xr"; +import { XRLightEstimate, XRLightEstimation, XRSessionMode, XRTrackedInputDevice } from "@galacean/engine-xr"; -class LightEstimationApplier extends Script { +class LightEstimateApplier extends Script { lightEntity: Entity | null = null; - statusElement: HTMLElement | null = null; + panelElement: HTMLElement | null = null; private _direction = new Vector3(); private _target = new Vector3(); - private _hadEstimate = false; + private _reflectionTexture: TextureCube | null = null; + private _reflectionCubeMap: unknown | null = null; + private _reflectionCubeMapSize = 0; + private _reflectionCubeMapMipmapCount = 0; onUpdate(): void { const feature = this.engine.xrManager.getFeature(XRLightEstimation); if (!feature || !feature.available) { - if (this.statusElement && this._hadEstimate) { - this.statusElement.textContent = "Light estimation: waiting..."; - this._hadEstimate = false; - } + this._setPanelContent(false, null, false); return; } - const estimate = feature.estimate; + const { estimate } = feature; const ambient = this.scene.ambientLight; + ambient.diffuseMode = DiffuseMode.SphericalHarmonics; ambient.diffuseSphericalHarmonics = estimate.sphericalHarmonics; + ambient.diffuseIntensity = 1; + ambient.specularIntensity = 1; + + const hasReflection = this._applyReflectionCubeMap(estimate); if (this.lightEntity) { - const light = this.lightEntity.getComponent(DirectLight); - light.color.copyFrom(estimate.primaryLightIntensity); - const transform = this.lightEntity.transform; + const mainLight = this.lightEntity.getComponent(DirectLight); + mainLight.color.copyFrom(estimate.primaryLightIntensity); + this._direction.set( -estimate.primaryLightDirection.x, -estimate.primaryLightDirection.y, -estimate.primaryLightDirection.z ); - this._target.copyFrom(transform.worldPosition).add(this._direction); - transform.lookAt(this._target); + this._target.copyFrom(this.lightEntity.transform.worldPosition).add(this._direction); + this.lightEntity.transform.lookAt(this._target); } - if (this.statusElement && !this._hadEstimate) { - this.statusElement.textContent = "Light estimation: active"; - this._hadEstimate = true; + this._setPanelContent(true, estimate, hasReflection); + } + + override onDestroy(): void { + this._unbindReflectionTexture(); + } + + private _applyReflectionCubeMap(estimate: XRLightEstimate): boolean { + const reflectionCubeMap = estimate.reflectionCubeMap; + const ambient = this.scene.ambientLight; + + if (!reflectionCubeMap) { + this._unbindReflectionTexture(); + return false; } + + const size = Math.max(estimate.reflectionCubeMapSize || 1, 1); + const mipmapCount = Math.max(estimate.reflectionCubeMapMipmapCount || 1, 1); + + if (!this._reflectionTexture) { + this._reflectionTexture = new TextureCube(this.engine, size, TextureFormat.R8G8B8A8, mipmapCount > 1); + } + + if ( + this._reflectionCubeMap !== reflectionCubeMap || + this._reflectionCubeMapSize !== size || + this._reflectionCubeMapMipmapCount !== mipmapCount || + !this._reflectionTexture._isExternalTextureBound() + ) { + this._reflectionTexture._bindExternalTexture(reflectionCubeMap, { + size, + mipmapCount, + ownedByEngine: false, + immutable: true + }); + this._reflectionCubeMap = reflectionCubeMap; + this._reflectionCubeMapSize = size; + this._reflectionCubeMapMipmapCount = mipmapCount; + } + + if (ambient.specularTexture !== this._reflectionTexture) { + ambient.specularTexture = this._reflectionTexture; + ambient.specularTextureDecodeRGBM = false; + } + + return true; + } + + private _unbindReflectionTexture(): void { + const ambient = this.scene.ambientLight; + + if (this._reflectionTexture && ambient.specularTexture === this._reflectionTexture) { + ambient.specularTexture = null; + } + + if (this._reflectionTexture) { + this._reflectionTexture._unbindExternalTexture(); + this._reflectionTexture.destroy(); + this._reflectionTexture = null; + } + + this._reflectionCubeMap = null; + this._reflectionCubeMapSize = 0; + this._reflectionCubeMapMipmapCount = 0; + } + + private _setPanelContent(active: boolean, estimate: XRLightEstimate | null, hasReflection: boolean): void { + if (!this.panelElement) { + return; + } + + if (!active || !estimate) { + this.panelElement.textContent = [ + "WebXR Light Estimation", + "state: waiting...", + "diffuse SH: no", + "specular cube: no" + ].join("\n"); + return; + } + + const direction = estimate.primaryLightDirection; + const intensity = estimate.primaryLightIntensity; + this.panelElement.textContent = [ + "WebXR Light Estimation", + "state: active", + "diffuse SH: yes", + `specular cube: ${hasReflection ? "yes" : "no"}`, + `cube size: ${hasReflection ? estimate.reflectionCubeMapSize : 0}`, + `mipmap count: ${hasReflection ? estimate.reflectionCubeMapMipmapCount : 0}`, + `main light dir: ${direction.x.toFixed(2)}, ${direction.y.toFixed(2)}, ${direction.z.toFixed(2)}`, + `main light rgb: ${intensity.r.toFixed(2)}, ${intensity.g.toFixed(2)}, ${intensity.b.toFixed(2)}` + ].join("\n"); + } +} + +class RotationScript extends Script { + speed = 20; + + onUpdate(deltaTime: number): void { + this.entity.transform.rotate(0, this.speed * deltaTime, 0); } } main(); -async function main(): Promise { - const engine = await WebGLEngine.create({ +function main(): void { + WebGLEngine.create({ canvas: "canvas", xrDevice: new WebXRDevice() + }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const root = scene.createRootEntity("Root"); + + const xrOrigin = root.createChild("XROrigin"); + engine.xrManager.origin = xrOrigin; + + const cameraEntity = xrOrigin.createChild("Camera"); + const camera = cameraEntity.addComponent(Camera); + engine.xrManager.cameraManager.attachCamera(XRTrackedInputDevice.Camera, camera); + + const lightEntity = xrOrigin.createChild("MainLight"); + lightEntity.addComponent(DirectLight); + + createShowcaseSpheres(engine, xrOrigin); + + const infoPanel = createInfoPanel(); + const legendPanel = createLegendPanel(); + const enterButton = createEnterButton(); + + const xrManager = engine.xrManager; + const lightEstimationSupported = xrManager.isSupportedFeature(XRLightEstimation); + if (lightEstimationSupported) { + xrManager.addFeature(XRLightEstimation); + infoPanel.textContent = [ + "WebXR Light Estimation", + "state: ready", + "diffuse SH: pending", + "specular cube: pending" + ].join("\n"); + } else { + infoPanel.textContent = [ + "WebXR Light Estimation", + "state: not supported", + "diffuse SH: no", + "specular cube: no" + ].join("\n"); + legendPanel.textContent += "\nlight-estimation feature not supported on this device/browser."; + } + + enterButton.disabled = true; + xrManager.sessionManager.isSupportedMode(XRSessionMode.AR).then( + () => { + enterButton.disabled = false; + enterButton.onclick = () => { + xrManager.enterXR(XRSessionMode.AR).then( + () => { + infoPanel.textContent = [ + "WebXR Light Estimation", + "state: entering AR...", + "diffuse SH: pending", + "specular cube: pending" + ].join("\n"); + }, + (error) => { + infoPanel.textContent = [ + "WebXR Light Estimation", + "state: enter AR failed", + "diffuse SH: no", + "specular cube: no" + ].join("\n"); + console.error(error); + } + ); + }; + }, + (error) => { + infoPanel.textContent = [ + "WebXR Light Estimation", + "state: AR mode not supported", + "diffuse SH: no", + "specular cube: no" + ].join("\n"); + console.error(error); + } + ); + + const applier = root.addComponent(LightEstimateApplier); + applier.lightEntity = lightEntity; + applier.panelElement = infoPanel; + + engine.run(); }); - engine.canvas.resizeByClientSize(); +} - const scene = engine.sceneManager.activeScene; - const root = scene.createRootEntity("Root"); - const xrOrigin = root.createChild("XROrigin"); - engine.xrManager.origin = xrOrigin; +function createShowcaseSpheres(engine: WebGLEngine, xrOrigin: Entity): void { + const showcaseRoot = xrOrigin.createChild("Showcase"); + showcaseRoot.transform.setPosition(0, 0, -1.2); - const cameraEntity = xrOrigin.createChild("Camera"); - const camera = cameraEntity.addComponent(Camera); - engine.xrManager.cameraManager.attachCamera(XRTrackedInputDevice.Camera, camera); + const sphereMesh = PrimitiveMesh.createSphere(engine, 0.13, 36); - const lightEntity = xrOrigin.createChild("MainLight"); - lightEntity.addComponent(DirectLight); + const roughnessValues = [0.05, 0.35, 0.75]; + const dielectricColors = [ + new Color(0.95, 0.35, 0.3, 1), + new Color(0.25, 0.65, 0.95, 1), + new Color(0.35, 0.9, 0.45, 1) + ]; - const targetEntity = xrOrigin.createChild("Target"); - targetEntity.transform.setPosition(0, 0, -1.2); - const renderer = targetEntity.addComponent(MeshRenderer); - renderer.mesh = PrimitiveMesh.createSphere(engine, 0.15, 32); - const material = new PBRMaterial(engine); - renderer.setMaterial(material); + for (let i = 0; i < roughnessValues.length; i++) { + const x = (i - 1) * 0.34; + const roughness = roughnessValues[i]; - const status = createStatusLabel(); - const enterButton = createEnterButton(); + const metalSphere = showcaseRoot.createChild(`MetalSphere-${i}`); + metalSphere.transform.setPosition(x, 0.22, 0); + const metalRenderer = metalSphere.addComponent(MeshRenderer); + metalRenderer.mesh = sphereMesh; + const metalMaterial = new PBRMaterial(engine); + metalMaterial.baseColor = new Color(0.95, 0.95, 0.95, 1); + metalMaterial.metallic = 1; + metalMaterial.roughness = roughness; + metalRenderer.setMaterial(metalMaterial); + metalSphere.addComponent(RotationScript).speed = 10 + i * 6; - const xrManager = engine.xrManager; - const lightEstimationSupported = xrManager.isSupportedFeature(XRLightEstimation); - if (lightEstimationSupported) { - xrManager.addFeature(XRLightEstimation); - } else { - status.textContent = "Light estimation: not supported"; + const dielectricSphere = showcaseRoot.createChild(`DielectricSphere-${i}`); + dielectricSphere.transform.setPosition(x, -0.18, 0); + const dielectricRenderer = dielectricSphere.addComponent(MeshRenderer); + dielectricRenderer.mesh = sphereMesh; + const dielectricMaterial = new PBRMaterial(engine); + dielectricMaterial.baseColor = dielectricColors[i]; + dielectricMaterial.metallic = 0; + dielectricMaterial.roughness = roughness; + dielectricRenderer.setMaterial(dielectricMaterial); + dielectricSphere.addComponent(RotationScript).speed = 8 + i * 4; } - enterButton.disabled = true; - xrManager.sessionManager.isSupportedMode(XRSessionMode.AR).then( - () => { - enterButton.disabled = false; - enterButton.onclick = () => { - xrManager.enterXR(XRSessionMode.AR).then( - () => { - status.textContent = lightEstimationSupported ? "Light estimation: waiting..." : "AR session running"; - }, - (error) => { - status.textContent = "Enter AR failed"; - console.error(error); - } - ); - }; - }, - (error) => { - status.textContent = "AR not supported"; - console.error(error); - } - ); - - const applier = root.addComponent(LightEstimationApplier); - applier.lightEntity = lightEntity; - applier.statusElement = status; - - engine.run(); + const mirrorSphere = showcaseRoot.createChild("MirrorSphere"); + mirrorSphere.transform.setPosition(0, 0.52, 0.08); + const mirrorRenderer = mirrorSphere.addComponent(MeshRenderer); + mirrorRenderer.mesh = PrimitiveMesh.createSphere(engine, 0.11, 36); + const mirrorMaterial = new PBRMaterial(engine); + mirrorMaterial.baseColor = new Color(1, 1, 1, 1); + mirrorMaterial.metallic = 1; + mirrorMaterial.roughness = 0.02; + mirrorRenderer.setMaterial(mirrorMaterial); + mirrorSphere.addComponent(RotationScript).speed = 24; } function createEnterButton(): HTMLButtonElement { @@ -146,18 +328,48 @@ function createEnterButton(): HTMLButtonElement { return button; } -function createStatusLabel(): HTMLDivElement { +function createInfoPanel(): HTMLDivElement { const label = document.createElement("div"); - label.textContent = "Light estimation: waiting..."; + label.textContent = [ + "WebXR Light Estimation", + "state: booting...", + "diffuse SH: pending", + "specular cube: pending" + ].join("\n"); label.style.position = "absolute"; label.style.left = "16px"; label.style.bottom = "56px"; - label.style.padding = "6px 10px"; + label.style.padding = "8px 10px"; label.style.borderRadius = "6px"; - label.style.background = "rgba(0, 0, 0, 0.6)"; + label.style.background = "rgba(0, 0, 0, 0.65)"; label.style.color = "#fff"; label.style.fontSize = "12px"; label.style.fontFamily = "monospace"; + label.style.lineHeight = "1.45"; + label.style.whiteSpace = "pre"; document.body.appendChild(label); return label; } + +function createLegendPanel(): HTMLDivElement { + const legend = document.createElement("div"); + legend.textContent = [ + "showcase:", + "top row: metallic=1.0, roughness=0.05 / 0.35 / 0.75", + "bottom row: metallic=0.0, roughness=0.05 / 0.35 / 0.75", + "top center: mirror sphere for reflection probe" + ].join("\n"); + legend.style.position = "absolute"; + legend.style.left = "16px"; + legend.style.top = "16px"; + legend.style.padding = "8px 10px"; + legend.style.borderRadius = "6px"; + legend.style.background = "rgba(0, 0, 0, 0.55)"; + legend.style.color = "#fff"; + legend.style.fontSize = "11px"; + legend.style.fontFamily = "monospace"; + legend.style.lineHeight = "1.45"; + legend.style.whiteSpace = "pre"; + document.body.appendChild(legend); + return legend; +} diff --git a/examples/vite.config.js b/examples/vite.config.js index 9559c482f..2c1041232 100644 --- a/examples/vite.config.js +++ b/examples/vite.config.js @@ -1,5 +1,6 @@ const path = require("path"); const fs = require("fs-extra"); +const basicSsl = require("@vitejs/plugin-basic-ssl"); const OUT_PATH = "dist"; const templateStr = fs.readFileSync(path.join(__dirname, "template/iframe.ejs"), "utf8"); @@ -30,11 +31,22 @@ const demoList = fs }; }); +const outDir = path.resolve(__dirname, OUT_PATH); +const validDemoFileSet = new Set(demoList.map(({ file }) => file)); +fs.ensureDirSync(outDir); +fs.readdirSync(outDir).forEach((name) => { + if (!/\.(ts|html)$/.test(name)) return; + const file = name.replace(/\.(ts|html)$/, ""); + if (!validDemoFileSet.has(file)) { + fs.removeSync(path.join(outDir, name)); + } +}); + demoList.forEach(({ title, file }) => { const ejs = templateStr.replaceEJS("title", title).replaceEJS("url", `./${file}.ts`); - fs.outputFileSync(path.resolve(__dirname, OUT_PATH, file + ".ts"), `import "../src/${file}"`); - fs.outputFileSync(path.resolve(__dirname, OUT_PATH, file + ".html"), ejs); + fs.outputFileSync(path.join(outDir, file + ".ts"), `import "../src/${file}"`); + fs.outputFileSync(path.join(outDir, file + ".html"), ejs); }); // output demolist @@ -49,9 +61,12 @@ demoList.forEach(({ title, category, file }) => { }); }); -fs.outputJSONSync(path.join(__dirname, OUT_PATH, ".demoList.json"), demoSorted); +fs.outputJSONSync(path.join(outDir, ".demoList.json"), demoSorted); + +const useHttps = process.argv.includes("--https"); module.exports = { + plugins: useHttps ? [basicSsl()] : [], server: { open: true, host: "0.0.0.0", diff --git a/packages/core/src/renderingHardwareInterface/IPlatformTextureCube.ts b/packages/core/src/renderingHardwareInterface/IPlatformTextureCube.ts index b96120068..6be16bd3d 100644 --- a/packages/core/src/renderingHardwareInterface/IPlatformTextureCube.ts +++ b/packages/core/src/renderingHardwareInterface/IPlatformTextureCube.ts @@ -66,3 +66,47 @@ export interface IPlatformTextureCube extends IPlatformTexture { out: ArrayBufferView ): void; } + +/** + * @internal + */ +export interface IPlatformTextureCubeExternalOptions { + /** + * The face size of the external cube texture. + */ + size?: number; + /** + * Mipmap count of the external cube texture. + */ + mipmapCount?: number; + /** + * Whether the external handle should be destroyed by engine. + * @defaultValue `false` + */ + ownedByEngine?: boolean; + /** + * Whether the external texture data should be treated as immutable. + * @defaultValue `true` + */ + immutable?: boolean; +} + +/** + * @internal + */ +export interface IPlatformTextureCubeInternal extends IPlatformTextureCube { + /** + * Bind an external native cube texture handle. + */ + _bindExternalTexture(handle: unknown, options?: IPlatformTextureCubeExternalOptions): void; + + /** + * Unbind current external texture and restore engine-owned texture. + */ + _unbindExternalTexture(): void; + + /** + * Whether current texture is backed by an external handle. + */ + _isExternalTextureBound(): boolean; +} diff --git a/packages/core/src/texture/TextureCube.ts b/packages/core/src/texture/TextureCube.ts index 372fd6c32..92c2d47a4 100644 --- a/packages/core/src/texture/TextureCube.ts +++ b/packages/core/src/texture/TextureCube.ts @@ -1,5 +1,9 @@ import { Engine } from "../Engine"; -import { IPlatformTextureCube } from "../renderingHardwareInterface"; +import type { + IPlatformTextureCube, + IPlatformTextureCubeExternalOptions, + IPlatformTextureCubeInternal +} from "../renderingHardwareInterface/IPlatformTextureCube"; import { TextureCubeFace } from "./enums/TextureCubeFace"; import { TextureFilterMode } from "./enums/TextureFilterMode"; import { TextureFormat } from "./enums/TextureFormat"; @@ -190,4 +194,35 @@ export class TextureCube extends Texture { this._platformTexture = this._engine._hardwareRenderer.createPlatformTextureCube(this); super._rebuild(); } + + /** + * Bind an external native cube texture handle. + * @internal + */ + _bindExternalTexture(handle: unknown, options?: IPlatformTextureCubeExternalOptions): void { + const platformTexture = this._platformTexture as IPlatformTextureCubeInternal; + if (!platformTexture._bindExternalTexture) { + throw new Error("Current backend does not support external cube texture binding."); + } + platformTexture._bindExternalTexture(handle, options); + this._isContentLost = false; + } + + /** + * Unbind current external texture and restore engine-owned texture. + * @internal + */ + _unbindExternalTexture(): void { + const platformTexture = this._platformTexture as IPlatformTextureCubeInternal; + platformTexture._unbindExternalTexture && platformTexture._unbindExternalTexture(); + } + + /** + * Whether current texture is backed by an external handle. + * @internal + */ + _isExternalTextureBound(): boolean { + const platformTexture = this._platformTexture as IPlatformTextureCubeInternal; + return !!platformTexture._isExternalTextureBound?.(); + } } diff --git a/packages/design/src/xr/feature/lightEstimation/IXRLightEstimate.ts b/packages/design/src/xr/feature/lightEstimation/IXRLightEstimate.ts index c6bc62c9d..525f77952 100644 --- a/packages/design/src/xr/feature/lightEstimation/IXRLightEstimate.ts +++ b/packages/design/src/xr/feature/lightEstimation/IXRLightEstimate.ts @@ -10,4 +10,10 @@ export interface IXRLightEstimate { primaryLightDirection: Vector3; /** Main light intensity (linear RGB). */ primaryLightIntensity: Color; + /** Reflection cube map native handle from XR light probe. */ + reflectionCubeMap: unknown | null; + /** Reflection cube map face size. */ + reflectionCubeMapSize: number; + /** Reflection cube map mipmap count. */ + reflectionCubeMapMipmapCount: number; } diff --git a/packages/rhi-webgl/src/GLTexture.ts b/packages/rhi-webgl/src/GLTexture.ts index c0e005e83..8dfe7f6d9 100644 --- a/packages/rhi-webgl/src/GLTexture.ts +++ b/packages/rhi-webgl/src/GLTexture.ts @@ -440,6 +440,10 @@ export class GLTexture implements IPlatformTexture { /** @internal */ _glTexture: WebGLTexture; /** @internal */ + _ownsGLTexture: boolean = true; + /** @internal */ + _isExternalTexture: boolean = false; + /** @internal */ _rhi: WebGLGraphicDevice; /** @internal */ _gl: WebGLRenderingContext & WebGL2RenderingContext; @@ -549,10 +553,12 @@ export class GLTexture implements IPlatformTexture { * Destroy texture. */ destroy() { - this._gl.deleteTexture(this._glTexture); + this._ownsGLTexture && this._glTexture && this._gl.deleteTexture(this._glTexture); this._texture = null; this._glTexture = null; this._formatDetail = null; + this._ownsGLTexture = true; + this._isExternalTexture = false; } /** @@ -567,6 +573,11 @@ export class GLTexture implements IPlatformTexture { * Generate multi-level textures based on the 0th level data. */ generateMipmaps(): void { + if (this._isExternalTexture) { + Logger.warn("Cannot generate mipmaps for external texture."); + return; + } + const texture = this._texture; //@ts-ignore const mipmap = texture._mipmap; @@ -589,6 +600,11 @@ export class GLTexture implements IPlatformTexture { this._rhi.bindTexture(this); } + /** @internal */ + protected _invalidateTextureBindingCache() { + this._rhi.invalidateTextureBinding(this); + } + /** * Pre-development mipmapping GPU memory. */ diff --git a/packages/rhi-webgl/src/GLTextureCube.ts b/packages/rhi-webgl/src/GLTextureCube.ts index e1e9fce28..43daab4bc 100644 --- a/packages/rhi-webgl/src/GLTextureCube.ts +++ b/packages/rhi-webgl/src/GLTextureCube.ts @@ -2,12 +2,24 @@ import { IPlatformTextureCube, TextureCube, TextureCubeFace } from "@galacean/en import { GLTexture } from "./GLTexture"; import { WebGLGraphicDevice } from "./WebGLGraphicDevice"; +interface IExternalCubeTextureOptions { + size?: number; + mipmapCount?: number; + ownedByEngine?: boolean; + immutable?: boolean; +} + +interface IWebGL2CubeTextureQueryContext extends WebGL2RenderingContext { + getTexLevelParameter(target: number, level: number, pname: number): number; +} + /** * Cube texture in WebGL platform. */ export class GLTextureCube extends GLTexture implements IPlatformTextureCube { /** Backward compatible with WebGL1.0. */ private _compressedFaceFilled: number[] = [0, 0, 0, 0, 0, 0]; + private _externalTextureImmutable: boolean = true; constructor(rhi: WebGLGraphicDevice, textureCube: TextureCube) { super(rhi, textureCube, rhi.gl.TEXTURE_CUBE_MAP); @@ -32,6 +44,10 @@ export class GLTextureCube extends GLTexture implements IPlatformTextureCube { width?: number, height?: number ): void { + if (this._isExternalTexture && this._externalTextureImmutable) { + throw new Error("Cannot upload pixel data to an immutable external cube texture."); + } + const gl = this._gl; const isWebGL2 = this._isWebGL2; const formatDetail = this._formatDetail; @@ -99,6 +115,10 @@ export class GLTextureCube extends GLTexture implements IPlatformTextureCube { x: number, y: number ): void { + if (this._isExternalTexture && this._externalTextureImmutable) { + throw new Error("Cannot upload image data to an immutable external cube texture."); + } + const gl = this._gl; const { baseFormat, dataType } = this._formatDetail; @@ -134,4 +154,104 @@ export class GLTextureCube extends GLTexture implements IPlatformTextureCube { } super._getPixelBuffer(face, x, y, width, height, mipLevel, out); } + + /** + * @internal + */ + _bindExternalTexture(handle: unknown, options?: IExternalCubeTextureOptions): void { + const externalTexture = handle as WebGLTexture; + if (!externalTexture) { + throw new Error("External cube texture handle is invalid."); + } + + if (this._glTexture === externalTexture && this._isExternalTexture) { + this._externalTextureImmutable = options?.immutable ?? true; + this._syncExternalTextureMeta(options); + return; + } + + this._ownsGLTexture && this._glTexture && this._gl.deleteTexture(this._glTexture); + + this._glTexture = externalTexture; + this._isExternalTexture = true; + this._ownsGLTexture = options?.ownedByEngine ?? false; + this._externalTextureImmutable = options?.immutable ?? true; + this._syncExternalTextureMeta(options); + this._applyTextureState(); + this._invalidateTextureBindingCache(); + } + + /** + * @internal + */ + _unbindExternalTexture(): void { + if (!this._isExternalTexture) { + return; + } + + this._ownsGLTexture && this._glTexture && this._gl.deleteTexture(this._glTexture); + + this._glTexture = this._gl.createTexture(); + this._isExternalTexture = false; + this._ownsGLTexture = true; + this._externalTextureImmutable = true; + this._compressedFaceFilled.fill(0); + (this._formatDetail.isCompressed && !this._isWebGL2) || this._init(true); + this._applyTextureState(); + this._invalidateTextureBindingCache(); + } + + /** + * @internal + */ + _isExternalTextureBound(): boolean { + return this._isExternalTexture; + } + + private _applyTextureState(): void { + const texture = this._texture as any; + this.wrapModeU = texture._wrapModeU; + this.wrapModeV = texture._wrapModeV; + this.filterMode = texture._filterMode; + this.anisoLevel = texture._anisoLevel; + } + + private _syncExternalTextureMeta(options?: IExternalCubeTextureOptions): void { + let size = options?.size ?? 0; + let mipmapCount = options?.mipmapCount ?? 0; + + if (this._isWebGL2 && (size <= 0 || mipmapCount <= 0)) { + const gl = this._gl as IWebGL2CubeTextureQueryContext; + const currentBinding = gl.getParameter(gl.TEXTURE_BINDING_CUBE_MAP) as WebGLTexture | null; + const textureWidth = 0x1000; + gl.bindTexture(gl.TEXTURE_CUBE_MAP, this._glTexture); + try { + if (size <= 0) { + size = gl.getTexLevelParameter(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, textureWidth) || 0; + } + if (mipmapCount <= 0 && size > 0) { + for (let level = 0; level < 16; level++) { + const levelSize = gl.getTexLevelParameter(gl.TEXTURE_CUBE_MAP_POSITIVE_X, level, textureWidth) || 0; + if (levelSize <= 0) { + break; + } + mipmapCount++; + if (levelSize === 1) { + break; + } + } + } + } finally { + gl.bindTexture(gl.TEXTURE_CUBE_MAP, currentBinding); + } + } + + const texture = this._texture as any; + size = Math.max(size || texture._width || 1, 1); + mipmapCount = Math.max(mipmapCount || texture._mipmapCount || 1, 1); + texture._width = size; + texture._height = size; + texture._mipmap = mipmapCount > 1; + texture._mipmapCount = mipmapCount; + } } diff --git a/packages/rhi-webgl/src/WebGLGraphicDevice.ts b/packages/rhi-webgl/src/WebGLGraphicDevice.ts index 9609daa22..8ce89cabc 100644 --- a/packages/rhi-webgl/src/WebGLGraphicDevice.ts +++ b/packages/rhi-webgl/src/WebGLGraphicDevice.ts @@ -516,6 +516,18 @@ export class WebGLGraphicDevice implements IHardwareRenderer { } } + /** + * @internal + */ + invalidateTextureBinding(texture: GLTexture): void { + const activeTextures = this._activeTextures; + for (let i = 0, n = activeTextures.length; i < n; i++) { + if (activeTextures[i] === texture) { + activeTextures[i] = null; + } + } + } + setGlobalDepthBias(bias: number, slopeBias: number): void { const gl = this._gl; const enable = bias !== 0 || slopeBias !== 0; diff --git a/packages/xr-webxr/src/WebXRDevice.ts b/packages/xr-webxr/src/WebXRDevice.ts index f44344a03..4e3fca217 100644 --- a/packages/xr-webxr/src/WebXRDevice.ts +++ b/packages/xr-webxr/src/WebXRDevice.ts @@ -83,7 +83,7 @@ export class WebXRDevice implements IXRDevice { }); } session.requestReferenceSpace("local").then((referenceSpace: XRReferenceSpace) => { - resolve(new WebXRSession(session, layer, referenceSpace)); + resolve(new WebXRSession(session, layer, referenceSpace, gl)); }, reject); }, reject); }, reject); diff --git a/packages/xr-webxr/src/WebXRSession.ts b/packages/xr-webxr/src/WebXRSession.ts index a35fecb02..1c1c382dc 100644 --- a/packages/xr-webxr/src/WebXRSession.ts +++ b/packages/xr-webxr/src/WebXRSession.ts @@ -15,6 +15,8 @@ export class WebXRSession implements IXRSession { _platformLayer: XRWebGLLayer; /** @internal */ _platformReferenceSpace: XRReferenceSpace; + /** @internal */ + _gl: WebGLRenderingContext | WebGL2RenderingContext; private _frame: WebXRFrame; private _events: IXRInputEvent[] = []; @@ -78,11 +80,17 @@ export class WebXRSession implements IXRSession { return events; } - constructor(session: XRSession, layer: XRWebGLLayer, referenceSpace: XRReferenceSpace) { + constructor( + session: XRSession, + layer: XRWebGLLayer, + referenceSpace: XRReferenceSpace, + gl: WebGLRenderingContext | WebGL2RenderingContext + ) { this._frame = new WebXRFrame(this); this._platformSession = session; this._platformLayer = layer; this._platformReferenceSpace = referenceSpace; + this._gl = gl; const xrRequestAnimationFrame = session.requestAnimationFrame.bind(session); const onFrame = function (time: number, frame: XRFrame, callback: FrameRequestCallback) { this._frame._platformFrame = frame; diff --git a/packages/xr-webxr/src/feature/WebXRLightEstimation.ts b/packages/xr-webxr/src/feature/WebXRLightEstimation.ts index 7108debb7..ce838577d 100644 --- a/packages/xr-webxr/src/feature/WebXRLightEstimation.ts +++ b/packages/xr-webxr/src/feature/WebXRLightEstimation.ts @@ -5,6 +5,10 @@ import { WebXRFrame } from "../WebXRFrame"; import { WebXRSession } from "../WebXRSession"; import { WebXRFeature } from "./WebXRFeature"; +interface IWebGL2CubeTextureQueryContext extends WebGL2RenderingContext { + getTexLevelParameter(target: number, level: number, pname: number): number; +} + /** * WebXR implementation of light estimation. */ @@ -13,20 +17,42 @@ export class WebXRLightEstimation extends WebXRFeature implements IXRLightEstima private _lightProbe: XRLightProbe | null = null; private _probeRequestInFlight = false; private _probeRequestFailed = false; + private _platformSession: XRSession | null = null; + private _reflectionBinding: XRWebGLBinding | null = null; + private _reflectionChanged = true; + + private readonly _onReflectionChange = () => { + this._reflectionChanged = true; + }; checkAvailable(session: WebXRSession, frame: WebXRFrame): boolean { if (!frame._platformFrame) { return false; } + const platformSession = session._platformSession; + if (this._platformSession !== platformSession) { + this._resetSessionState(); + this._platformSession = platformSession; + } if (this._lightProbe) { + this._ensureReflectionBinding(session); return true; } - if (!this._probeRequestInFlight && !this._probeRequestFailed && session._platformSession.requestLightProbe) { + if (!this._probeRequestInFlight && !this._probeRequestFailed && platformSession.requestLightProbe) { this._probeRequestInFlight = true; - session._platformSession - .requestLightProbe() + const lightProbeOptions: XRLightProbeInit = {}; + const preferredReflectionFormat = platformSession.preferredReflectionFormat; + preferredReflectionFormat && (lightProbeOptions.reflectionFormat = preferredReflectionFormat); + platformSession + .requestLightProbe(Object.keys(lightProbeOptions).length ? lightProbeOptions : undefined) .then((probe: XRLightProbe) => { + if (this._platformSession !== platformSession) { + return; + } this._lightProbe = probe; + probe.addEventListener?.("reflectionchange", this._onReflectionChange); + this._reflectionChanged = true; + this._ensureReflectionBinding(session); }) .catch((error: unknown) => { this._probeRequestFailed = true; @@ -39,34 +65,120 @@ export class WebXRLightEstimation extends WebXRFeature implements IXRLightEstima return false; } - getLightEstimate(_session: WebXRSession, frame: WebXRFrame, estimate: IXRLightEstimate): boolean { + getLightEstimate(session: WebXRSession, frame: WebXRFrame, estimate: IXRLightEstimate): boolean { if (!this._lightProbe) { return false; } - const platformEstimate = frame._platformFrame.getLightEstimate(this._lightProbe); - if (!platformEstimate) { - return false; - } let updated = false; - const coefficients = platformEstimate.sphericalHarmonicsCoefficients; - if (coefficients && coefficients.length >= 27) { - estimate.sphericalHarmonics.copyFromArray(coefficients); - updated = true; - } - const direction = platformEstimate.primaryLightDirection; - if (direction) { - estimate.primaryLightDirection.set(direction.x, direction.y, direction.z); - updated = true; - } - const intensity = platformEstimate.primaryLightIntensity; - if (intensity) { - estimate.primaryLightIntensity.set(intensity.x, intensity.y, intensity.z, 1); + const platformEstimate = frame._platformFrame.getLightEstimate(this._lightProbe); + if (platformEstimate) { + const coefficients = platformEstimate.sphericalHarmonicsCoefficients; + if (coefficients && coefficients.length >= 27) { + estimate.sphericalHarmonics.copyFromArray(coefficients); + updated = true; + } + const direction = platformEstimate.primaryLightDirection; + if (direction) { + estimate.primaryLightDirection.set(direction.x, direction.y, direction.z); + updated = true; + } + const intensity = platformEstimate.primaryLightIntensity; + if (intensity) { + estimate.primaryLightIntensity.set(intensity.x, intensity.y, intensity.z, 1); + updated = true; + } + } + + this._ensureReflectionBinding(session); + if (this._reflectionBinding?.getReflectionCubeMap) { + if (this._reflectionChanged || !estimate.reflectionCubeMap) { + const reflectionCubeMap = this._reflectionBinding.getReflectionCubeMap(this._lightProbe); + this._reflectionChanged = false; + + if (estimate.reflectionCubeMap !== reflectionCubeMap) { + estimate.reflectionCubeMap = reflectionCubeMap; + updated = true; + } + + if (reflectionCubeMap) { + const { size, mipmapCount } = this._queryReflectionCubeMapInfo(session._gl, reflectionCubeMap); + if (estimate.reflectionCubeMapSize !== size || estimate.reflectionCubeMapMipmapCount !== mipmapCount) { + estimate.reflectionCubeMapSize = size; + estimate.reflectionCubeMapMipmapCount = mipmapCount; + updated = true; + } + } else if (estimate.reflectionCubeMapSize !== 0 || estimate.reflectionCubeMapMipmapCount !== 0) { + estimate.reflectionCubeMapSize = 0; + estimate.reflectionCubeMapMipmapCount = 0; + updated = true; + } + } + } else if (estimate.reflectionCubeMap || estimate.reflectionCubeMapSize !== 0 || estimate.reflectionCubeMapMipmapCount !== 0) { + estimate.reflectionCubeMap = null; + estimate.reflectionCubeMapSize = 0; + estimate.reflectionCubeMapMipmapCount = 0; updated = true; } + return updated; } _assembleOptions(options: XRSessionInit): void { options.optionalFeatures.push("light-estimation"); } + + private _ensureReflectionBinding(session: WebXRSession): void { + if (!this._lightProbe || this._reflectionBinding || typeof XRWebGLBinding === "undefined") { + return; + } + try { + this._reflectionBinding = new XRWebGLBinding(session._platformSession, session._gl); + this._reflectionChanged = true; + } catch (error) { + console.warn("WebXR light estimation reflection binding failed.", error); + } + } + + private _queryReflectionCubeMapInfo( + gl: WebGLRenderingContext | WebGL2RenderingContext, + reflectionCubeMap: WebGLTexture + ): { size: number; mipmapCount: number } { + let size = 1; + let mipmapCount = 1; + const webgl2 = gl as IWebGL2CubeTextureQueryContext; + + if ((gl as WebGL2RenderingContext).texStorage2D) { + const previousBinding = gl.getParameter(gl.TEXTURE_BINDING_CUBE_MAP) as WebGLTexture | null; + const textureWidth = 0x1000; + gl.bindTexture(gl.TEXTURE_CUBE_MAP, reflectionCubeMap); + try { + size = webgl2.getTexLevelParameter(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, textureWidth) || 1; + mipmapCount = 0; + for (let mipLevel = 0; mipLevel < 16; mipLevel++) { + const levelSize = webgl2.getTexLevelParameter(gl.TEXTURE_CUBE_MAP_POSITIVE_X, mipLevel, textureWidth) || 0; + if (levelSize <= 0) { + break; + } + mipmapCount++; + if (levelSize === 1) { + break; + } + } + mipmapCount = Math.max(mipmapCount, 1); + } finally { + gl.bindTexture(gl.TEXTURE_CUBE_MAP, previousBinding); + } + } + + return { size: Math.max(size, 1), mipmapCount: Math.max(mipmapCount, 1) }; + } + + private _resetSessionState(): void { + this._lightProbe?.removeEventListener?.("reflectionchange", this._onReflectionChange); + this._lightProbe = null; + this._probeRequestInFlight = false; + this._probeRequestFailed = false; + this._reflectionBinding = null; + this._reflectionChanged = true; + } } diff --git a/packages/xr-webxr/src/typings/webxr-light-estimation.d.ts b/packages/xr-webxr/src/typings/webxr-light-estimation.d.ts index 3deb0bab5..83c431603 100644 --- a/packages/xr-webxr/src/typings/webxr-light-estimation.d.ts +++ b/packages/xr-webxr/src/typings/webxr-light-estimation.d.ts @@ -1,7 +1,10 @@ export {}; declare global { - interface XRLightProbe extends EventTarget {} + interface XRLightProbe extends EventTarget { + addEventListener(type: "reflectionchange", listener: EventListenerOrEventListenerObject): void; + removeEventListener(type: "reflectionchange", listener: EventListenerOrEventListenerObject): void; + } interface XRLightEstimate { readonly sphericalHarmonicsCoefficients?: Float32Array; @@ -14,10 +17,15 @@ declare global { } interface XRSession { + readonly preferredReflectionFormat?: string; requestLightProbe(options?: XRLightProbeInit): Promise; } interface XRFrame { getLightEstimate(lightProbe: XRLightProbe): XRLightEstimate | null; } + + interface XRWebGLBinding { + getReflectionCubeMap(lightProbe: XRLightProbe): WebGLTexture | null; + } } diff --git a/packages/xr/package.json b/packages/xr/package.json index f2d83c0a4..67a113931 100644 --- a/packages/xr/package.json +++ b/packages/xr/package.json @@ -28,6 +28,9 @@ "libs/**/*", "types/**/*" ], + "dependencies": { + "@galacean/engine-math": "workspace:*" + }, "devDependencies": { "@galacean/engine-design": "workspace:*", "@galacean/engine": "workspace:*" diff --git a/packages/xr/src/feature/lightEstimation/XRLightEstimate.ts b/packages/xr/src/feature/lightEstimation/XRLightEstimate.ts index 50927392e..4da7431c5 100644 --- a/packages/xr/src/feature/lightEstimation/XRLightEstimate.ts +++ b/packages/xr/src/feature/lightEstimation/XRLightEstimate.ts @@ -5,4 +5,7 @@ export class XRLightEstimate implements IXRLightEstimate { sphericalHarmonics: SphericalHarmonics3 = new SphericalHarmonics3(); primaryLightDirection: Vector3 = new Vector3(); primaryLightIntensity: Color = new Color(1, 1, 1, 1); + reflectionCubeMap: unknown | null = null; + reflectionCubeMapSize: number = 0; + reflectionCubeMapMipmapCount: number = 0; } diff --git a/packages/xr/src/feature/lightEstimation/XRLightEstimation.ts b/packages/xr/src/feature/lightEstimation/XRLightEstimation.ts index a67918739..e8d73a39a 100644 --- a/packages/xr/src/feature/lightEstimation/XRLightEstimation.ts +++ b/packages/xr/src/feature/lightEstimation/XRLightEstimation.ts @@ -1,5 +1,5 @@ import { IXRLightEstimationPlatformFeature } from "@galacean/engine-design"; -import { registerXRFeature } from "../../XRManagerExtended"; +import { XRManagerExtended, registerXRFeature } from "../../XRManagerExtended"; import { XRFeature } from "../XRFeature"; import { XRFeatureType } from "../XRFeatureType"; import { XRLightEstimate } from "./XRLightEstimate"; @@ -12,6 +12,10 @@ export class XRLightEstimation extends XRFeature