Skip to content

Commit

Permalink
Initial implementation for Area Lights (#16078)
Browse files Browse the repository at this point in the history
Added initial implementation of Area Lights. Initial light type supported is RectAreaLight and support is only available in Standard and PBR shaders.  

---------

Co-authored-by: Gary Hsu <[email protected]>
Co-authored-by: Popov72 <[email protected]>
  • Loading branch information
3 people authored Jan 23, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent f2a7eab commit 5484832
Showing 36 changed files with 1,034 additions and 88 deletions.
45 changes: 45 additions & 0 deletions packages/dev/core/src/Lights/LTC/ltcTextureTool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { BaseTexture } from "core/Materials/Textures/baseTexture";
import { Tools } from "core/Misc/tools";
import type { Tuple } from "core/types";

/**
* Linearly transformed cosine textures that are used in the Area Lights shaders.
*/
export type ILTCTextures = {
/**
* Linearly transformed cosine texture BRDF Approximation.
*/
LTC1: BaseTexture;

/**
* Linearly transformed cosine texture Fresnel Approximation.
*/
LTC2: BaseTexture;
};

/**
* Loads LTC texture data from Babylon.js CDN.
* @returns Promise with data for LTC1 and LTC2 textures for area lights.
*/
export async function DecodeLTCTextureDataAsync(): Promise<Tuple<Uint16Array, 2>> {
const ltc1 = new Uint16Array(64 * 64 * 4);
const ltc2 = new Uint16Array(64 * 64 * 4);
const file = await Tools.LoadFileAsync(Tools.GetAssetUrl("https://assets.babylonjs.com/core/areaLights/areaLightsLTC.bin"));
const ltcEncoded = new Uint16Array(file);

const pixelCount = ltcEncoded.length / 8;

for (let pixelIndex = 0; pixelIndex < pixelCount; pixelIndex++) {
ltc1[pixelIndex * 4] = ltcEncoded[pixelIndex * 8];
ltc1[pixelIndex * 4 + 1] = ltcEncoded[pixelIndex * 8 + 1];
ltc1[pixelIndex * 4 + 2] = ltcEncoded[pixelIndex * 8 + 2];
ltc1[pixelIndex * 4 + 3] = ltcEncoded[pixelIndex * 8 + 3];

ltc2[pixelIndex * 4] = ltcEncoded[pixelIndex * 8 + 4];
ltc2[pixelIndex * 4 + 1] = ltcEncoded[pixelIndex * 8 + 5];
ltc2[pixelIndex * 4 + 2] = ltcEncoded[pixelIndex * 8 + 6];
ltc2[pixelIndex * 4 + 3] = ltcEncoded[pixelIndex * 8 + 7];
}

return [ltc1, ltc2];
}
114 changes: 114 additions & 0 deletions packages/dev/core/src/Lights/areaLight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { Vector3 } from "core/Maths/math.vector";
import { RawTexture } from "core/Materials/Textures/rawTexture";
import { Texture } from "core/Materials/Textures/texture";
import { Constants } from "core/Engines/constants";
import { Light } from "core/Lights/light";
import type { Effect } from "core/Materials/effect";
import type { ILTCTextures } from "core/Lights/LTC/ltcTextureTool";
import { DecodeLTCTextureDataAsync } from "core/Lights/LTC/ltcTextureTool";
import type { Scene } from "core/scene";
import { Logger } from "core/Misc/logger";

declare module "../scene" {
export interface Scene {
/**
* @internal
*/
_ltcTextures?: ILTCTextures;
}
}

function CreateSceneLTCTextures(scene: Scene): void {
const useDelayedTextureLoading = scene.useDelayedTextureLoading;
scene.useDelayedTextureLoading = false;

const previousState = scene._blockEntityCollection;
scene._blockEntityCollection = false;

scene._ltcTextures = {
LTC1: RawTexture.CreateRGBATexture(null, 64, 64, scene.getEngine(), false, false, Constants.TEXTURE_LINEAR_LINEAR, Constants.TEXTURETYPE_HALF_FLOAT, 0, false, true),
LTC2: RawTexture.CreateRGBATexture(null, 64, 64, scene.getEngine(), false, false, Constants.TEXTURE_LINEAR_LINEAR, Constants.TEXTURETYPE_HALF_FLOAT, 0, false, true),
};

scene._blockEntityCollection = previousState;

scene._ltcTextures.LTC1.wrapU = Texture.CLAMP_ADDRESSMODE;
scene._ltcTextures.LTC1.wrapV = Texture.CLAMP_ADDRESSMODE;

scene._ltcTextures.LTC2.wrapU = Texture.CLAMP_ADDRESSMODE;
scene._ltcTextures.LTC2.wrapV = Texture.CLAMP_ADDRESSMODE;

scene.useDelayedTextureLoading = useDelayedTextureLoading;

DecodeLTCTextureDataAsync()
.then((textureData) => {
if (scene._ltcTextures) {
const ltc1 = scene._ltcTextures?.LTC1 as RawTexture;
ltc1.update(textureData[0]);

const ltc2 = scene._ltcTextures?.LTC2 as RawTexture;
ltc2.update(textureData[1]);

scene.onDisposeObservable.addOnce(() => {
scene._ltcTextures?.LTC1.dispose();
scene._ltcTextures?.LTC2.dispose();
});
}
})
.catch((error) => {
Logger.Error(`Area Light fail to get LTC textures data. Error: ${error}`);
});
}

/**
* Abstract Area Light class that servers as parent for all Area Lights implementations.
* The light is emitted from the area in the -Z direction.
*/
export abstract class AreaLight extends Light {
/**
* Area Light position.
*/
public position: Vector3;

/**
* Creates a area light object.
* Documentation : https://doc.babylonjs.com/features/featuresDeepDive/lights/lights_introduction
* @param name The friendly name of the light
* @param position The position of the area light.
* @param scene The scene the light belongs to
*/
constructor(name: string, position: Vector3, scene?: Scene) {
super(name, scene);
this.position = position;

if (!this._scene._ltcTextures) {
CreateSceneLTCTextures(this._scene);
}
}

public override transferTexturesToEffect(effect: Effect): Light {
if (this._scene._ltcTextures) {
effect.setTexture("areaLightsLTC1Sampler", this._scene._ltcTextures.LTC1);
effect.setTexture("areaLightsLTC2Sampler", this._scene._ltcTextures.LTC2);
}
return this;
}

/**
* Prepares the list of defines specific to the light type.
* @param defines the list of defines
* @param lightIndex defines the index of the light for the effect
*/
public prepareLightSpecificDefines(defines: any, lightIndex: number): void {
defines["AREALIGHT" + lightIndex] = true;
defines["AREALIGHTUSED"] = true;
}

public override _isReady(): boolean {
if (this._scene._ltcTextures) {
return this._scene._ltcTextures.LTC1.isReady() && this._scene._ltcTextures.LTC2.isReady();
}

return false;
}
}
2 changes: 2 additions & 0 deletions packages/dev/core/src/Lights/index.ts
Original file line number Diff line number Diff line change
@@ -6,4 +6,6 @@ export * from "./directionalLight";
export * from "./hemisphericLight";
export * from "./pointLight";
export * from "./spotLight";
export * from "./areaLight";
export * from "./rectAreaLight";
export * from "./IES/iesLoader";
12 changes: 12 additions & 0 deletions packages/dev/core/src/Lights/light.ts
Original file line number Diff line number Diff line change
@@ -106,6 +106,11 @@ export abstract class Light extends Node implements ISortableLight {
*/
public static readonly LIGHTTYPEID_HEMISPHERICLIGHT = LightConstants.LIGHTTYPEID_HEMISPHERICLIGHT;

/**
* Light type const id of the area light.
*/
public static readonly LIGHTTYPEID_RECT_AREALIGHT = LightConstants.LIGHTTYPEID_RECT_AREALIGHT;

/**
* Diffuse gives the basic color to an object.
*/
@@ -923,4 +928,11 @@ export abstract class Light extends Node implements ISortableLight {
* @param lightIndex defines the index of the light for the effect
*/
public abstract prepareLightSpecificDefines(defines: any, lightIndex: number): void;

/**
* @internal
*/
public _isReady() {
return true;
}
}
5 changes: 5 additions & 0 deletions packages/dev/core/src/Lights/lightConstants.ts
Original file line number Diff line number Diff line change
@@ -86,6 +86,11 @@ export class LightConstants {
*/
public static readonly LIGHTTYPEID_HEMISPHERICLIGHT = 3;

/**
* Light type const id of the area light.
*/
public static readonly LIGHTTYPEID_RECT_AREALIGHT = 4;

/**
* Sort function to order lights for rendering.
* @param a First Light object to compare to second.
142 changes: 142 additions & 0 deletions packages/dev/core/src/Lights/rectAreaLight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { Vector3 } from "../Maths/math.vector";
import { Node } from "../node";
import { Light } from "./light";
import type { Effect } from "core/Materials/effect";
import { RegisterClass } from "core/Misc/typeStore";
import { serialize } from "../Misc/decorators";
import type { Scene } from "core/scene";
import { AreaLight } from "./areaLight";

Node.AddNodeConstructor("Light_Type_4", (name, scene) => {
return () => new RectAreaLight(name, Vector3.Zero(), 1, 1, scene);
});

/**
* A rectangular area light defined by an unique point in world space, a width and a height.
* The light is emitted from the rectangular area in the -Z direction.
*/
export class RectAreaLight extends AreaLight {
private readonly _width: Vector3;
private readonly _height: Vector3;
protected readonly _pointTransformedPosition: Vector3;
protected readonly _pointTransformedWidth: Vector3;
protected readonly _pointTransformedHeight: Vector3;

/**
* Rect Area Light width.
*/
@serialize()
public get width(): number {
return this._width.x;
}
/**
* Rect Area Light width.
*/
public set width(value: number) {
this._width.x = value;
}

/**
* Rect Area Light height.
*/
@serialize()
public get height(): number {
return this._height.y;
}
/**
* Rect Area Light height.
*/
public set height(value: number) {
this._height.y = value;
}

/**
* Creates a rectangular area light object.
* Documentation : https://doc.babylonjs.com/features/featuresDeepDive/lights/lights_introduction
* @param name The friendly name of the light
* @param position The position of the area light.
* @param width The width of the area light.
* @param height The height of the area light.
* @param scene The scene the light belongs to
*/
constructor(name: string, position: Vector3, width: number, height: number, scene?: Scene) {
super(name, position, scene);
this._width = new Vector3(width, 0, 0);
this._height = new Vector3(0, height, 0);
this._pointTransformedPosition = Vector3.Zero();
this._pointTransformedWidth = Vector3.Zero();
this._pointTransformedHeight = Vector3.Zero();
}

/**
* Returns the string "RectAreaLight"
* @returns the class name
*/
public override getClassName(): string {
return "RectAreaLight";
}

/**
* Returns the integer 4.
* @returns The light Type id as a constant defines in Light.LIGHTTYPEID_x
*/
public override getTypeID(): number {
return Light.LIGHTTYPEID_RECT_AREALIGHT;
}

protected _buildUniformLayout(): void {
this._uniformBuffer.addUniform("vLightData", 4);
this._uniformBuffer.addUniform("vLightDiffuse", 4);
this._uniformBuffer.addUniform("vLightSpecular", 4);
this._uniformBuffer.addUniform("vLightWidth", 4);
this._uniformBuffer.addUniform("vLightHeight", 4);
this._uniformBuffer.addUniform("shadowsInfo", 3);
this._uniformBuffer.addUniform("depthValues", 2);
this._uniformBuffer.create();
}

protected _computeTransformedInformation(): boolean {
if (this.parent && this.parent.getWorldMatrix) {
Vector3.TransformCoordinatesToRef(this.position, this.parent.getWorldMatrix(), this._pointTransformedPosition);
Vector3.TransformNormalToRef(this._width, this.parent.getWorldMatrix(), this._pointTransformedWidth);
Vector3.TransformNormalToRef(this._height, this.parent.getWorldMatrix(), this._pointTransformedHeight);
return true;
}

return false;
}

/**
* Sets the passed Effect "effect" with the PointLight transformed position (or position, if none) and passed name (string).
* @param effect The effect to update
* @param lightIndex The index of the light in the effect to update
* @returns The point light
*/
public transferToEffect(effect: Effect, lightIndex: string): RectAreaLight {
if (this._computeTransformedInformation()) {
this._uniformBuffer.updateFloat4("vLightData", this._pointTransformedPosition.x, this._pointTransformedPosition.y, this._pointTransformedPosition.z, 0, lightIndex);
this._uniformBuffer.updateFloat4("vLightWidth", this._pointTransformedWidth.x / 2, this._pointTransformedWidth.y / 2, this._pointTransformedWidth.z / 2, 0, lightIndex);
this._uniformBuffer.updateFloat4(
"vLightHeight",
this._pointTransformedHeight.x / 2,
this._pointTransformedHeight.y / 2,
this._pointTransformedHeight.z / 2,
0,
lightIndex
);
} else {
this._uniformBuffer.updateFloat4("vLightData", this.position.x, this.position.y, this.position.z, 0.0, lightIndex);
this._uniformBuffer.updateFloat4("vLightWidth", this._width.x / 2, this._width.y / 2, this._width.z / 2, 0.0, lightIndex);
this._uniformBuffer.updateFloat4("vLightHeight", this._height.x / 2, this._height.y / 2, this._height.z / 2, 0.0, lightIndex);
}
return this;
}

public transferToNodeMaterialEffect(effect: Effect, lightDataUniformName: string) {
// TO DO: Implement this to add support for NME.
return this;
}
}

// Register Class Name
RegisterClass("BABYLON.RectAreaLight", RectAreaLight);
12 changes: 12 additions & 0 deletions packages/dev/core/src/Materials/PBR/pbrBaseMaterial.ts
Original file line number Diff line number Diff line change
@@ -281,6 +281,7 @@ export class PBRMaterialDefines extends MaterialDefines implements IImageProcess
public LOGARITHMICDEPTH = false;
public CAMERA_ORTHOGRAPHIC = false;
public CAMERA_PERSPECTIVE = false;
public AREALIGHTSUPPORTED = true;

public FORCENORMALFORWARD = false;

@@ -1213,6 +1214,15 @@ export abstract class PBRBaseMaterial extends PushMaterial {
}
}

// Check if Area Lights have LTC texture.
if (defines["AREALIGHTUSED"]) {
for (let index = 0; index < mesh.lightSources.length; index++) {
if (!mesh.lightSources[index]._isReady()) {
return false;
}
}
}

if (!engine.getCaps().standardDerivatives && !mesh.isVerticesDataPresent(VertexBuffer.NormalKind)) {
mesh.createNormals(true);
Logger.Warn("PBRMaterial: Normals have been created for the mesh: " + mesh.name);
@@ -1497,6 +1507,8 @@ export abstract class PBRBaseMaterial extends PushMaterial {
"oitDepthSampler",
"oitFrontColorSampler",
"icdfSampler",
"areaLightsLTC1Sampler",
"areaLightsLTC2Sampler",
];

const uniformBuffers = ["Material", "Scene", "Mesh"];
Loading

0 comments on commit 5484832

Please sign in to comment.