@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.
186 lines (162 loc) โข 8.29 kB
text/typescript
import { AmbientLight, Color, HemisphereLight, Object3D } from "three";
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
import { Behaviour, GameObject } from "../../engine-components/Component.js";
import { ContextEvent, ContextRegistry } from "../engine_context_registry.js";
import { Mathf } from "../engine_math.js";
import { AmbientMode, DefaultReflectionMode } from "../engine_scenelighting.js";
import { Context } from "../engine_setup.js";
import { type SourceIdentifier } from "../engine_types.js";
import { getParam } from "../engine_utils.js";
import { LightmapType } from "./NEEDLE_lightmaps.js";
export const EXTENSION_NAME = "NEEDLE_lighting_settings";
const debug = getParam("debugenvlight");
export type LightingSettings = {
ambientMode: AmbientMode;
ambientIntensity: number,
ambientLight: number[],
ambientTrilight: Array<number[]>,
environmentReflectionSource: DefaultReflectionMode;
}
export class NEEDLE_lighting_settings implements GLTFLoaderPlugin {
get name(): string {
return EXTENSION_NAME;
}
private parser: GLTFParser;
private sourceId: SourceIdentifier;
private context: Context;
constructor(parser: GLTFParser, sourceId: SourceIdentifier, context: Context) {
this.parser = parser;
this.sourceId = sourceId;
this.context = context;
}
afterRoot(_result: GLTF): Promise<void> | null {
const extensions = this.parser.json.extensions;
if (extensions) {
const ext: LightingSettings = extensions[EXTENSION_NAME];
if (ext) {
if (debug)
console.log("Loaded \"" + this.name + "\", src: \"" + this.sourceId + "\"", ext);
let settings: SceneLightSettings | undefined = undefined;
// If the result scene has only one child we add the LightingSettingsComponent to that child
if (_result.scene.children.length === 1) {
const obj = _result.scene.children[0];
// add a component to the root of the scene
settings = GameObject.addComponent(obj, SceneLightSettings, {}, { callAwake: false });
}
// if the scene already has multiple children we add it as a new object
else {
const lightSettings = new Object3D();
lightSettings.name = "LightSettings " + this.sourceId;
_result.scene.add(lightSettings);
settings = GameObject.addComponent(lightSettings, SceneLightSettings, {}, { callAwake: false });
}
settings.sourceId = this.sourceId;
settings.ambientIntensity = ext.ambientIntensity;
settings.ambientLight = new Color().fromArray(ext.ambientLight);
if (Array.isArray(ext.ambientTrilight))
settings.ambientTrilight = ext.ambientTrilight.map(c => new Color().fromArray(c));
settings.ambientMode = ext.ambientMode;
settings.environmentReflectionSource = ext.environmentReflectionSource;
}
}
return null;
}
}
ContextRegistry.registerCallback(ContextEvent.ContextCreated, e => {
const ctx = e.context as Context;
const lightingSettings = GameObject.findObjectOfType(SceneLightSettings, ctx as Context);
if (lightingSettings?.sourceId) lightingSettings.enabled = true;
})
// exists once per gltf scene root (if it contains reflection)
// when enabled it does currently automatically set the reflection
// this might not be desireable
export class SceneLightSettings extends Behaviour {
ambientMode: AmbientMode = AmbientMode.Skybox;
ambientLight?: Color;
ambientTrilight?: Color[];
ambientIntensity: number = 1;
environmentReflectionSource: DefaultReflectionMode = DefaultReflectionMode.Skybox;
private _hasReflection: boolean = false;
private _ambientLightObj?: AmbientLight;
private _hemisphereLightObj?: HemisphereLight;
awake() {
if (this.sourceId) {
const type = this.environmentReflectionSource === DefaultReflectionMode.Skybox ? LightmapType.Skybox : LightmapType.Reflection;
const tex = this.context.lightmaps.tryGet(this.sourceId, type, 0);
this._hasReflection = tex !== null && tex !== undefined;
if (tex)
this.context.sceneLighting.internalRegisterReflection(this.sourceId, tex);
}
this.enabled = false;
this.context.sceneLighting.internalRegisterSceneLightSettings(this);
if (debug) {
window.addEventListener("keydown", evt => {
if(this.destroyed) return;
switch (evt.key) {
case "l":
this.enabled = !this.enabled;
break;
}
});
}
// make sure the component is in the end of the list
// (e.g. if we have an animation on the first component from an instance and add the scenelightingcomponent the animation binding will break)
const comps = this.gameObject.userData?.components;
if (comps) {
const index = comps.indexOf(this);
comps.splice(index, 1);
comps.push(this);
}
}
onDestroy(): void {
this.context.sceneLighting.internalUnregisterSceneLightSettings(this);
}
private calculateIntensityFactor(col:Color){
const intensity = Math.max(col.r, col.g, col.b);// * 0.2126 + col.g * 0.7152 + col.b * 0.0722;
const factor = 2.2 * Mathf.lerp(0, 1.33, intensity); // scale based on intensity
return factor;
}
onEnable(): void {
if (debug) console.warn("๐ก๐ก >>> Enable lighting", this.sourceId, this.enabled, this);
if (this.ambientMode == AmbientMode.Flat) {
if (this.ambientLight && !this._ambientLightObj) {
// TODO: currently ambient intensity is always exported as 1? The exported values are not correct in threejs
// the following calculation is a workaround to get the correct intensity
const factor = this.calculateIntensityFactor(this.ambientLight);
this._ambientLightObj = new AmbientLight(this.ambientLight, this.ambientIntensity * factor);
if (debug) console.log("Created ambient light", this.sourceId, this._ambientLightObj, this.ambientIntensity, factor)
}
if (this._ambientLightObj) {
this.gameObject.add(this._ambientLightObj)
}
}
else if (this.ambientMode === AmbientMode.Trilight) {
if (this.ambientTrilight) {
const ground = this.ambientTrilight[0];
const sky = this.ambientTrilight[this.ambientTrilight.length - 1];
const factor = this.calculateIntensityFactor(sky);
this._hemisphereLightObj = new HemisphereLight(sky, ground, this.ambientIntensity * factor);
this.gameObject.add(this._hemisphereLightObj)
if (debug) console.log("Created hemisphere ambient light", this.sourceId, this._hemisphereLightObj, this.ambientIntensity, factor)
}
}
else {
if (this._ambientLightObj)
this._ambientLightObj.removeFromParent();
if (this._hemisphereLightObj)
this._hemisphereLightObj.removeFromParent();
}
if (this.sourceId)
this.context.sceneLighting.internalEnableReflection(this.sourceId);
}
onDisable() {
if (debug)
console.warn("๐กโซ <<< Disable lighting:", this.sourceId, this);
if (this._ambientLightObj)
this._ambientLightObj.removeFromParent();
if (this._hemisphereLightObj)
this._hemisphereLightObj.removeFromParent();
if (this.sourceId)
this.context.sceneLighting.internalDisableReflection(this.sourceId);
}
}