Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions packages/dev/loaders/src/glTF/2.0/Extensions/EXT_lights_area.ts
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely should be marked experimental as the spec hasn't been merged yet. The link also doesn't work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I'll fix this and add a test when I update this importer to match the new spec.

*/
// 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<Promise<TransformNode>> {
return GLTFLoader.LoadExtensionAsync<IEXTLightsArea_LightReference, TransformNode>(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));
1 change: 1 addition & 0 deletions packages/dev/loaders/src/glTF/2.0/Extensions/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
7 changes: 7 additions & 0 deletions packages/dev/loaders/src/glTF/2.0/glTFLoaderInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
31 changes: 31 additions & 0 deletions packages/public/glTF2Interface/babylon.glTF2Interface.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down