Skip to content

Commit 7ca8939

Browse files
authored
EXT_lights_area updates and export support (#17312)
Modifications to the area lights extension (replacing "shape" with "type" property, etc.) and adding export support.
1 parent a56a10c commit 7ca8939

File tree

12 files changed

+317
-21
lines changed

12 files changed

+317
-21
lines changed

packages/dev/inspector/src/components/actionTabs/tabs/tools/gltfComponent.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,11 @@ export class GLTFComponent extends React.Component<IGLTFComponentProps, IGLTFCom
289289
isSelected={() => extensionStates["KHR_lights_punctual"].enabled}
290290
onSelect={(value) => (extensionStates["KHR_lights_punctual"].enabled = value)}
291291
/>
292+
<CheckBoxLineComponent
293+
label="EXT_lights_area"
294+
isSelected={() => extensionStates["EXT_lights_area"].enabled}
295+
onSelect={(value) => (extensionStates["EXT_lights_area"].enabled = value)}
296+
/>
292297
<CheckBoxLineComponent
293298
label="KHR_texture_basisu"
294299
isSelected={() => extensionStates["KHR_texture_basisu"].enabled}

packages/dev/inspector/src/components/globalState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export class GlobalState {
5858
KHR_materials_volume: { enabled: true },
5959
KHR_materials_dispersion: { enabled: true },
6060
KHR_lights_punctual: { enabled: true },
61+
EXT_lights_area: { enabled: true },
6162
EXT_lights_ies: { enabled: true },
6263
KHR_texture_basisu: { enabled: true },
6364
KHR_texture_transform: { enabled: true },

packages/dev/loaders/src/glTF/2.0/Extensions/EXT_lights_area.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { TransformNode } from "core/Meshes/transformNode";
88
import { TransformNode as BabylonTransformNode } from "core/Meshes/transformNode";
99

1010
import type { IEXTLightsArea_LightReference } from "babylonjs-gltf2interface";
11-
import { EXTLightsArea_LightShape } from "babylonjs-gltf2interface";
11+
import { EXTLightsArea_LightType } from "babylonjs-gltf2interface";
1212
import type { INode, IEXTLightsArea_Light } from "../glTFLoaderInterfaces";
1313
import type { IGLTFLoaderExtension } from "../glTFLoaderExtension";
1414
import { GLTFLoader, ArrayItem } from "../glTFLoader";
@@ -85,27 +85,27 @@ export class EXT_lights_area implements IGLTFLoaderExtension {
8585
const name = light.name || babylonMesh.name;
8686

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

89-
switch (light.shape) {
90-
case EXTLightsArea_LightShape.RECT: {
91-
const width = light.width !== undefined ? light.width : 1.0;
92-
const height = light.height !== undefined ? light.height : 1.0;
90+
switch (light.type) {
91+
case EXTLightsArea_LightType.RECT: {
92+
const width = light.rect?.aspect !== undefined ? light.rect.aspect * size : size;
93+
const height = size;
9394
const babylonRectAreaLight = new RectAreaLight(name, Vector3.Zero(), width, height, this._loader.babylonScene);
9495
babylonLight = babylonRectAreaLight;
9596
break;
9697
}
97-
case EXTLightsArea_LightShape.DISK: {
98-
// For disk lights, we'll use RectAreaLight with equal width and height to approximate a square area
98+
case EXTLightsArea_LightType.DISK: {
99+
// For disk lights, we'll use a rectangle light with the same area to approximate the disk light
99100
// In the future, this could be extended to support actual disk area lights
100-
const radius = light.radius !== undefined ? light.radius : 0.5;
101-
const size = radius * 2; // Convert radius to square size
102-
const babylonRectAreaLight = new RectAreaLight(name, Vector3.Zero(), size, size, this._loader.babylonScene);
101+
const newSize = Math.sqrt(size * size * 0.25 * Math.PI); // Area of the disk
102+
const babylonRectAreaLight = new RectAreaLight(name, Vector3.Zero(), newSize, newSize, this._loader.babylonScene);
103103
babylonLight = babylonRectAreaLight;
104104
break;
105105
}
106106
default: {
107107
this._loader.babylonScene._blockEntityCollection = false;
108-
throw new Error(`${extensionContext}: Invalid area light shape (${light.shape})`);
108+
throw new Error(`${extensionContext}: Invalid area light type (${light.type})`);
109109
}
110110
}
111111

packages/dev/loaders/src/glTF/2.0/Extensions/KHR_animation_pointer.data.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,11 @@ SetInterpolationForKey("/extensions/KHR_lights_punctual/lights/{}/spot/outerCone
333333
new LightAnimationPropertyInfo(Animation.ANIMATIONTYPE_FLOAT, "angle", getFloatBy2, () => 1),
334334
]);
335335

336+
SetInterpolationForKey("/extensions/EXT_lights_area/lights/{}/color", [new LightAnimationPropertyInfo(Animation.ANIMATIONTYPE_COLOR3, "diffuse", getColor3, () => 3)]);
337+
SetInterpolationForKey("/extensions/EXT_lights_area/lights/{}/intensity", [new LightAnimationPropertyInfo(Animation.ANIMATIONTYPE_FLOAT, "intensity", getFloat, () => 1)]);
338+
SetInterpolationForKey("/extensions/EXT_lights_area/lights/{}/size", [new LightAnimationPropertyInfo(Animation.ANIMATIONTYPE_FLOAT, "radius", getFloat, () => 1)]);
339+
SetInterpolationForKey("/extensions/EXT_lights_area/lights/{}/rect/aspect", [new LightAnimationPropertyInfo(Animation.ANIMATIONTYPE_FLOAT, "radius", getFloat, () => 1)]);
340+
336341
SetInterpolationForKey("/nodes/{}/extensions/EXT_lights_ies/color", [new LightAnimationPropertyInfo(Animation.ANIMATIONTYPE_COLOR3, "diffuse", getColor3, () => 3)]);
337342
SetInterpolationForKey("/nodes/{}/extensions/EXT_lights_ies/multiplier", [new LightAnimationPropertyInfo(Animation.ANIMATIONTYPE_FLOAT, "intensity", getFloat, () => 1)]);
338343

packages/dev/loaders/src/glTF/2.0/Extensions/dynamic.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export function registerBuiltInGLTFExtensions() {
5656
return new KHR_lights(loader);
5757
});
5858

59+
registerGLTFExtension("EXT_lights_area", true, async (loader) => {
60+
const { EXT_lights_area } = await import("./EXT_lights_area");
61+
return new EXT_lights_area(loader);
62+
});
63+
5964
registerGLTFExtension("EXT_lights_ies", true, async (loader) => {
6065
const { EXT_lights_ies } = await import("./EXT_lights_ies");
6166
return new EXT_lights_ies(loader);

packages/dev/loaders/src/glTF/2.0/Extensions/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-restricted-imports */
22
export * from "./objectModelMapping";
3-
export * from "./EXT_lights_area";
43
export * from "./EXT_lights_image_based";
54
export * from "./EXT_mesh_gpu_instancing";
65
export * from "./EXT_meshopt_compression";
@@ -9,6 +8,7 @@ export * from "./EXT_texture_avif";
98
export * from "./EXT_lights_ies";
109
export * from "./KHR_draco_mesh_compression";
1110
export * from "./KHR_lights_punctual";
11+
export * from "./EXT_lights_area";
1212
export * from "./KHR_materials_pbrSpecularGlossiness";
1313
export * from "./KHR_materials_unlit";
1414
export * from "./KHR_materials_clearcoat";

packages/dev/loaders/src/glTF/2.0/Extensions/objectModelMapping.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable @typescript-eslint/naming-convention */
22

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

2122
export interface IGLTFObjectModelTree {
2223
cameras: IGLTFObjectModelTreeCamerasObject;
@@ -255,6 +256,20 @@ export interface IGLTFObjectModelTreeExtensionsObject {
255256
};
256257
};
257258
};
259+
EXT_lights_area: {
260+
lights: {
261+
length: IObjectAccessor<IEXTLightsArea_Light[], Light[], number>;
262+
__array__: {
263+
__target__: boolean;
264+
color: IObjectAccessor<IEXTLightsArea_Light, Light, Color3>;
265+
intensity: IObjectAccessor<IEXTLightsArea_Light, Light, number>;
266+
size: IObjectAccessor<IEXTLightsArea_Light, Light, number>;
267+
rect: {
268+
aspect: IObjectAccessor<IEXTLightsArea_Light, Light, number>;
269+
};
270+
};
271+
};
272+
};
258273
EXT_lights_ies: {
259274
lights: {
260275
length: IObjectAccessor<IKHRLightsPunctual_Light[], Light[], number>;
@@ -920,6 +935,50 @@ const extensionsTree: IGLTFObjectModelTreeExtensionsObject = {
920935
},
921936
},
922937
},
938+
EXT_lights_area: {
939+
lights: {
940+
length: {
941+
type: "number",
942+
get: (lights: IEXTLightsArea_Light[]) => lights.length,
943+
getTarget: (lights: IEXTLightsArea_Light[]) => lights.map((light) => light._babylonLight!),
944+
getPropertyName: [(_lights: IEXTLightsArea_Light[]) => "length"],
945+
},
946+
__array__: {
947+
__target__: true,
948+
color: {
949+
type: "Color3",
950+
get: (light: IEXTLightsArea_Light) => light._babylonLight?.diffuse,
951+
set: (value: Color3, light: IEXTLightsArea_Light) => light._babylonLight?.diffuse.copyFrom(value),
952+
getTarget: (light: IEXTLightsArea_Light) => light._babylonLight,
953+
getPropertyName: [(_light: IEXTLightsArea_Light) => "diffuse"],
954+
},
955+
intensity: {
956+
type: "number",
957+
get: (light: IEXTLightsArea_Light) => light._babylonLight?.intensity,
958+
set: (value: number, light: IEXTLightsArea_Light) => (light._babylonLight ? (light._babylonLight.intensity = value) : undefined),
959+
getTarget: (light: IEXTLightsArea_Light) => light._babylonLight,
960+
getPropertyName: [(_light: IEXTLightsArea_Light) => "intensity"],
961+
},
962+
size: {
963+
type: "number",
964+
get: (light: IEXTLightsArea_Light) => (light._babylonLight as RectAreaLight)?.height,
965+
set: (value: number, light: IEXTLightsArea_Light) => (light._babylonLight ? ((light._babylonLight as RectAreaLight).height = value) : undefined),
966+
getTarget: (light: IEXTLightsArea_Light) => light._babylonLight,
967+
getPropertyName: [(_light: IEXTLightsArea_Light) => "size"],
968+
},
969+
rect: {
970+
aspect: {
971+
type: "number",
972+
get: (light: IEXTLightsArea_Light) => (light._babylonLight as RectAreaLight)?.width / (light._babylonLight as RectAreaLight)?.height,
973+
set: (value: number, light: IEXTLightsArea_Light) =>
974+
light._babylonLight ? ((light._babylonLight as RectAreaLight).width = value * (light._babylonLight as RectAreaLight).height) : undefined,
975+
getTarget: (light: IEXTLightsArea_Light) => light._babylonLight,
976+
getPropertyName: [(_light: IEXTLightsArea_Light) => "aspect"],
977+
},
978+
},
979+
},
980+
},
981+
},
923982
EXT_lights_ies: {
924983
lights: {
925984
length: {
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import type { Nullable } from "core/types";
2+
import { Vector3, Quaternion, TmpVectors } from "core/Maths/math.vector";
3+
import { Light } from "core/Lights/light";
4+
import type { Node } from "core/node";
5+
import type { INode, IEXTLightsArea_LightReference, IEXTLightsArea_Light, IEXTLightsArea } from "babylonjs-gltf2interface";
6+
import { EXTLightsArea_LightType } from "babylonjs-gltf2interface";
7+
import type { IGLTFExporterExtensionV2 } from "../glTFExporterExtension";
8+
import { GLTFExporter } from "../glTFExporter";
9+
import { Logger } from "core/Misc/logger";
10+
import { ConvertToRightHandedPosition, OmitDefaultValues, CollapseChildIntoParent, IsChildCollapsible } from "../glTFUtilities";
11+
import type { RectAreaLight } from "core/Lights/rectAreaLight";
12+
13+
const NAME = "EXT_lights_area";
14+
const DEFAULTS: Omit<IEXTLightsArea_Light, "type"> = {
15+
name: "",
16+
color: [1, 1, 1],
17+
intensity: 1,
18+
size: 1,
19+
};
20+
const RECTDEFAULTS: NonNullable<IEXTLightsArea_Light["rect"]> = {
21+
aspect: 1,
22+
};
23+
const LIGHTDIRECTION = Vector3.Backward();
24+
25+
/**
26+
* [Specification](https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/EXT_lights_area/README.md)
27+
*/
28+
// eslint-disable-next-line @typescript-eslint/naming-convention
29+
export class EXT_lights_area implements IGLTFExporterExtensionV2 {
30+
/** The name of this extension. */
31+
public readonly name = NAME;
32+
33+
/** Defines whether this extension is enabled. */
34+
public enabled = true;
35+
36+
/** Defines whether this extension is required */
37+
public required = false;
38+
39+
/** Reference to the glTF exporter */
40+
private _exporter: GLTFExporter;
41+
42+
private _lights: IEXTLightsArea;
43+
44+
/**
45+
* @internal
46+
*/
47+
constructor(exporter: GLTFExporter) {
48+
this._exporter = exporter;
49+
}
50+
51+
/** @internal */
52+
public dispose() {
53+
(this._lights as any) = null;
54+
}
55+
56+
/** @internal */
57+
public get wasUsed() {
58+
return !!this._lights;
59+
}
60+
61+
/** @internal */
62+
public onExporting(): void {
63+
this._exporter._glTF.extensions![NAME] = this._lights;
64+
}
65+
/**
66+
* Define this method to modify the default behavior when exporting a node
67+
* @param context The context when exporting the node
68+
* @param node glTF node
69+
* @param babylonNode BabylonJS node
70+
* @param nodeMap Node mapping of babylon node to glTF node index
71+
* @param convertToRightHanded Flag to convert the values to right-handed
72+
* @returns nullable INode promise
73+
*/
74+
public async postExportNodeAsync(context: string, node: INode, babylonNode: Node, nodeMap: Map<Node, number>, convertToRightHanded: boolean): Promise<Nullable<INode>> {
75+
return await new Promise((resolve) => {
76+
if (!(babylonNode instanceof Light)) {
77+
resolve(node);
78+
return;
79+
}
80+
81+
const lightType = babylonNode.getTypeID() == Light.LIGHTTYPEID_RECT_AREALIGHT ? EXTLightsArea_LightType.RECT : null;
82+
if (!lightType) {
83+
Logger.Warn(`${context}: Light ${babylonNode.name} is not supported in ${NAME}`);
84+
resolve(node);
85+
return;
86+
}
87+
88+
const areaLight = babylonNode as RectAreaLight;
89+
90+
if (areaLight.falloffType !== Light.FALLOFF_GLTF) {
91+
Logger.Warn(`${context}: Light falloff for ${babylonNode.name} does not match the ${NAME} specification!`);
92+
}
93+
94+
// Set the node's translation and rotation here, since lights are not handled in exportNodeAsync
95+
if (!areaLight.position.equalsToFloats(0, 0, 0)) {
96+
const translation = TmpVectors.Vector3[0].copyFrom(areaLight.position);
97+
if (convertToRightHanded) {
98+
ConvertToRightHandedPosition(translation);
99+
}
100+
node.translation = translation.asArray();
101+
}
102+
103+
// Represent the Babylon light's direction as a quaternion
104+
// relative to glTF lights' forward direction, (0, 0, -1).
105+
const direction = Vector3.Forward();
106+
if (convertToRightHanded) {
107+
ConvertToRightHandedPosition(direction);
108+
}
109+
110+
const lightRotationQuaternion = Quaternion.FromUnitVectorsToRef(LIGHTDIRECTION, direction, TmpVectors.Quaternion[0]);
111+
if (!Quaternion.IsIdentity(lightRotationQuaternion)) {
112+
node.rotation = lightRotationQuaternion.asArray();
113+
}
114+
115+
const light: IEXTLightsArea_Light = {
116+
type: lightType,
117+
name: areaLight.name,
118+
color: areaLight.diffuse.asArray(),
119+
intensity: areaLight.intensity,
120+
size: areaLight.height,
121+
rect: {
122+
aspect: areaLight.width / areaLight.height,
123+
},
124+
};
125+
OmitDefaultValues(light, DEFAULTS);
126+
127+
if (light.rect) {
128+
OmitDefaultValues(light.rect, RECTDEFAULTS);
129+
}
130+
131+
this._lights ||= {
132+
lights: [],
133+
};
134+
this._lights.lights.push(light);
135+
136+
const lightReference: IEXTLightsArea_LightReference = {
137+
light: this._lights.lights.length - 1,
138+
};
139+
140+
// Assign the light to its parent node, if possible, to condense the glTF
141+
// Why and when: the glTF loader generates a new parent TransformNode for each light node, which we should undo on export
142+
const parentBabylonNode = babylonNode.parent;
143+
144+
if (parentBabylonNode && IsChildCollapsible(areaLight, parentBabylonNode)) {
145+
const parentNodeIndex = nodeMap.get(parentBabylonNode);
146+
if (parentNodeIndex) {
147+
// Combine the light's transformation with the parent's
148+
const parentNode = this._exporter._nodes[parentNodeIndex];
149+
CollapseChildIntoParent(node, parentNode);
150+
parentNode.extensions ||= {};
151+
parentNode.extensions[NAME] = lightReference;
152+
153+
// Do not export the original node
154+
resolve(null);
155+
return;
156+
}
157+
}
158+
159+
node.extensions ||= {};
160+
node.extensions[NAME] = lightReference;
161+
resolve(node);
162+
});
163+
}
164+
}
165+
166+
GLTFExporter.RegisterExtension(NAME, (exporter) => new EXT_lights_area(exporter));

packages/dev/serializers/src/glTF/2.0/Extensions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from "./EXT_mesh_gpu_instancing";
22
export * from "./KHR_draco_mesh_compression";
33
export * from "./KHR_lights_punctual";
4+
export * from "./EXT_lights_area";
45
export * from "./KHR_materials_anisotropy";
56
export * from "./KHR_materials_clearcoat";
67
export * from "./KHR_materials_clearcoat_darkening";

packages/dev/serializers/src/glTF/2.0/glTFUtilities.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { TargetCamera } from "core/Cameras/targetCamera";
1515
import type { ShadowLight } from "core/Lights/shadowLight";
1616
import { Epsilon } from "core/Maths/math.constants";
1717
import { ConvertHandednessMatrix } from "../../exportUtils";
18+
import type { AreaLight } from "core/Lights/areaLight";
1819

1920
// Default values for comparison.
2021
export const DefaultTranslation = Vector3.ZeroReadOnly;
@@ -301,7 +302,7 @@ export function CollapseChildIntoParent(node: INode, parentNode: INode): void {
301302
* @param parentBabylonNode Target Babylon parent node.
302303
* @returns True if the two nodes can be merged, false otherwise.
303304
*/
304-
export function IsChildCollapsible(babylonNode: ShadowLight | TargetCamera, parentBabylonNode: Node): boolean {
305+
export function IsChildCollapsible(babylonNode: ShadowLight | TargetCamera | AreaLight, parentBabylonNode: Node): boolean {
305306
if (!(parentBabylonNode instanceof TransformNode)) {
306307
return false;
307308
}

0 commit comments

Comments
 (0)