@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
578 lines • 21.9 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { CameraHelper, Color, DirectionalLight, DirectionalLightHelper, PointLight, SpotLight, Vector3 } from "three";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { FrameEvent } from "../engine/engine_setup.js";
import { setWorldPositionXYZ } from "../engine/engine_three_utils.js";
import { getParam } from "../engine/engine_utils.js";
import { Behaviour } from "./Component.js";
// https://threejs.org/examples/webgl_shadowmap_csm.html
function toDegrees(radians) {
return radians * 180 / Math.PI;
}
function toRadians(degrees) {
return degrees * Math.PI / 180;
}
const shadowMaxDistance = 300;
const debug = getParam("debuglights");
/**
* Defines the type of light in a scene.
* @see {@link Light} for configuring light properties and behavior
*/
export var LightType;
(function (LightType) {
/** Spot light that emits light in a cone shape */
LightType[LightType["Spot"] = 0] = "Spot";
/** Directional light that emits parallel light rays in a specific direction */
LightType[LightType["Directional"] = 1] = "Directional";
/** Point light that emits light in all directions from a single point */
LightType[LightType["Point"] = 2] = "Point";
/** Area light */
LightType[LightType["Area"] = 3] = "Area";
/** Rectangle shaped area light that only affects baked lightmaps and light probes */
LightType[LightType["Rectangle"] = 3] = "Rectangle";
/** Disc shaped area light that only affects baked lightmaps and light probes */
LightType[LightType["Disc"] = 4] = "Disc";
})(LightType || (LightType = {}));
/**
* Defines how a light contributes to the scene lighting.
* @see {@link Light} for configuring light properties and behavior
*/
export var LightmapBakeType;
(function (LightmapBakeType) {
/** Light affects the scene in real-time with no baking */
LightmapBakeType[LightmapBakeType["Realtime"] = 4] = "Realtime";
/** Light is completely baked into lightmaps and light probes */
LightmapBakeType[LightmapBakeType["Baked"] = 2] = "Baked";
/** Combines aspects of realtime and baked lighting */
LightmapBakeType[LightmapBakeType["Mixed"] = 1] = "Mixed";
})(LightmapBakeType || (LightmapBakeType = {}));
/**
* Defines the shadow casting options for a Light.
* @enum {number}
* @see {@link Light} for configuring shadow settings
*/
var LightShadows;
(function (LightShadows) {
/** No shadows are cast */
LightShadows[LightShadows["None"] = 0] = "None";
/** Hard-edged shadows without filtering */
LightShadows[LightShadows["Hard"] = 1] = "Hard";
/** Soft shadows with PCF filtering */
LightShadows[LightShadows["Soft"] = 2] = "Soft";
})(LightShadows || (LightShadows = {}));
/**
* [Light](https://engine.needle.tools/docs/api/Light) creates a light source in the scene for illuminating 3D objects.
*
* **Light types:**
* - `Directional` - Sun-like parallel rays (best for outdoor scenes)
* - `Point` - Omnidirectional from a point (bulbs, candles)
* - `Spot` - Cone-shaped (flashlights, stage lights)
*
* **Shadows:**
* Enable shadows via `shadows` property. Configure quality with shadow resolution
* settings. Directional lights support adaptive shadow cascades.
*
* **Performance tips:**
* - Use baked lighting (`lightmapBakeType = Baked`) when possible
* - Limit shadow-casting lights (1-2 recommended)
* - Reduce shadow resolution for mobile
*
* **Debug:** Use `?debuglights` URL parameter for visual helpers.
*
* @example Configure a directional light
* ```ts
* const light = myLight.getComponent(Light);
* light.intensity = 1.5;
* light.color = new Color(1, 0.95, 0.9); // Warm white
* light.shadows = LightShadows.Soft;
* ```
*
* @summary Light component for various light types and shadow settings
* @category Rendering
* @group Components
* @see {@link LightType} for available light types
* @see {@link ReflectionProbe} for environment reflections
* @see {@link Camera} for rendering configuration
*/
export class Light extends Behaviour {
/**
* The type of light (spot, directional, point, etc.)
* Can not be changed at runtime.
*/
type = 0;
/**
* The maximum distance the light affects.
* Only applicable for spot and point lights.
*/
get range() {
return this._range;
}
set range(value) {
this._range = value;
if (this.light && (this.light.type === "SpotLight" || this.light.type === "PointLight") && ("distance" in this.light)) {
this.light.distance = value;
}
}
_range = 1;
/**
* The full outer angle of the spotlight cone in degrees.
* Only applicable for spot lights.
*/
get spotAngle() {
return this._spotAngle;
}
set spotAngle(value) {
this._spotAngle = value;
if (this.light && this.light.type === "SpotLight" && ("angle" in this.light)) {
this.light.angle = toRadians(value / 2);
}
}
_spotAngle = 30;
/**
* The angle of the inner cone in degrees for soft-edge spotlights.
* Must be less than or equal to the outer spot angle.
* Only applicable for spot lights.
*/
get innerSpotAngle() {
return this._innerSpotAngle;
}
set innerSpotAngle(value) {
this._innerSpotAngle = value;
if (this.light && this.light.type === "SpotLight" && ("penumbra" in this.light)) {
const outerAngle = this.spotAngle;
const innerAngle = value;
const penumbra = 1 - (toRadians(innerAngle / 2) / toRadians(outerAngle / 2));
this.light.penumbra = penumbra;
}
}
_innerSpotAngle = 10;
/**
* The color of the light
*/
set color(val) {
this._color = val;
if (this.light !== undefined) {
this.light.color = val;
}
}
get color() {
if (this.light)
return this.light.color;
return this._color;
}
_color = new Color(0xffffff);
/**
* The near plane distance for shadow projection
*/
set shadowNearPlane(val) {
if (val === this._shadowNearPlane)
return;
this._shadowNearPlane = val;
if (this.light?.shadow?.camera !== undefined) {
const cam = this.light.shadow.camera;
cam.near = val;
}
}
get shadowNearPlane() { return this._shadowNearPlane; }
_shadowNearPlane = .1;
/**
* Shadow bias value to reduce shadow acne and peter-panning
*/
set shadowBias(val) {
if (val === this._shadowBias)
return;
this._shadowBias = val;
if (this.light?.shadow?.bias !== undefined) {
this.light.shadow.bias = val;
this.light.shadow.needsUpdate = true;
}
}
get shadowBias() { return this._shadowBias; }
_shadowBias = 0;
/**
* Shadow normal bias to reduce shadow acne on sloped surfaces
*/
set shadowNormalBias(val) {
if (val === this._shadowNormalBias)
return;
this._shadowNormalBias = val;
if (this.light?.shadow?.normalBias !== undefined) {
this.light.shadow.normalBias = val;
this.light.shadow.needsUpdate = true;
}
}
get shadowNormalBias() { return this._shadowNormalBias; }
_shadowNormalBias = 0;
/** when enabled this will remove the multiplication when setting the shadow bias settings initially */
_overrideShadowBiasSettings = false;
/**
* Shadow casting mode (None, Hard, or Soft)
*/
set shadows(val) {
this._shadows = val;
if (this.light) {
this.light.castShadow = val !== LightShadows.None;
this.updateShadowSoftHard();
}
}
get shadows() { return this._shadows; }
_shadows = 1;
/**
* Determines if the light contributes to realtime lighting, baked lighting, or a mix
*/
lightmapBakeType = LightmapBakeType.Realtime;
/**
* Brightness of the light. In WebXR experiences, the intensity is automatically
* adjusted based on the AR session scale to maintain consistent lighting.
*/
set intensity(val) {
this._intensity = val;
if (this.light) {
this.light.intensity = val;
}
if (debug)
console.log("Set light intensity to " + this._intensity, val, this);
}
get intensity() { return this._intensity; }
_intensity = -1;
/**
* Maximum distance the shadow is projected
*/
get shadowDistance() {
const light = this.light;
if (light?.shadow) {
const cam = light.shadow.camera;
return cam.far;
}
return -1;
}
set shadowDistance(val) {
this._shadowDistance = val;
const light = this.light;
if (light?.shadow) {
const cam = light.shadow.camera;
cam.far = val;
cam.updateProjectionMatrix();
}
}
_shadowDistance;
// set from additional component
shadowWidth;
shadowHeight;
/**
* Resolution of the shadow map in pixels (width and height)
*/
get shadowResolution() {
const light = this.light;
if (light?.shadow) {
return light.shadow.mapSize.x;
}
return -1;
}
set shadowResolution(val) {
if (val === this._shadowResolution)
return;
this._shadowResolution = val;
const light = this.light;
if (light?.shadow) {
light.shadow.mapSize.set(val, val);
light.shadow.needsUpdate = true;
}
}
_shadowResolution = undefined;
/**
* Whether this light's illumination is entirely baked into lightmaps
*/
get isBaked() {
return this.lightmapBakeType === LightmapBakeType.Baked;
}
/**
* Checks if the GameObject itself is a {@link ThreeLight} object
*/
get selfIsLight() {
if (this.gameObject["isLight"] === true)
return true;
switch (this.gameObject.type) {
case "SpotLight":
case "PointLight":
case "DirectionalLight":
return true;
}
return false;
}
/**
* The underlying three.js {@link ThreeLight} instance
*/
light = undefined;
/**
* Gets the world position of the light
* @param vec Vector3 to store the result
* @returns The world position as a Vector3
*/
getWorldPosition(vec) {
if (this.light) {
if (this.type === LightType.Directional) {
return this.light.getWorldPosition(vec).multiplyScalar(1);
}
return this.light.getWorldPosition(vec);
}
return vec;
}
awake() {
this.color = new Color(this.color ?? 0xffffff);
if (debug)
console.log(this.name, this);
}
onEnable() {
if (debug)
console.log("ENABLE LIGHT", this.name);
this.createLight();
if (this.isBaked)
return;
else if (this.light) {
this.light.visible = true;
this.light.intensity = this._intensity;
if (debug)
console.log("Set light intensity to " + this.light.intensity, this.name);
if (this.selfIsLight) {
// nothing to do
}
else if (this.light.parent !== this.gameObject)
this.gameObject.add(this.light);
}
if (this.type === LightType.Directional)
this.startCoroutine(this.updateMainLightRoutine(), FrameEvent.LateUpdate);
}
onDisable() {
if (debug)
console.log("DISABLE LIGHT", this.name);
if (this.light) {
if (this.selfIsLight)
this.light.intensity = 0;
else
this.light.visible = false;
}
}
/**
* Creates the appropriate three.js light based on the configured light type
* and applies all settings like shadows, intensity, and color.
*/
createLight() {
const lightAlreadyCreated = this.selfIsLight;
if (lightAlreadyCreated && !this.light) {
this.light = this.gameObject;
this.light.name = this.name;
this._intensity = this.light.intensity;
switch (this.type) {
case LightType.Directional:
this.setDirectionalLight(this.light);
break;
}
}
else if (!this.light) {
switch (this.type) {
case LightType.Directional:
// console.log(this);
const dirLight = new DirectionalLight(this.color, this.intensity * Math.PI);
// directional light target is at 0 0 0 by default
dirLight.position.set(0, 0, -shadowMaxDistance * .5).applyQuaternion(this.gameObject.quaternion);
this.gameObject.add(dirLight.target);
setWorldPositionXYZ(dirLight.target, 0, 0, 0);
this.light = dirLight;
this.gameObject.position.set(0, 0, 0);
this.gameObject.rotation.set(0, 0, 0);
if (debug) {
const spotLightHelper = new DirectionalLightHelper(this.light, .2, this.color);
this.context.scene.add(spotLightHelper);
// const bh = new BoxHelper(this.context.scene, 0xfff0000);
// this.context.scene.add(bh);
}
break;
case LightType.Spot:
const spotLight = new SpotLight(this.color, this.intensity * Math.PI, this.range, toRadians(this.spotAngle / 2), 1 - toRadians(this.innerSpotAngle / 2) / toRadians(this.spotAngle / 2), 2);
spotLight.position.set(0, 0, 0);
spotLight.rotation.set(0, 0, 0);
this.light = spotLight;
const spotLightTarget = spotLight.target;
spotLight.add(spotLightTarget);
spotLightTarget.position.set(0, 0, this.range);
spotLightTarget.rotation.set(0, 0, 0);
break;
case LightType.Point:
const pointLight = new PointLight(this.color, this.intensity * Math.PI, this.range);
this.light = pointLight;
// const pointHelper = new PointLightHelper(pointLight, this.range, this.color);
// scene.add(pointHelper);
break;
}
}
if (this.light) {
if (this._intensity >= 0)
this.light.intensity = this._intensity;
else
this._intensity = this.light.intensity;
if (this.shadows !== LightShadows.None) {
this.light.castShadow = true;
}
else
this.light.castShadow = false;
if (this.light.shadow) {
// shadow intensity is currently not a thing: https://github.com/mrdoob/three.js/pull/14087
if (this._shadowResolution !== undefined && this._shadowResolution > 4) {
this.light.shadow.mapSize.width = this._shadowResolution;
this.light.shadow.mapSize.height = this._shadowResolution;
}
else {
this.light.shadow.mapSize.width = 2048;
this.light.shadow.mapSize.height = 2048;
}
// this.light.shadow.needsUpdate = true;
// console.log(this.light.shadow.mapSize);
// return;
if (debug)
console.log("Override shadow bias?", this._overrideShadowBiasSettings, this.shadowBias, this.shadowNormalBias);
this.light.shadow.bias = this.shadowBias;
this.light.shadow.normalBias = this.shadowNormalBias;
this.updateShadowSoftHard();
const cam = this.light.shadow.camera;
cam.near = this.shadowNearPlane;
// use shadow distance that was set explictly (if any)
if (this._shadowDistance !== undefined && typeof this._shadowDistance === "number")
cam.far = this._shadowDistance;
else // otherwise fallback to object scale and max distance
cam.far = shadowMaxDistance * Math.abs(this.gameObject.scale.z);
// width and height
this.gameObject.scale.set(1, 1, 1);
if (this.shadowWidth !== undefined) {
cam.left = -this.shadowWidth / 2;
cam.right = this.shadowWidth / 2;
}
else {
const sx = this.gameObject.scale.x;
cam.left *= sx;
cam.right *= sx;
}
if (this.shadowHeight !== undefined) {
cam.top = this.shadowHeight / 2;
cam.bottom = -this.shadowHeight / 2;
}
else {
const sy = this.gameObject.scale.y;
cam.top *= sy;
cam.bottom *= sy;
}
this.light.shadow.needsUpdate = true;
if (debug)
this.context.scene.add(new CameraHelper(cam));
}
if (this.isBaked) {
this.light.removeFromParent();
}
else if (!lightAlreadyCreated)
this.gameObject.add(this.light);
}
}
/**
* Coroutine that updates the main light reference in the context
* if this directional light should be the main light
*/
*updateMainLightRoutine() {
while (true) {
if (this.type === LightType.Directional) {
if (!this.context.mainLight || this.intensity > this.context.mainLight.intensity) {
this.context.mainLight = this;
}
yield;
}
break;
}
}
/**
* Controls whether the renderer's shadow map type can be changed when soft shadows are used
*/
static allowChangingRendererShadowMapType = true;
/**
* Updates shadow settings based on whether the shadows are set to hard or soft
*/
updateShadowSoftHard() {
if (!this.light)
return;
if (!this.light.shadow)
return;
if (this.shadows === LightShadows.Soft) {
// const radius = this.light.shadow.mapSize.width / 1024 * 5;
// const samples = Mathf.clamp(Math.round(radius), 2, 10);
// this.light.shadow.radius = radius;
// this.light.shadow.blurSamples = samples;
// if (isMobileDevice()) {
// this.light.shadow.radius *= .5;
// this.light.shadow.blurSamples = Math.floor(this.light.shadow.blurSamples * .5);
// }
// if (Light.allowChangingRendererShadowMapType) {
// if(this.context.renderer.shadowMap.type !== VSMShadowMap){
// if(isLocalNetwork()) console.warn("Changing renderer shadow map type to VSMShadowMap because a light with soft shadows enabled was found (this will cause all shadow receivers to also cast shadows). If you don't want this behaviour either set the shadow type to hard or set Light.allowChangingRendererShadowMapType to false.", this);
// this.context.renderer.shadowMap.type = VSMShadowMap;
// }
// }
}
else {
this.light.shadow.radius = 1;
this.light.shadow.blurSamples = 1;
}
}
/**
* Configures a directional light by adding and positioning its target
* @param dirLight The directional light to set up
*/
setDirectionalLight(dirLight) {
dirLight.add(dirLight.target);
dirLight.target.position.set(0, 0, -1);
// dirLight.position.add(vec.set(0,0,1).multiplyScalar(shadowMaxDistance*.1).applyQuaternion(this.gameObject.quaternion));
}
}
__decorate([
serializable()
], Light.prototype, "type", void 0);
__decorate([
serializable()
], Light.prototype, "range", null);
__decorate([
serializable()
], Light.prototype, "spotAngle", null);
__decorate([
serializable()
], Light.prototype, "innerSpotAngle", null);
__decorate([
serializable(Color)
], Light.prototype, "color", null);
__decorate([
serializable()
], Light.prototype, "shadowNearPlane", null);
__decorate([
serializable()
], Light.prototype, "shadowBias", null);
__decorate([
serializable()
], Light.prototype, "shadowNormalBias", null);
__decorate([
serializable()
], Light.prototype, "shadows", null);
__decorate([
serializable()
], Light.prototype, "lightmapBakeType", void 0);
__decorate([
serializable()
], Light.prototype, "intensity", null);
__decorate([
serializable()
], Light.prototype, "shadowDistance", null);
__decorate([
serializable()
], Light.prototype, "shadowResolution", null);
const vec = new Vector3(0, 0, 0);
//# sourceMappingURL=Light.js.map