|
| 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)); |
0 commit comments