playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
1,246 lines (1,245 loc) • 34.7 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { math } from "../../../core/math/math.js";
import { Vec4 } from "../../../core/math/vec4.js";
import { LAYERID_WORLD, MASK_AFFECT_LIGHTMAPPED, MASK_AFFECT_DYNAMIC, MASK_BAKE } from "../../../scene/constants.js";
import { Light, lightTypes } from "../../../scene/light.js";
import { Asset } from "../../asset/asset.js";
import { Component } from "../component.js";
const _properties = [
"type",
"color",
"intensity",
"luminance",
"shape",
"affectSpecularity",
"castShadows",
"shadowDistance",
"shadowIntensity",
"shadowResolution",
"shadowBias",
"numCascades",
"cascadeBlend",
"bakeNumSamples",
"bakeArea",
"cascadeDistribution",
"normalOffsetBias",
"range",
"innerConeAngle",
"outerConeAngle",
"falloffMode",
"shadowType",
"vsmBlurSize",
"vsmBlurMode",
"vsmBias",
"cookieAsset",
"cookie",
"cookieIntensity",
"cookieFalloff",
"cookieChannel",
"cookieAngle",
"cookieScale",
"cookieOffset",
"shadowUpdateMode",
"mask",
"affectDynamic",
"affectLightmapped",
"bake",
"bakeDir",
"isStatic",
"layers",
"penumbraSize",
"penumbraFalloff",
"shadowSamples",
"shadowBlockerSamples"
];
class LightComponent extends Component {
/**
* Create a new LightComponent instance.
*
* @param {LightComponentSystem} system - The ComponentSystem that created this Component.
* @param {Entity} entity - The Entity that this Component is attached to.
*/
constructor(system, entity) {
super(system, entity);
/**
* @type {Light}
* @private
*/
__publicField(this, "_light");
/**
* @type {EventHandle|null}
* @private
*/
__publicField(this, "_evtLayersChanged", null);
/**
* @type {EventHandle|null}
* @private
*/
__publicField(this, "_evtLayerAdded", null);
/**
* @type {EventHandle|null}
* @private
*/
__publicField(this, "_evtLayerRemoved", null);
/**
* @type {Asset|null}
* @private
*/
__publicField(this, "_cookieAsset", null);
/**
* @type {number|null}
* @private
*/
__publicField(this, "_cookieAssetId", null);
/** @private */
__publicField(this, "_cookieAssetAdd", false);
/**
* @type {Vec4|null}
* @private
*/
__publicField(this, "_cookieMatrix", null);
/**
* @type {number}
* @private
*/
__publicField(this, "_shadowBias", 0.05);
/**
* @type {number}
* @private
*/
__publicField(this, "_cookieAngle", 0);
/**
* @type {Vec2|null}
* @private
*/
__publicField(this, "_cookieScale", null);
/**
* @type {boolean}
* @private
*/
__publicField(this, "_castShadows", false);
/**
* Mirrors the user-supplied value. {@link Light#affectSpecularity} silently ignores writes for
* non-directional lights, so storing it on the component is required to round-trip the user's
* intent and re-apply it when the type later becomes directional (via {@link refreshProperties}).
*
* @type {boolean}
* @private
*/
__publicField(this, "_affectSpecularity", true);
/**
* @type {boolean}
* @private
*/
__publicField(this, "_affectDynamic", true);
/**
* @type {boolean}
* @private
*/
__publicField(this, "_affectLightmapped", false);
/**
* @type {boolean}
* @private
*/
__publicField(this, "_bake", false);
/**
* @type {number[]}
* @private
*/
__publicField(this, "_layers", [LAYERID_WORLD]);
/**
* Preserves the user-facing type string. Required because `'point'` and `'omni'` both map to
* the same underlying int on the {@link Light}, so reverse-mapping would normalise the user's
* input.
*
* @type {string}
* @private
*/
__publicField(this, "_type", "directional");
this._light = new Light(system.app.graphicsDevice, system.app.scene.clusteredLightingEnabled);
this._light._node = entity;
this._light.setColor(1, 1, 1);
}
/**
* Gets the light component's underlying Light instance.
*
* @type {Light}
* @ignore
*/
get light() {
return this._light;
}
/**
* Sets the type of the light. Can be:
*
* - `"directional"`: A global light that emits light in the direction of the negative y-axis
* of the owner entity.
* - `"omni"`: A local light that emits light in all directions from the owner entity's
* position.
* - `"spot"`: A local light that emits light similarly to an omni light but is bounded by a
* cone centered on the owner entity's negative y-axis.
*
* Defaults to `"directional"`.
*
* @type {string}
*/
set type(value) {
if (this._type === value) return;
this.removeLightFromLayers();
this._type = value;
this._light.type = lightTypes[value];
this.refreshProperties();
}
/**
* Gets the type of the light.
*
* @type {string}
*/
get type() {
return this._type;
}
/**
* Sets the color of the light in sRGB space. The alpha component of the color is ignored.
* Defaults to white (`[1, 1, 1]`).
*
* @type {Color}
*/
set color(value) {
this._light.setColor(value);
}
/**
* Gets the color of the light.
*
* @type {Color}
*/
get color() {
return this._light.getColor();
}
/**
* Sets the brightness of the light. Defaults to 1.
*
* @type {number}
*/
set intensity(value) {
this._light.intensity = value;
}
/**
* Gets the brightness of the light.
*
* @type {number}
*/
get intensity() {
return this._light.intensity;
}
/**
* Sets the physically-based luminance. Only used if `scene.physicalUnits` is true. Defaults to 0.
*
* @type {number}
*/
set luminance(value) {
this._light.luminance = value;
}
/**
* Gets the physically-based luminance.
*
* @type {number}
*/
get luminance() {
return this._light.luminance;
}
/**
* Sets the light source shape. Can be:
*
* - {@link LIGHTSHAPE_PUNCTUAL}: Infinitesimally small point.
* - {@link LIGHTSHAPE_RECT}: Rectangle shape.
* - {@link LIGHTSHAPE_DISK}: Disk shape.
* - {@link LIGHTSHAPE_SPHERE}: Sphere shape.
*
* Defaults to {@link LIGHTSHAPE_PUNCTUAL}.
*
* @type {number}
*/
set shape(value) {
this._light.shape = value;
}
/**
* Gets the light source shape.
*
* @type {number}
*/
get shape() {
return this._light.shape;
}
/**
* Sets whether material specularity will be affected by this light. Only takes effect when
* {@link type} is `"directional"`; for other types the value is preserved on the component and
* applied if {@link type} later becomes `"directional"`. Defaults to true.
*
* @type {boolean}
*/
set affectSpecularity(value) {
this._affectSpecularity = value;
this._light.affectSpecularity = value;
}
/**
* Gets whether material specularity will be affected by this light.
*
* @type {boolean}
*/
get affectSpecularity() {
return this._affectSpecularity;
}
/**
* Sets whether the light will cast shadows. Defaults to false.
*
* @type {boolean}
*/
set castShadows(value) {
this._castShadows = value;
this._light.castShadows = value;
}
/**
* Gets whether the light will cast shadows.
*
* @type {boolean}
*/
get castShadows() {
return this._castShadows;
}
/**
* Sets the distance from the viewpoint beyond which shadows are no longer rendered. Affects
* directional lights only. Defaults to 40.
*
* @type {number}
*/
set shadowDistance(value) {
this._light.shadowDistance = value;
}
/**
* Gets the distance from the viewpoint beyond which shadows are no longer rendered.
*
* @type {number}
*/
get shadowDistance() {
return this._light.shadowDistance;
}
/**
* Sets the intensity of the shadow darkening. 0 having no effect and 1 meaning shadows are
* entirely black. Defaults to 1.
*
* @type {number}
*/
set shadowIntensity(value) {
this._light.shadowIntensity = value;
}
/**
* Gets the intensity of the shadow darkening.
*
* @type {number}
*/
get shadowIntensity() {
return this._light.shadowIntensity;
}
/**
* Sets the size of the texture used for the shadow map. Valid sizes are 64, 128, 256, 512,
* 1024, 2048. Defaults to 1024.
*
* @type {number}
*/
set shadowResolution(value) {
this._light.shadowResolution = value;
}
/**
* Gets the size of the texture used for the shadow map.
*
* @type {number}
*/
get shadowResolution() {
return this._light.shadowResolution;
}
/**
* Set the depth bias for tuning the appearance of the shadow mapping generated by this light. Valid
* range is 0 to 1. Defaults to 0.05.
*
* @type {number}
*/
set shadowBias(value) {
this._shadowBias = value;
this._light.shadowBias = -0.01 * math.clamp(value, 0, 1);
}
/**
* Get the depth bias for tuning the appearance of the shadow mapping generated by this light.
*
* @type {number}
*/
get shadowBias() {
return this._shadowBias;
}
/**
* Sets the number of shadow cascades. Can be 1, 2, 3 or 4. Defaults to 1, representing no
* cascades.
*
* @type {number}
*/
set numCascades(value) {
this._light.numCascades = math.clamp(Math.floor(value), 1, 4);
}
/**
* Gets the number of shadow cascades.
*
* @type {number}
*/
get numCascades() {
return this._light.numCascades;
}
/**
* Sets the blend factor for cascaded shadow maps, defining the fraction of each cascade level
* used for blending between adjacent cascades. The value should be between 0 and 1. Defaults
* to 0, which disables blending between cascades.
*
* @type {number}
*/
set cascadeBlend(value) {
this._light.cascadeBlend = math.clamp(value, 0, 1);
}
/**
* Gets the blend factor for cascaded shadow maps.
*
* @type {number}
*/
get cascadeBlend() {
return this._light.cascadeBlend;
}
/**
* Sets the number of samples used to bake this light into the lightmap. Defaults to 1. Maximum
* value is 255.
*
* @type {number}
*/
set bakeNumSamples(value) {
this._light.bakeNumSamples = math.clamp(Math.floor(value), 1, 255);
}
/**
* Gets the number of samples used to bake this light into the lightmap.
*
* @type {number}
*/
get bakeNumSamples() {
return this._light.bakeNumSamples;
}
/**
* Sets the angular size in degrees of the area used when baking soft shadow boundaries for the
* directional light into the lightmap. Range is 0 to 180. Requires {@link bake} to be set to
* true and {@link type} to be `"directional"`. Defaults to 0.
*
* @type {number}
*/
set bakeArea(value) {
this._light.bakeArea = math.clamp(value, 0, 180);
}
/**
* Gets the angular size in degrees of the area used when baking soft shadow boundaries for
* the directional light into the lightmap.
*
* @type {number}
*/
get bakeArea() {
return this._light.bakeArea;
}
/**
* Sets the distribution of subdivision of the camera frustum for individual shadow cascades.
* Only used if {@link numCascades} is larger than 1. Can be a value in range of 0 and 1. Value
* of 0 represents a linear distribution, value of 1 represents a logarithmic distribution.
* Defaults to 0.5. Larger value increases the resolution of the shadows in the near distance.
*
* @type {number}
*/
set cascadeDistribution(value) {
this._light.cascadeDistribution = math.clamp(value, 0, 1);
}
/**
* Gets the distribution of subdivision of the camera frustum for individual shadow cascades.
*
* @type {number}
*/
get cascadeDistribution() {
return this._light.cascadeDistribution;
}
/**
* Sets the normal offset depth bias. Valid range is 0 to 1. Defaults to 0.
*
* @type {number}
*/
set normalOffsetBias(value) {
this._light.normalOffsetBias = math.clamp(value, 0, 1);
}
/**
* Gets the normal offset depth bias.
*
* @type {number}
*/
get normalOffsetBias() {
return this._light.normalOffsetBias;
}
/**
* Sets the range of the light. Affects omni and spot lights only. Defaults to 10.
*
* @type {number}
*/
set range(value) {
this._light.attenuationEnd = value;
}
/**
* Gets the range of the light.
*
* @type {number}
*/
get range() {
return this._light.attenuationEnd;
}
/**
* Sets the half-angle (measured in degrees from the light's direction axis to the cone edge)
* at which the spotlight cone starts to fade off. The full inner beam angle is twice this
* value. Affects spot lights only. Defaults to 40 (i.e. an 80-degree full inner beam).
*
* @type {number}
*/
set innerConeAngle(value) {
this._light.innerConeAngle = value;
}
/**
* Gets the half-angle (measured in degrees from the light's direction axis to the cone edge)
* at which the spotlight cone starts to fade off.
*
* @type {number}
*/
get innerConeAngle() {
return this._light.innerConeAngle;
}
/**
* Sets the half-angle (measured in degrees from the light's direction axis to the cone edge)
* at which the spotlight cone has faded to nothing. The full outer beam angle is twice this
* value. Affects spot lights only. Defaults to 45 (i.e. a 90-degree full outer beam).
*
* @type {number}
*/
set outerConeAngle(value) {
this._light.outerConeAngle = value;
}
/**
* Gets the half-angle (measured in degrees from the light's direction axis to the cone edge)
* at which the spotlight cone has faded to nothing.
*
* @type {number}
*/
get outerConeAngle() {
return this._light.outerConeAngle;
}
/**
* Sets the fall off mode for the light. This controls the rate at which a light attenuates
* from its position. Can be:
*
* - {@link LIGHTFALLOFF_LINEAR}: Linear.
* - {@link LIGHTFALLOFF_INVERSESQUARED}: Inverse squared.
*
* Affects omni and spot lights only. Defaults to {@link LIGHTFALLOFF_LINEAR}.
*
* @type {number}
*/
set falloffMode(value) {
this._light.falloffMode = value;
}
/**
* Gets the fall off mode for the light.
*
* @type {number}
*/
get falloffMode() {
return this._light.falloffMode;
}
/**
* Sets the type of shadows being rendered by this light. Can be:
*
* - {@link SHADOW_PCF1_32F}
* - {@link SHADOW_PCF3_32F}
* - {@link SHADOW_PCF5_32F}
* - {@link SHADOW_PCF1_16F}
* - {@link SHADOW_PCF3_16F}
* - {@link SHADOW_PCF5_16F}
* - {@link SHADOW_VSM_16F}
* - {@link SHADOW_VSM_32F}
* - {@link SHADOW_PCSS_32F}
*
* Defaults to {@link SHADOW_PCF3_32F}.
*
* @type {number}
*/
set shadowType(value) {
this._light.shadowType = value;
}
/**
* Gets the type of shadows being rendered by this light.
*
* @type {number}
*/
get shadowType() {
return this._light.shadowType;
}
/**
* Sets the number of samples used for blurring a variance shadow map. Only odd values are
* supported; even values are rounded up to the next odd value. Values should be between 1 and
* 25. Defaults to 11.
*
* @type {number}
*/
set vsmBlurSize(value) {
this._light.vsmBlurSize = value;
}
/**
* Gets the number of samples used for blurring a variance shadow map.
*
* @type {number}
*/
get vsmBlurSize() {
return this._light.vsmBlurSize;
}
/**
* Sets the blurring mode for variance shadow maps. Can be:
*
* - {@link BLUR_BOX}: Box filter.
* - {@link BLUR_GAUSSIAN}: Gaussian filter. May look smoother than box, but requires more samples.
*
* Defaults to {@link BLUR_GAUSSIAN}.
*
* @type {number}
*/
set vsmBlurMode(value) {
this._light.vsmBlurMode = value;
}
/**
* Gets the blurring mode for variance shadow maps.
*
* @type {number}
*/
get vsmBlurMode() {
return this._light.vsmBlurMode;
}
/**
* Sets the bias used to fight shadow acne when rendering variance shadow maps. Range is 0 to
* 1. Defaults to 0.0025.
*
* @type {number}
*/
set vsmBias(value) {
this._light.vsmBias = math.clamp(value, 0, 1);
}
/**
* Gets the VSM bias value.
*
* @type {number}
*/
get vsmBias() {
return this._light.vsmBias;
}
/**
* Sets the id of the texture asset to be used as the cookie for this light. Only spot and
* omni lights can have cookies. Spot lights expect a 2D texture; omni lights expect a
* cubemap. Defaults to null.
*
* @type {number|null}
*/
set cookieAsset(value) {
if (this._cookieAssetId && (value instanceof Asset && value.id === this._cookieAssetId || value === this._cookieAssetId)) {
return;
}
this.onCookieAssetRemove();
this._cookieAssetId = null;
if (value instanceof Asset) {
this._cookieAssetId = value.id;
this.onCookieAssetAdd(value);
} else if (typeof value === "number") {
this._cookieAssetId = value;
const asset = this.system.app.assets.get(value);
if (asset) {
this.onCookieAssetAdd(asset);
} else {
this._cookieAssetAdd = true;
this.system.app.assets.on(`add:${this._cookieAssetId}`, this.onCookieAssetAdd, this);
}
}
}
/**
* Gets the id of the texture asset used as the cookie for this light, or null if none is set.
*
* @type {number|null}
*/
get cookieAsset() {
return this._cookieAssetId;
}
/**
* Sets the texture to be used as the cookie for this light. Only spot and omni lights can have
* cookies. Spot lights expect a 2D texture; omni lights expect a cubemap. Defaults to null.
*
* @type {Texture|null}
*/
set cookie(value) {
this._light.cookie = value;
}
/**
* Gets the texture to be used as the cookie for this light.
*
* @type {Texture|null}
*/
get cookie() {
return this._light.cookie;
}
/**
* Sets the cookie texture intensity. Defaults to 1.
*
* @type {number}
*/
set cookieIntensity(value) {
this._light.cookieIntensity = math.clamp(value, 0, 1);
}
/**
* Gets the cookie texture intensity.
*
* @type {number}
*/
get cookieIntensity() {
return this._light.cookieIntensity;
}
/**
* Sets whether normal spotlight falloff is active when a cookie texture is set. When set to
* false, a spotlight will work like a pure texture projector (only fading with distance).
* Defaults to true.
*
* @type {boolean}
*/
set cookieFalloff(value) {
this._light.cookieFalloff = value;
}
/**
* Gets whether normal spotlight falloff is active when a cookie texture is set.
*
* @type {boolean}
*/
get cookieFalloff() {
return this._light.cookieFalloff;
}
/**
* Sets the color channels of the cookie texture to use. Can be `"r"`, `"g"`, `"b"`, `"a"` or
* `"rgb"`. Defaults to `"rgb"`.
*
* @type {string}
*/
set cookieChannel(value) {
this._light.cookieChannel = value;
}
/**
* Gets the color channels of the cookie texture to use.
*
* @type {string}
*/
get cookieChannel() {
return this._light.cookieChannel;
}
/**
* Sets the angle for spotlight cookie rotation in degrees. Defaults to 0.
*
* @type {number}
*/
set cookieAngle(value) {
if (this._cookieAngle === value) return;
this._cookieAngle = value;
if (value !== 0 || this._cookieScale !== null) {
if (!this._cookieMatrix) this._cookieMatrix = new Vec4();
let scx = 1;
let scy = 1;
if (this._cookieScale) {
scx = this._cookieScale.x;
scy = this._cookieScale.y;
}
const c = Math.cos(value * math.DEG_TO_RAD);
const s = Math.sin(value * math.DEG_TO_RAD);
this._cookieMatrix.set(c / scx, -s / scx, s / scy, c / scy);
this._light.cookieTransform = this._cookieMatrix;
} else {
this._light.cookieTransform = null;
}
}
/**
* Gets the angle for spotlight cookie rotation (in degrees).
*
* @type {number}
*/
get cookieAngle() {
return this._cookieAngle;
}
/**
* Sets the spotlight cookie scale. Set to null to use no scaling. Defaults to null.
*
* @type {Vec2|null}
*/
set cookieScale(value) {
this._cookieScale = value;
if (value !== null || this._cookieAngle !== 0) {
if (!this._cookieMatrix) this._cookieMatrix = new Vec4();
const scx = value ? value.x : 1;
const scy = value ? value.y : 1;
const c = Math.cos(this._cookieAngle * math.DEG_TO_RAD);
const s = Math.sin(this._cookieAngle * math.DEG_TO_RAD);
this._cookieMatrix.set(c / scx, -s / scx, s / scy, c / scy);
this._light.cookieTransform = this._cookieMatrix;
} else {
this._light.cookieTransform = null;
}
}
/**
* Gets the spotlight cookie scale.
*
* @type {Vec2|null}
*/
get cookieScale() {
return this._cookieScale;
}
/**
* Sets the spotlight cookie position offset. Defaults to null.
*
* @type {Vec2|null}
*/
set cookieOffset(value) {
this._light.cookieOffset = value;
}
/**
* Gets the spotlight cookie position offset.
*
* @type {Vec2|null}
*/
get cookieOffset() {
return this._light.cookieOffset;
}
/**
* Sets the shadow update mode. This tells the renderer how often shadows must be updated for
* this light. Can be:
*
* - {@link SHADOWUPDATE_NONE}: Don't render shadows.
* - {@link SHADOWUPDATE_THISFRAME}: Render shadows only once (then automatically switches to
* {@link SHADOWUPDATE_NONE}).
* - {@link SHADOWUPDATE_REALTIME}: Render shadows every frame.
*
* Defaults to {@link SHADOWUPDATE_REALTIME}.
*
* @type {number}
*/
set shadowUpdateMode(value) {
this._light.shadowUpdateMode = value;
}
/**
* Gets the shadow update mode.
*
* @type {number}
*/
get shadowUpdateMode() {
return this._light.shadowUpdateMode;
}
/**
* Sets the bitmask that determines which {@link MeshInstance}s are lit by this light. The
* value is composed from {@link MASK_AFFECT_DYNAMIC}, {@link MASK_AFFECT_LIGHTMAPPED} and
* {@link MASK_BAKE}. The {@link affectDynamic}, {@link affectLightmapped} and {@link bake}
* helpers write to the same underlying mask but maintain their own state and are not
* recomputed from `mask`, so writing `mask` directly will not update those helpers (and a
* subsequent write to a helper may overwrite bits set via `mask`). Defaults to
* {@link MASK_AFFECT_DYNAMIC}.
*
* @type {number}
*/
set mask(value) {
this._light.mask = value;
}
/**
* Gets the mask to determine which {@link MeshInstance}s are lit by this light.
*
* @type {number}
*/
get mask() {
return this._light.mask;
}
/**
* Sets whether the light will affect non-lightmapped objects. Toggles the
* {@link MASK_AFFECT_DYNAMIC} bit on {@link mask}. Defaults to true.
*
* @type {boolean}
*/
set affectDynamic(value) {
if (this._affectDynamic === value) return;
this._affectDynamic = value;
if (value) {
this._light.mask |= MASK_AFFECT_DYNAMIC;
} else {
this._light.mask &= ~MASK_AFFECT_DYNAMIC;
}
this._light.layersDirty();
}
/**
* Gets whether the light will affect non-lightmapped objects.
*
* @type {boolean}
*/
get affectDynamic() {
return this._affectDynamic;
}
/**
* Sets whether the light will affect lightmapped objects. Toggles the
* {@link MASK_AFFECT_LIGHTMAPPED} bit on {@link mask}. Mutually exclusive with {@link bake} on
* the mask: enabling one clears the other's mask bit. Defaults to false.
*
* @type {boolean}
*/
set affectLightmapped(value) {
if (this._affectLightmapped === value) return;
this._affectLightmapped = value;
if (value) {
this._light.mask |= MASK_AFFECT_LIGHTMAPPED;
if (this._bake) this._light.mask &= ~MASK_BAKE;
} else {
this._light.mask &= ~MASK_AFFECT_LIGHTMAPPED;
if (this._bake) this._light.mask |= MASK_BAKE;
}
}
/**
* Gets whether the light will affect lightmapped objects.
*
* @type {boolean}
*/
get affectLightmapped() {
return this._affectLightmapped;
}
/**
* Sets whether the light will be rendered into lightmaps. Toggles the {@link MASK_BAKE} bit
* on {@link mask}. Mutually exclusive with {@link affectLightmapped} on the mask: enabling one
* clears the other's mask bit. Defaults to false.
*
* @type {boolean}
*/
set bake(value) {
if (this._bake === value) return;
this._bake = value;
if (value) {
this._light.mask |= MASK_BAKE;
if (this._affectLightmapped) this._light.mask &= ~MASK_AFFECT_LIGHTMAPPED;
} else {
this._light.mask &= ~MASK_BAKE;
if (this._affectLightmapped) this._light.mask |= MASK_AFFECT_LIGHTMAPPED;
}
this._light.layersDirty();
}
/**
* Gets whether the light will be rendered into lightmaps.
*
* @type {boolean}
*/
get bake() {
return this._bake;
}
/**
* Sets whether the light's direction will contribute to directional lightmaps. The light must
* be enabled and {@link bake} set to true. Be aware that the directional lightmap is an
* approximation and can only hold a single direction per pixel. Intersecting multiple lights
* with {@link bakeDir} set to true may lead to incorrect-looking specular/bump mapping in the
* area of intersection. The error is not always visible though, and is highly scene-dependent.
* Defaults to true.
*
* @type {boolean}
*/
set bakeDir(value) {
this._light.bakeDir = value;
}
/**
* Gets whether the light's direction will contribute to directional lightmaps.
*
* @type {boolean}
*/
get bakeDir() {
return this._light.bakeDir;
}
/**
* Sets whether the light ever moves. This is an optimization hint. Defaults to false.
*
* @type {boolean}
*/
set isStatic(value) {
this._light.isStatic = value;
}
/**
* Gets whether the light ever moves.
*
* @type {boolean}
*/
get isStatic() {
return this._light.isStatic;
}
/**
* Sets the array of layer IDs ({@link Layer#id}) to which this light should belong. Don't
* push/pop/splice or modify this array. If you want to change it, set a new one instead.
* Defaults to [{@link LAYERID_WORLD}].
*
* @type {number[]}
*/
set layers(value) {
const oldValue = this._layers;
for (let i = 0; i < oldValue.length; i++) {
const layer = this.system.app.scene.layers.getLayerById(oldValue[i]);
if (!layer) continue;
layer.removeLight(this);
this._light.removeLayer(layer);
}
this._layers = value;
for (let i = 0; i < value.length; i++) {
const layer = this.system.app.scene.layers.getLayerById(value[i]);
if (!layer) continue;
if (this.enabled && this.entity.enabled) {
layer.addLight(this);
this._light.addLayer(layer);
}
}
}
/**
* Gets the array of layer IDs ({@link Layer#id}) to which this light should belong.
*
* @type {number[]}
*/
get layers() {
return this._layers;
}
/**
* Sets an array of SHADOWUPDATE_ settings per shadow cascade. Set to null if not used.
* Defaults to null.
*
* @type {number[]|null}
*/
set shadowUpdateOverrides(values) {
this._light.shadowUpdateOverrides = values;
}
/**
* Gets an array of SHADOWUPDATE_ settings per shadow cascade.
*
* @type {number[]|null}
*/
get shadowUpdateOverrides() {
return this._light.shadowUpdateOverrides;
}
/**
* Sets the number of shadow samples used for soft shadows when the shadow type is
* {@link SHADOW_PCSS_32F}. This value should be a positive whole number starting at 1. Higher
* values result in smoother shadows but can significantly decrease performance. Defaults to 16.
*
* @type {number}
*/
set shadowSamples(value) {
this._light.shadowSamples = value;
}
/**
* Gets the number of shadow samples used for soft shadows.
*
* @type {number}
*/
get shadowSamples() {
return this._light.shadowSamples;
}
/**
* Sets the number of blocker samples used for soft shadows when the shadow type is
* {@link SHADOW_PCSS_32F}. These samples are used to estimate the distance between the shadow
* caster and the shadow receiver, which is then used for the estimation of contact hardening
* in the shadow. This value should be a non-negative whole number. Higher values improve
* shadow quality by considering more occlusion points, but can decrease performance. When set
* to 0, contact hardening is disabled and the shadow has constant softness. Defaults to 16.
* Note that this value can be lower than shadowSamples to optimize performance, often without
* large impact on quality.
*
* @type {number}
*/
set shadowBlockerSamples(value) {
this._light.shadowBlockerSamples = value;
}
/**
* Gets the number of blocker samples used for contact hardening shadows.
*
* @type {number}
*/
get shadowBlockerSamples() {
return this._light.shadowBlockerSamples;
}
/**
* Sets the size of penumbra for contact hardening shadows. For area lights, acts as a
* multiplier with the dimensions of the area light. For punctual and directional lights it's
* the area size of the light. Defaults to 1.
*
* @type {number}
*/
set penumbraSize(value) {
this._light.penumbraSize = value;
}
/**
* Gets the size of penumbra for contact hardening shadows.
*
* @type {number}
*/
get penumbraSize() {
return this._light.penumbraSize;
}
/**
* Sets the falloff rate for shadow penumbra for contact hardening shadows. This is a value larger
* than or equal to 1. This parameter determines how quickly the shadow softens with distance.
* Higher values result in a faster softening of the shadow, while lower values produce a more
* gradual transition. Defaults to 1.
*
* @type {number}
*/
set penumbraFalloff(value) {
this._light.penumbraFalloff = value;
}
/**
* Gets the falloff rate for shadow penumbra for contact hardening shadows.
*
* @type {number}
*/
get penumbraFalloff() {
return this._light.penumbraFalloff;
}
addLightToLayers() {
for (let i = 0; i < this._layers.length; i++) {
const layer = this.system.app.scene.layers.getLayerById(this._layers[i]);
if (layer) {
layer.addLight(this);
this._light.addLayer(layer);
}
}
}
removeLightFromLayers() {
for (let i = 0; i < this._layers.length; i++) {
const layer = this.system.app.scene.layers.getLayerById(this._layers[i]);
if (layer) {
layer.removeLight(this);
this._light.removeLayer(layer);
}
}
}
onLayersChanged(oldComp, newComp) {
if (this.enabled && this.entity.enabled) {
this.addLightToLayers();
}
oldComp.off("add", this.onLayerAdded, this);
oldComp.off("remove", this.onLayerRemoved, this);
newComp.on("add", this.onLayerAdded, this);
newComp.on("remove", this.onLayerRemoved, this);
}
onLayerAdded(layer) {
const index = this._layers.indexOf(layer.id);
if (index >= 0 && this.enabled && this.entity.enabled) {
layer.addLight(this);
this._light.addLayer(layer);
}
}
onLayerRemoved(layer) {
const index = this._layers.indexOf(layer.id);
if (index >= 0) {
layer.removeLight(this);
this._light.removeLayer(layer);
}
}
refreshProperties() {
for (let i = 0; i < _properties.length; i++) {
const name = _properties[i];
this[name] = this[name];
}
if (this.enabled && this.entity.enabled) {
this.onEnable();
}
}
onCookieAssetSet() {
let forceLoad = false;
if (this._cookieAsset.type === "cubemap" && !this._cookieAsset.loadFaces) {
this._cookieAsset.loadFaces = true;
forceLoad = true;
}
if (!this._cookieAsset.resource || forceLoad) this.system.app.assets.load(this._cookieAsset);
if (this._cookieAsset.resource) {
this.onCookieAssetLoad();
}
}
onCookieAssetAdd(asset) {
if (this._cookieAssetId !== asset.id) return;
this._cookieAsset = asset;
if (this._light.enabled) {
this.onCookieAssetSet();
}
this._cookieAsset.on("load", this.onCookieAssetLoad, this);
this._cookieAsset.on("remove", this.onCookieAssetRemove, this);
}
onCookieAssetLoad() {
if (!this._cookieAsset || !this._cookieAsset.resource) {
return;
}
this.cookie = this._cookieAsset.resource;
}
onCookieAssetRemove() {
if (!this._cookieAssetId) {
return;
}
if (this._cookieAssetAdd) {
this.system.app.assets.off(`add:${this._cookieAssetId}`, this.onCookieAssetAdd, this);
this._cookieAssetAdd = false;
}
if (this._cookieAsset) {
this._cookieAsset.off("load", this.onCookieAssetLoad, this);
this._cookieAsset.off("remove", this.onCookieAssetRemove, this);
this._cookieAsset = null;
}
this.cookie = null;
}
onEnable() {
const scene = this.system.app.scene;
const layers = scene.layers;
this._light.enabled = true;
this._evtLayersChanged?.off();
this._evtLayersChanged = scene.on("set:layers", this.onLayersChanged, this);
if (layers) {
this._evtLayerAdded?.off();
this._evtLayerAdded = layers.on("add", this.onLayerAdded, this);
this._evtLayerRemoved?.off();
this._evtLayerRemoved = layers.on("remove", this.onLayerRemoved, this);
}
if (this.enabled && this.entity.enabled) {
this.addLightToLayers();
}
if (this._cookieAsset && !this.cookie) {
this.onCookieAssetSet();
}
}
onDisable() {
const scene = this.system.app.scene;
const layers = scene.layers;
this._light.enabled = false;
this._evtLayersChanged?.off();
this._evtLayersChanged = null;
if (layers) {
this._evtLayerAdded?.off();
this._evtLayerAdded = null;
this._evtLayerRemoved?.off();
this._evtLayerRemoved = null;
}
this.removeLightFromLayers();
}
onRemove() {
this.onDisable();
this._light.destroy();
this.cookieAsset = null;
}
}
export {
LightComponent,
_properties
};