mirror of
https://github.com/galacean/engine.git
synced 2026-05-31 15:51:33 +08:00
feat: add light estimation
This commit is contained in:
@@ -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 }) => {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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?.();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
"libs/**/*",
|
||||
"types/**/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@galacean/engine-math": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@galacean/engine-design": "workspace:*",
|
||||
"@galacean/engine": "workspace:*"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user