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
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,11 @@ export class GLTFComponent extends React.Component<IGLTFComponentProps, IGLTFCom
isSelected={() => extensionStates["KHR_lights_punctual"].enabled}
onSelect={(value) => (extensionStates["KHR_lights_punctual"].enabled = value)}
/>
<CheckBoxLineComponent
label="EXT_lights_area"
isSelected={() => extensionStates["EXT_lights_area"].enabled}
onSelect={(value) => (extensionStates["EXT_lights_area"].enabled = value)}
/>
<CheckBoxLineComponent
label="KHR_texture_basisu"
isSelected={() => extensionStates["KHR_texture_basisu"].enabled}
Expand Down
1 change: 1 addition & 0 deletions packages/dev/inspector/src/components/globalState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class GlobalState {
KHR_materials_volume: { enabled: true },
KHR_materials_dispersion: { enabled: true },
KHR_lights_punctual: { enabled: true },
EXT_lights_area: { enabled: true },
EXT_lights_ies: { enabled: true },
KHR_texture_basisu: { enabled: true },
KHR_texture_transform: { enabled: true },
Expand Down
22 changes: 11 additions & 11 deletions packages/dev/loaders/src/glTF/2.0/Extensions/EXT_lights_area.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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 { EXTLightsArea_LightType } from "babylonjs-gltf2interface";
import type { INode, IEXTLightsArea_Light } from "../glTFLoaderInterfaces";
import type { IGLTFLoaderExtension } from "../glTFLoaderExtension";
import { GLTFLoader, ArrayItem } from "../glTFLoader";
Expand Down Expand Up @@ -85,27 +85,27 @@ export class EXT_lights_area implements IGLTFLoaderExtension {
const name = light.name || babylonMesh.name;

this._loader.babylonScene._blockEntityCollection = !!this._loader._assetContainer;
const size = light.size !== undefined ? light.size : 1.0;

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;
switch (light.type) {
case EXTLightsArea_LightType.RECT: {
const width = light.rect?.aspect !== undefined ? light.rect.aspect * size : size;
const height = size;
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
case EXTLightsArea_LightType.DISK: {
// For disk lights, we'll use a rectangle light with the same area to approximate the disk light
// 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);
const newSize = Math.sqrt(size * size * 0.25 * Math.PI); // Area of the disk
const babylonRectAreaLight = new RectAreaLight(name, Vector3.Zero(), newSize, newSize, this._loader.babylonScene);
babylonLight = babylonRectAreaLight;
break;
}
default: {
this._loader.babylonScene._blockEntityCollection = false;
throw new Error(`${extensionContext}: Invalid area light shape (${light.shape})`);
throw new Error(`${extensionContext}: Invalid area light type (${light.type})`);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,5 +324,10 @@ SetInterpolationForKey("/extensions/KHR_lights_punctual/lights/{}/spot/outerCone
new LightAnimationPropertyInfo(Animation.ANIMATIONTYPE_FLOAT, "angle", getFloatBy2, () => 1),
]);

SetInterpolationForKey("/extensions/EXT_lights_area/lights/{}/color", [new LightAnimationPropertyInfo(Animation.ANIMATIONTYPE_COLOR3, "diffuse", getColor3, () => 3)]);
SetInterpolationForKey("/extensions/EXT_lights_area/lights/{}/intensity", [new LightAnimationPropertyInfo(Animation.ANIMATIONTYPE_FLOAT, "intensity", getFloat, () => 1)]);
SetInterpolationForKey("/extensions/EXT_lights_area/lights/{}/size", [new LightAnimationPropertyInfo(Animation.ANIMATIONTYPE_FLOAT, "radius", getFloat, () => 1)]);
SetInterpolationForKey("/extensions/EXT_lights_area/lights/{}/rect/aspect", [new LightAnimationPropertyInfo(Animation.ANIMATIONTYPE_FLOAT, "radius", getFloat, () => 1)]);

SetInterpolationForKey("/nodes/{}/extensions/EXT_lights_ies/color", [new LightAnimationPropertyInfo(Animation.ANIMATIONTYPE_COLOR3, "diffuse", getColor3, () => 3)]);
SetInterpolationForKey("/nodes/{}/extensions/EXT_lights_ies/multiplier", [new LightAnimationPropertyInfo(Animation.ANIMATIONTYPE_FLOAT, "intensity", getFloat, () => 1)]);
5 changes: 5 additions & 0 deletions packages/dev/loaders/src/glTF/2.0/Extensions/dynamic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export function registerBuiltInGLTFExtensions() {
return new KHR_lights(loader);
});

registerGLTFExtension("EXT_lights_area", true, async (loader) => {
const { EXT_lights_area } = await import("./EXT_lights_area");
return new EXT_lights_area(loader);
});

registerGLTFExtension("EXT_lights_ies", true, async (loader) => {
const { EXT_lights_ies } = await import("./EXT_lights_ies");
return new EXT_lights_ies(loader);
Expand Down
2 changes: 1 addition & 1 deletion packages/dev/loaders/src/glTF/2.0/Extensions/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* 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 All @@ -9,6 +8,7 @@ export * from "./EXT_texture_avif";
export * from "./EXT_lights_ies";
export * from "./KHR_draco_mesh_compression";
export * from "./KHR_lights_punctual";
export * from "./EXT_lights_area";
export * from "./KHR_materials_pbrSpecularGlossiness";
export * from "./KHR_materials_unlit";
export * from "./KHR_materials_clearcoat";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */

import type { TransformNode } from "core/Meshes/transformNode";
import type { IAnimation, ICamera, IGLTF, IKHRLightsPunctual_Light, IMaterial, IMesh, INode } from "../glTFLoaderInterfaces";
import type { IAnimation, ICamera, IGLTF, IKHRLightsPunctual_Light, IEXTLightsArea_Light, IMaterial, IMesh, INode } from "../glTFLoaderInterfaces";
import type { Vector3 } from "core/Maths/math.vector";
import { Matrix, Quaternion, Vector2 } from "core/Maths/math.vector";
import { Constants } from "core/Engines/constants";
Expand All @@ -17,6 +17,7 @@ import type { IInterpolationPropertyInfo, IObjectAccessor } from "core/FlowGraph
import { GLTFPathToObjectConverter } from "./gltfPathToObjectConverter";
import type { AnimationGroup } from "core/Animations/animationGroup";
import type { Mesh } from "core/Meshes/mesh";
import type { RectAreaLight } from "core/Lights/rectAreaLight";

export interface IGLTFObjectModelTree {
cameras: IGLTFObjectModelTreeCamerasObject;
Expand Down Expand Up @@ -252,6 +253,20 @@ export interface IGLTFObjectModelTreeExtensionsObject {
};
};
};
EXT_lights_area: {
lights: {
length: IObjectAccessor<IEXTLightsArea_Light[], Light[], number>;
__array__: {
__target__: boolean;
color: IObjectAccessor<IEXTLightsArea_Light, Light, Color3>;
intensity: IObjectAccessor<IEXTLightsArea_Light, Light, number>;
size: IObjectAccessor<IEXTLightsArea_Light, Light, number>;
rect: {
aspect: IObjectAccessor<IEXTLightsArea_Light, Light, number>;
};
};
};
};
EXT_lights_ies: {
lights: {
length: IObjectAccessor<IKHRLightsPunctual_Light[], Light[], number>;
Expand Down Expand Up @@ -903,6 +918,50 @@ const extensionsTree: IGLTFObjectModelTreeExtensionsObject = {
},
},
},
EXT_lights_area: {
lights: {
length: {
type: "number",
get: (lights: IEXTLightsArea_Light[]) => lights.length,
getTarget: (lights: IEXTLightsArea_Light[]) => lights.map((light) => light._babylonLight!),
getPropertyName: [(_lights: IEXTLightsArea_Light[]) => "length"],
},
__array__: {
__target__: true,
color: {
type: "Color3",
get: (light: IEXTLightsArea_Light) => light._babylonLight?.diffuse,
set: (value: Color3, light: IEXTLightsArea_Light) => light._babylonLight?.diffuse.copyFrom(value),
getTarget: (light: IEXTLightsArea_Light) => light._babylonLight,
getPropertyName: [(_light: IEXTLightsArea_Light) => "diffuse"],
},
intensity: {
type: "number",
get: (light: IEXTLightsArea_Light) => light._babylonLight?.intensity,
set: (value: number, light: IEXTLightsArea_Light) => (light._babylonLight ? (light._babylonLight.intensity = value) : undefined),
getTarget: (light: IEXTLightsArea_Light) => light._babylonLight,
getPropertyName: [(_light: IEXTLightsArea_Light) => "intensity"],
},
size: {
type: "number",
get: (light: IEXTLightsArea_Light) => (light._babylonLight as RectAreaLight)?.height,
set: (value: number, light: IEXTLightsArea_Light) => (light._babylonLight ? ((light._babylonLight as RectAreaLight).height = value) : undefined),
getTarget: (light: IEXTLightsArea_Light) => light._babylonLight,
getPropertyName: [(_light: IEXTLightsArea_Light) => "size"],
},
rect: {
aspect: {
type: "number",
get: (light: IEXTLightsArea_Light) => (light._babylonLight as RectAreaLight)?.width / (light._babylonLight as RectAreaLight)?.height,
set: (value: number, light: IEXTLightsArea_Light) =>
light._babylonLight ? ((light._babylonLight as RectAreaLight).width = value * (light._babylonLight as RectAreaLight).height) : undefined,
getTarget: (light: IEXTLightsArea_Light) => light._babylonLight,
getPropertyName: [(_light: IEXTLightsArea_Light) => "aspect"],
},
},
},
},
},
EXT_lights_ies: {
lights: {
length: {
Expand Down
166 changes: 166 additions & 0 deletions packages/dev/serializers/src/glTF/2.0/Extensions/EXT_lights_area.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import type { Nullable } from "core/types";
import { Vector3, Quaternion, TmpVectors } from "core/Maths/math.vector";
import { Light } from "core/Lights/light";
import type { Node } from "core/node";
import type { INode, IEXTLightsArea_LightReference, IEXTLightsArea_Light, IEXTLightsArea } from "babylonjs-gltf2interface";
import { EXTLightsArea_LightType } from "babylonjs-gltf2interface";
import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension";
import { GLTFExporter } from "../glTFExporter";
import { Logger } from "core/Misc/logger";
import { ConvertToRightHandedPosition, OmitDefaultValues, CollapseChildIntoParent, IsChildCollapsible } from "../glTFUtilities";
import type { RectAreaLight } from "core/Lights/rectAreaLight";

const NAME = "EXT_lights_area";
const DEFAULTS: Omit<IEXTLightsArea_Light, "type"> = {
name: "",
color: [1, 1, 1],
intensity: 1,
size: 1,
};
const RECTDEFAULTS: NonNullable<IEXTLightsArea_Light["rect"]> = {
aspect: 1,
};
const LIGHTDIRECTION = Vector3.Backward();

/**
* [Specification](https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/EXT_lights_area/README.md)
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export class EXT_lights_area implements IGLTFExporterExtensionV2 {
/** The name of this extension. */
public readonly name = NAME;

/** Defines whether this extension is enabled. */
public enabled = true;

/** Defines whether this extension is required */
public required = false;

/** Reference to the glTF exporter */
private _exporter: GLTFExporter;

private _lights: IEXTLightsArea;

/**
* @internal
*/
constructor(exporter: GLTFExporter) {
this._exporter = exporter;
}

/** @internal */
public dispose() {
(this._lights as any) = null;
}

/** @internal */
public get wasUsed() {
return !!this._lights;
}

/** @internal */
public onExporting(): void {
this._exporter._glTF.extensions![NAME] = this._lights;
}
/**
* Define this method to modify the default behavior when exporting a node
* @param context The context when exporting the node
* @param node glTF node
* @param babylonNode BabylonJS node
* @param nodeMap Node mapping of babylon node to glTF node index
* @param convertToRightHanded Flag to convert the values to right-handed
* @returns nullable INode promise
*/
public async postExportNodeAsync(context: string, node: INode, babylonNode: Node, nodeMap: Map<Node, number>, convertToRightHanded: boolean): Promise<Nullable<INode>> {
return await new Promise((resolve) => {
if (!(babylonNode instanceof Light)) {
resolve(node);
return;
}

const lightType = babylonNode.getTypeID() == Light.LIGHTTYPEID_RECT_AREALIGHT ? EXTLightsArea_LightType.RECT : null;
if (!lightType) {
Logger.Warn(`${context}: Light ${babylonNode.name} is not supported in ${NAME}`);
resolve(node);
return;
}

const areaLight = babylonNode as RectAreaLight;

if (areaLight.falloffType !== Light.FALLOFF_GLTF) {
Logger.Warn(`${context}: Light falloff for ${babylonNode.name} does not match the ${NAME} specification!`);
}

// Set the node's translation and rotation here, since lights are not handled in exportNodeAsync
if (!areaLight.position.equalsToFloats(0, 0, 0)) {
const translation = TmpVectors.Vector3[0].copyFrom(areaLight.position);
if (convertToRightHanded) {
ConvertToRightHandedPosition(translation);
}
node.translation = translation.asArray();
}

// Represent the Babylon light's direction as a quaternion
// relative to glTF lights' forward direction, (0, 0, -1).
const direction = Vector3.Forward();
if (convertToRightHanded) {
ConvertToRightHandedPosition(direction);
}

const lightRotationQuaternion = Quaternion.FromUnitVectorsToRef(LIGHTDIRECTION, direction, TmpVectors.Quaternion[0]);
if (!Quaternion.IsIdentity(lightRotationQuaternion)) {
node.rotation = lightRotationQuaternion.asArray();
}

const light: IEXTLightsArea_Light = {
type: lightType,
name: areaLight.name,
color: areaLight.diffuse.asArray(),
intensity: areaLight.intensity,
size: areaLight.height,
rect: {
aspect: areaLight.width / areaLight.height,
},
};
OmitDefaultValues(light, DEFAULTS);

if (light.rect) {
OmitDefaultValues(light.rect, RECTDEFAULTS);
}

this._lights ||= {
lights: [],
};
this._lights.lights.push(light);

const lightReference: IEXTLightsArea_LightReference = {
light: this._lights.lights.length - 1,
};

// Assign the light to its parent node, if possible, to condense the glTF
// Why and when: the glTF loader generates a new parent TransformNode for each light node, which we should undo on export
const parentBabylonNode = babylonNode.parent;

if (parentBabylonNode && IsChildCollapsible(areaLight, parentBabylonNode)) {
const parentNodeIndex = nodeMap.get(parentBabylonNode);
if (parentNodeIndex) {
// Combine the light's transformation with the parent's
const parentNode = this._exporter._nodes[parentNodeIndex];
CollapseChildIntoParent(node, parentNode);
parentNode.extensions ||= {};
parentNode.extensions[NAME] = lightReference;

// Do not export the original node
resolve(null);
return;
}
}

node.extensions ||= {};
node.extensions[NAME] = lightReference;
resolve(node);
});
}
}

GLTFExporter.RegisterExtension(NAME, (exporter) => new EXT_lights_area(exporter));
1 change: 1 addition & 0 deletions packages/dev/serializers/src/glTF/2.0/Extensions/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./EXT_mesh_gpu_instancing";
export * from "./KHR_draco_mesh_compression";
export * from "./KHR_lights_punctual";
export * from "./EXT_lights_area";
export * from "./KHR_materials_anisotropy";
export * from "./KHR_materials_clearcoat";
export * from "./KHR_materials_clearcoat_darkening";
Expand Down
3 changes: 2 additions & 1 deletion packages/dev/serializers/src/glTF/2.0/glTFUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { TargetCamera } from "core/Cameras/targetCamera";
import type { ShadowLight } from "core/Lights/shadowLight";
import { Epsilon } from "core/Maths/math.constants";
import { ConvertHandednessMatrix } from "../../exportUtils";
import type { AreaLight } from "core/Lights/areaLight";

// Default values for comparison.
export const DefaultTranslation = Vector3.Zero();
Expand Down Expand Up @@ -301,7 +302,7 @@ export function CollapseChildIntoParent(node: INode, parentNode: INode): void {
* @param parentBabylonNode Target Babylon parent node.
* @returns True if the two nodes can be merged, false otherwise.
*/
export function IsChildCollapsible(babylonNode: ShadowLight | TargetCamera, parentBabylonNode: Node): boolean {
export function IsChildCollapsible(babylonNode: ShadowLight | TargetCamera | AreaLight, parentBabylonNode: Node): boolean {
if (!(parentBabylonNode instanceof TransformNode)) {
return false;
}
Expand Down
Loading