feat: add light estimation

This commit is contained in:
cptbtptpbcptdtptp
2026-02-24 10:37:49 +08:00
parent 8e423bff3a
commit 0c889b0bf0
17 changed files with 709 additions and 104 deletions

View File

@@ -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 }) => {

View File

@@ -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"
}

View File

@@ -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<void> {
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;
}

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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?.();
}
}

View File

@@ -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;
}

View File

@@ -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.
*/

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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<XRLightProbe>;
}
interface XRFrame {
getLightEstimate(lightProbe: XRLightProbe): XRLightEstimate | null;
}
interface XRWebGLBinding {
getReflectionCubeMap(lightProbe: XRLightProbe): WebGLTexture | null;
}
}

View File

@@ -28,6 +28,9 @@
"libs/**/*",
"types/**/*"
],
"dependencies": {
"@galacean/engine-math": "workspace:*"
},
"devDependencies": {
"@galacean/engine-design": "workspace:*",
"@galacean/engine": "workspace:*"

View File

@@ -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;
}

View File

@@ -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<IXRLightEstimationPlatformFeatu
private _estimate: XRLightEstimate = new XRLightEstimate();
private _available: boolean = false;
constructor(xrManager: XRManagerExtended) {
super(xrManager, XRFeatureType.LightEstimation);
}
/**
* The latest light estimation data.
*/