From 3edeb325fdffeffa6380666cf7023d5f9c355e44 Mon Sep 17 00:00:00 2001 From: Mike Bond Date: Fri, 12 Sep 2025 13:16:50 -0700 Subject: [PATCH] Add import support for EXT_lights_area --- .../glTF/2.0/Extensions/EXT_lights_area.ts | 138 ++++++++++++++++++ .../loaders/src/glTF/2.0/Extensions/index.ts | 1 + .../src/glTF/2.0/glTFLoaderInterfaces.ts | 7 + .../babylon.glTF2Interface.d.ts | 31 ++++ 4 files changed, 177 insertions(+) create mode 100644 packages/dev/loaders/src/glTF/2.0/Extensions/EXT_lights_area.ts diff --git a/packages/dev/loaders/src/glTF/2.0/Extensions/EXT_lights_area.ts b/packages/dev/loaders/src/glTF/2.0/Extensions/EXT_lights_area.ts new file mode 100644 index 00000000000..bebe5f5c2fc --- /dev/null +++ b/packages/dev/loaders/src/glTF/2.0/Extensions/EXT_lights_area.ts @@ -0,0 +1,138 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { Nullable } from "core/types"; +import { Vector3, Quaternion } from "core/Maths/math.vector"; +import { Color3 } from "core/Maths/math.color"; +import { Light } from "core/Lights/light"; +import { RectAreaLight } from "core/Lights/rectAreaLight"; +import type { TransformNode } from "core/Meshes/transformNode"; +import { TransformNode as BabylonTransformNode } from "core/Meshes/transformNode"; + +import type { IEXTLightsArea_LightReference } from "babylonjs-gltf2interface"; +import { EXTLightsArea_LightShape } from "babylonjs-gltf2interface"; +import type { INode, IEXTLightsArea_Light } from "../glTFLoaderInterfaces"; +import type { IGLTFLoaderExtension } from "../glTFLoaderExtension"; +import { GLTFLoader, ArrayItem } from "../glTFLoader"; +import { registerGLTFExtension, unregisterGLTFExtension } from "../glTFLoaderExtensionRegistry"; + +const NAME = "EXT_lights_area"; + +declare module "../../glTFFileLoader" { + // eslint-disable-next-line jsdoc/require-jsdoc, @typescript-eslint/naming-convention + export interface GLTFLoaderExtensionOptions { + /** + * Defines options for the EXT_lights_area extension. + */ + // NOTE: Don't use NAME here as it will break the UMD type declarations. + ["EXT_lights_area"]: {}; + } +} + +/** + * [Specification](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/EXT_lights_area/README.md) + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export class EXT_lights_area implements IGLTFLoaderExtension { + /** + * The name of this extension. + */ + public readonly name = NAME; + + /** + * Defines whether this extension is enabled. + */ + public enabled: boolean; + + /** hidden */ + private _loader: GLTFLoader; + private _lights?: IEXTLightsArea_Light[]; + + /** + * @internal + */ + constructor(loader: GLTFLoader) { + this._loader = loader; + this.enabled = this._loader.isExtensionUsed(NAME); + } + + /** @internal */ + public dispose() { + (this._loader as any) = null; + delete this._lights; + } + + /** @internal */ + public onLoading(): void { + const extensions = this._loader.gltf.extensions; + if (extensions && extensions[this.name]) { + const extension = extensions[this.name]; + this._lights = extension.lights; + ArrayItem.Assign(this._lights); + } + } + + /** + * @internal + */ + // eslint-disable-next-line no-restricted-syntax + public loadNodeAsync(context: string, node: INode, assign: (babylonTransformNode: TransformNode) => void): Nullable> { + return GLTFLoader.LoadExtensionAsync(context, node, this.name, async (extensionContext, extension) => { + this._loader._allMaterialsDirtyRequired = true; + + return await this._loader.loadNodeAsync(context, node, (babylonMesh) => { + let babylonLight: Light; + + const light = ArrayItem.Get(extensionContext, this._lights, extension.light); + const name = light.name || babylonMesh.name; + + this._loader.babylonScene._blockEntityCollection = !!this._loader._assetContainer; + + switch (light.shape) { + case EXTLightsArea_LightShape.RECT: { + const width = light.width !== undefined ? light.width : 1.0; + const height = light.height !== undefined ? light.height : 1.0; + const babylonRectAreaLight = new RectAreaLight(name, Vector3.Zero(), width, height, this._loader.babylonScene); + babylonLight = babylonRectAreaLight; + break; + } + case EXTLightsArea_LightShape.DISK: { + // For disk lights, we'll use RectAreaLight with equal width and height to approximate a square area + // In the future, this could be extended to support actual disk area lights + const radius = light.radius !== undefined ? light.radius : 0.5; + const size = radius * 2; // Convert radius to square size + const babylonRectAreaLight = new RectAreaLight(name, Vector3.Zero(), size, size, this._loader.babylonScene); + babylonLight = babylonRectAreaLight; + break; + } + default: { + this._loader.babylonScene._blockEntityCollection = false; + throw new Error(`${extensionContext}: Invalid area light shape (${light.shape})`); + } + } + + babylonLight._parentContainer = this._loader._assetContainer; + this._loader.babylonScene._blockEntityCollection = false; + light._babylonLight = babylonLight; + + babylonLight.falloffType = Light.FALLOFF_GLTF; + babylonLight.diffuse = light.color ? Color3.FromArray(light.color) : Color3.White(); + babylonLight.intensity = light.intensity == undefined ? 1 : light.intensity; + + // glTF EXT_lights_area specifies lights face down -Z, but Babylon.js area lights face down +Z + // Create a parent transform node with 180-degree rotation around Y axis to flip the direction + const lightParentNode = new BabylonTransformNode(`${name}_orientation`, this._loader.babylonScene); + lightParentNode.rotationQuaternion = Quaternion.RotationAxis(Vector3.Up(), Math.PI); + lightParentNode.parent = babylonMesh; + babylonLight.parent = lightParentNode; + + this._loader._babylonLights.push(babylonLight); + + GLTFLoader.AddPointerMetadata(babylonLight, extensionContext); + + assign(babylonMesh); + }); + }); + } +} + +unregisterGLTFExtension(NAME); +registerGLTFExtension(NAME, true, (loader) => new EXT_lights_area(loader)); diff --git a/packages/dev/loaders/src/glTF/2.0/Extensions/index.ts b/packages/dev/loaders/src/glTF/2.0/Extensions/index.ts index f6d80d8a0c4..c533de0afcb 100644 --- a/packages/dev/loaders/src/glTF/2.0/Extensions/index.ts +++ b/packages/dev/loaders/src/glTF/2.0/Extensions/index.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-restricted-imports */ export * from "./objectModelMapping"; +export * from "./EXT_lights_area"; export * from "./EXT_lights_image_based"; export * from "./EXT_mesh_gpu_instancing"; export * from "./EXT_meshopt_compression"; diff --git a/packages/dev/loaders/src/glTF/2.0/glTFLoaderInterfaces.ts b/packages/dev/loaders/src/glTF/2.0/glTFLoaderInterfaces.ts index d120662d5dd..72b04109254 100644 --- a/packages/dev/loaders/src/glTF/2.0/glTFLoaderInterfaces.ts +++ b/packages/dev/loaders/src/glTF/2.0/glTFLoaderInterfaces.ts @@ -312,3 +312,10 @@ export interface IEXTLightsIES_Light extends GLTF2.IEXTLightsIES_Light, IArrayIt /** @hidden */ _babylonLight?: Light; } + +/** @internal */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface IEXTLightsArea_Light extends GLTF2.IEXTLightsArea_Light, IArrayItem { + /** @hidden */ + _babylonLight?: Light; +} diff --git a/packages/public/glTF2Interface/babylon.glTF2Interface.d.ts b/packages/public/glTF2Interface/babylon.glTF2Interface.d.ts index 9df38c49822..c001f57e53f 100644 --- a/packages/public/glTF2Interface/babylon.glTF2Interface.d.ts +++ b/packages/public/glTF2Interface/babylon.glTF2Interface.d.ts @@ -1388,6 +1388,37 @@ declare module BABYLON.GLTF2 { filter?: "NONE" | "OCTAHEDRAL" | "QUATERNION" | "EXPONENTIAL"; } + /** + * Interfaces from the EXT_lights_area extension + */ + + /** @internal */ + const enum EXTLightsArea_LightShape { + RECT = "rect", + DISK = "disk" + } + + /** @internal */ + interface IEXTLightsArea_LightReference { + light: number; + } + + /** @internal */ + interface IEXTLightsArea_Light extends IChildRootProperty { + shape: EXTLightsArea_LightShape; + color?: number[]; + intensity?: number; + type?: "area"; + width?: number; + height?: number; + radius?: number; + } + + /** @internal */ + interface IEXTLightsArea { + lights: IEXTLightsArea_Light[]; + } + /** * Interfaces for the KHR_interactivity extension */