@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
478 lines • 19.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, GameObject } from "./Component.js";
import { WebARSessionRoot } from "./webxr/WebARSessionRoot.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");
/// <summary>
/// <para>The type of a Light.</para>
/// </summary>
export var LightType;
(function (LightType) {
/// <summary>
/// <para>The light is a spot light.</para>
/// </summary>
LightType[LightType["Spot"] = 0] = "Spot";
/// <summary>
/// <para>The light is a directional light.</para>
/// </summary>
LightType[LightType["Directional"] = 1] = "Directional";
/// <summary>
/// <para>The light is a point light.</para>
/// </summary>
LightType[LightType["Point"] = 2] = "Point";
LightType[LightType["Area"] = 3] = "Area";
/// <summary>
/// <para>The light is a rectangle shaped area light. It affects only baked lightmaps and lightprobes.</para>
/// </summary>
LightType[LightType["Rectangle"] = 3] = "Rectangle";
/// <summary>
/// <para>The light is a disc shaped area light. It affects only baked lightmaps and lightprobes.</para>
/// </summary>
LightType[LightType["Disc"] = 4] = "Disc";
})(LightType || (LightType = {}));
export var LightmapBakeType;
(function (LightmapBakeType) {
/// <summary>
/// <para>Realtime lights cast run time light and shadows. They can change position, orientation, color, brightness, and many other properties at run time. No lighting gets baked into lightmaps or light probes..</para>
/// </summary>
LightmapBakeType[LightmapBakeType["Realtime"] = 4] = "Realtime";
/// <summary>
/// <para>Baked lights cannot move or change in any way during run time. All lighting for static objects gets baked into lightmaps. Lighting and shadows for dynamic objects gets baked into Light Probes.</para>
/// </summary>
LightmapBakeType[LightmapBakeType["Baked"] = 2] = "Baked";
/// <summary>
/// <para>Mixed lights allow a mix of realtime and baked lighting, based on the Mixed Lighting Mode used. These lights cannot move, but can change color and intensity at run time. Changes to color and intensity only affect direct lighting as indirect lighting gets baked. If using Subtractive mode, changes to color or intensity are not calculated at run time on static objects.</para>
/// </summary>
LightmapBakeType[LightmapBakeType["Mixed"] = 1] = "Mixed";
})(LightmapBakeType || (LightmapBakeType = {}));
/// <summary>
/// <para>Shadow casting options for a Light.</para>
/// </summary>
var LightShadows;
(function (LightShadows) {
/// <summary>
/// <para>Do not cast shadows (default).</para>
/// </summary>
LightShadows[LightShadows["None"] = 0] = "None";
/// <summary>
/// <para>Cast "hard" shadows (with no shadow filtering).</para>
/// </summary>
LightShadows[LightShadows["Hard"] = 1] = "Hard";
/// <summary>
/// <para>Cast "soft" shadows (with 4x PCF filtering).</para>
/// </summary>
LightShadows[LightShadows["Soft"] = 2] = "Soft";
})(LightShadows || (LightShadows = {}));
/** The Light component can be used to create a light source in the scene.
* It can be used to create a directional light, a spot light or a point light.
* The light can be set to cast shadows and the shadow type can be set to hard or soft shadows.
* The light can be set to be baked or realtime.
* The light can be set to be a main light which will be used for the main directional light in the scene.
* @category Rendering
* @group Components
*/
export class Light extends Behaviour {
type = 0;
range = 1;
spotAngle = 1;
innerSpotAngle = 1;
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);
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;
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;
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;
set shadows(val) {
this._shadows = val;
if (this.light) {
this.light.castShadow = val !== LightShadows.None;
this.updateShadowSoftHard();
}
}
get shadows() { return this._shadows; }
_shadows = 1;
lightmapBakeType = LightmapBakeType.Realtime;
set intensity(val) {
this._intensity = val;
if (this.light) {
let factor = 1;
if (this.context.isInXR && this._webARRoot) {
const scaleFactor = this._webARRoot?.arScale;
if (typeof scaleFactor === "number" && scaleFactor > 0) {
factor /= scaleFactor;
}
}
this.light.intensity = val * factor;
}
if (debug)
console.log("Set light intensity to " + this._intensity, val, this);
}
get intensity() { return this._intensity; }
_intensity = -1;
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;
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;
get isBaked() {
return this.lightmapBakeType === LightmapBakeType.Baked;
}
get selfIsLight() {
if (this.gameObject["isLight"] === true)
return true;
switch (this.gameObject.type) {
case "SpotLight":
case "PointLight":
case "DirectionalLight":
return true;
}
return false;
}
light = undefined;
getWorldPosition(vec) {
if (this.light) {
if (this.type === LightType.Directional) {
return this.light.getWorldPosition(vec).multiplyScalar(1);
}
return this.light.getWorldPosition(vec);
}
return vec;
}
// public updateIntensity() {
// this.intensity = this._intensity;
// }
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;
}
}
_webXRStartedListener;
_webXREndedListener;
_webARRoot;
onEnterXR(_args) {
this._webARRoot = GameObject.getComponentInParent(this.gameObject, WebARSessionRoot) ?? undefined;
// this.startCoroutine(this._updateLightIntensityInARRoutine());
}
// private *_updateLightIntensityInARRoutine() {
// while (this.context.isInAR) {
// yield;
// // this.updateIntensity();
// for (let i = 0; i < 30; i++) yield;
// }
// }
onLeaveXR(_args) {
// this.updateIntensity();
}
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);
}
}
*updateMainLightRoutine() {
while (true) {
if (this.type === LightType.Directional) {
if (!this.context.mainLight || this.intensity > this.context.mainLight.intensity) {
this.context.mainLight = this;
}
yield;
}
break;
}
}
static allowChangingRendererShadowMapType = true;
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;
}
}
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(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