@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.
470 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 { createLoaders } from "@needle-tools/gltf-progressive";
import { CompressedCubeTexture, CubeTexture, EquirectangularRefractionMapping, SRGBColorSpace, TextureLoader } from "three";
import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader.js";
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
import { disposeObjectResources, setDisposable } from "../engine/engine_assetdatabase.js";
import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
import { syncField } from "../engine/engine_networking_auto.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { addAttributeChangeCallback, getParam, removeAttributeChangeCallback } from "../engine/engine_utils.js";
import { registerObservableAttribute } from "../engine/webcomponents/needle-engine.extras.js";
import { Camera, ClearFlags } from "./Camera.js";
import { Behaviour, GameObject } from "./Component.js";
const debug = getParam("debugskybox");
registerObservableAttribute("background-image");
registerObservableAttribute("environment-image");
function createRemoteSkyboxComponent(context, url, skybox, environment, attribute) {
const remote = new RemoteSkybox();
remote.allowDrop = false;
remote.allowNetworking = false;
remote.background = skybox;
remote.environment = environment;
GameObject.addComponent(context.scene, remote);
const urlChanged = newValue => {
if (typeof newValue !== "string")
return;
if (debug)
console.log(attribute, "CHANGED TO", newValue);
remote.setSkybox(newValue);
};
addAttributeChangeCallback(context.domElement, attribute, urlChanged);
remote.addEventListener("destroy", () => {
if (debug)
console.log("Destroyed attribute remote skybox", attribute);
removeAttributeChangeCallback(context.domElement, attribute, urlChanged);
});
return remote.setSkybox(url);
}
const promises = new Array();
ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, (args) => {
const context = args.context;
const skyboxImage = context.domElement.getAttribute("background-image");
const environmentImage = context.domElement.getAttribute("environment-image");
if (skyboxImage) {
if (debug)
console.log("Creating remote skybox to load " + skyboxImage);
// if the user is loading a GLB without a camera then the CameraUtils (which creates the default camera)
// checks if we have this attribute set and then sets the skybox clearflags accordingly
// if the user has a GLB with a camera but set to solid color then the skybox image is not visible -> we will just warn then and not override the camera settings
if (context.mainCameraComponent?.clearFlags !== ClearFlags.Skybox)
console.warn("\"background-image\" attribute has no effect: camera clear flags are not set to \"Skybox\"");
const promise = createRemoteSkyboxComponent(context, skyboxImage, true, false, "background-image");
promises.push(promise);
}
if (environmentImage) {
if (debug)
console.log("Creating remote environment to load " + environmentImage);
const promise = createRemoteSkyboxComponent(context, environmentImage, false, true, "environment-image");
promises.push(promise);
}
});
ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, () => {
return Promise.all(promises).finally(() => {
promises.length = 0;
});
});
function ensureGlobalCache() {
if (!globalThis["NEEDLE_ENGINE_SKYBOX_TEXTURES"])
globalThis["NEEDLE_ENGINE_SKYBOX_TEXTURES"] = new Array();
return globalThis["NEEDLE_ENGINE_SKYBOX_TEXTURES"];
}
function tryGetPreviouslyLoadedTexture(src) {
const cache = ensureGlobalCache();
const found = cache.find(x => x.src === src);
if (found) {
if (debug)
console.log("Skybox: Found previously loaded texture for " + src);
return found.texture;
}
return null;
}
async function disposeCachedTexture(tex) {
const texture = await tex;
setDisposable(texture, true);
disposeObjectResources(texture);
}
function registerLoadedTexture(src, texture) {
const cache = ensureGlobalCache();
// Make sure the cache doesnt get too big
while (cache.length > 5) {
const entry = cache.shift();
if (entry) {
disposeCachedTexture(entry.texture);
}
}
texture.then(t => { return setDisposable(t, false); });
cache.push({ src, texture });
}
/**
* RemoteSkybox is a component that allows you to set the skybox of a scene from a URL or a local file.
* It supports .hdr, .exr, .jpg, .png files.
*
* ### Events
* - `dropped-unknown-url`: Emitted when a file is dropped on the scene. The event detail contains the sender, the url and a function to apply the url.
*
* @example adding a skybox
* ```ts
* GameObject.addComponent(gameObject, Skybox, { url: "https://example.com/skybox.hdr", background: true, environment: true });
* ```
*
* @example handle custom url
* ```ts
* const skybox = GameObject.addComponent(gameObject, Skybox);
* skybox.addEventListener("dropped-unknown-url", (evt) => {
* let url = evt.detail.url;
* console.log("User dropped file", url);
* // change url or resolve it differently
* url = "https://example.com/skybox.hdr";
* // apply the url
* evt.detail.apply(url);
* });
* ```
*
* @example update skybox url
* ```ts
* skybox.setSkybox("https://example.com/skybox.hdr");
* ```
*/
export class RemoteSkybox extends Behaviour {
/**
* URL to a remote skybox. This value can also use a magic skybox name. Options are "quicklook", "quicklook-ar", "studio", "blurred-skybox".
* To update the skybox/environment map use `setSkybox(url)`
* @example
* ```ts
* skybox.url = "https://example.com/skybox.hdr";
* ```
*/
url;
/**
* When enabled a user can drop a link to a skybox image on the scene to set the skybox.
* @default true
*/
allowDrop = true;
/**
* When enabled the skybox will be set as the background of the scene.
* @default true
*/
background = true;
/**
* When enabled the skybox will be set as the environment of the scene (to be used as environment map for reflections and lighting)
* @default true
*/
environment = true;
/**
* When enabled dropped skybox urls (or assigned skybox urls) will be networked to other users in the same networked room.
* @default true
*/
allowNetworking = true;
_loader;
_prevUrl;
_prevLoadedEnvironment;
_prevEnvironment = null;
_prevBackground = null;
/** @internal */
onEnable() {
this.setSkybox(this.url);
this.registerDropEvents();
}
/** @internal */
onDisable() {
if (this.context.scene.environment === this._prevLoadedEnvironment) {
this.context.scene.environment = this._prevEnvironment;
if (!Camera.backgroundShouldBeTransparent(this.context))
this.context.scene.background = this._prevBackground;
this._prevLoadedEnvironment = undefined;
}
this.unregisterDropEvents();
// Re-apply the skybox/background settings of the main camera
this.context.mainCameraComponent?.applyClearFlags();
}
urlChangedSyncField() {
if (this.allowNetworking && this.url) {
// omit local dragged files from being handled
if (this.isRemoteTexture(this.url)) {
this.setSkybox(this.url);
}
else if (debug) {
console.warn(`RemoteSkybox: Not setting skybox: ${this.url} is not a remote texture. If you want to set a local texture, set allowNetworking to false.`);
}
}
}
/**
* Set the skybox from a given url
* @param url The url of the skybox image
* @param name Define name of the file with extension if it isn't apart of the url
* @returns Whether the skybox was successfully set
*/
async setSkybox(url, name) {
if (!this.activeAndEnabled)
return false;
url = tryParseMagicSkyboxName(url, this.environment, this.background);
if (!url)
return false;
name ??= url;
if (!this.isValidTextureType(name)) {
console.warn("Potentially invalid skybox url", name, "on", this.name);
}
if (debug)
console.log("Set remote skybox url: " + url);
if (this._prevUrl === url && this._prevLoadedEnvironment) {
this.apply();
return true;
}
else {
this._prevLoadedEnvironment?.dispose();
this._prevLoadedEnvironment = undefined;
}
this._prevUrl = url;
const texture = await this.loadTexture(url, name);
if (!texture) {
if (debug)
console.warn("RemoteSkybox: Failed to load texture from url", url);
return false;
}
// Check if we're still enabled
if (!this.enabled) {
if (debug)
console.warn("RemoteSkybox: Component is not enabled, aborting setSkybox");
return false;
}
// Check if the url has changed while loading
if (this._prevUrl !== url) {
if (debug)
console.warn("RemoteSkybox: URL changed while loading texture, aborting setSkybox");
return false; // URL changed while loading
}
// Update the current url
this.url = url;
const nameIndex = url.lastIndexOf("/");
texture.name = url.substring(nameIndex >= 0 ? nameIndex + 1 : 0);
if (this._loader instanceof TextureLoader) {
texture.colorSpace = SRGBColorSpace;
}
this._prevLoadedEnvironment = texture;
this.apply();
return true;
}
async loadTexture(url, name) {
if (!url)
return Promise.resolve(null);
name ??= url;
const cached = tryGetPreviouslyLoadedTexture(name);
if (cached) {
const res = await cached;
if (res.source?.data?.length > 0 || res.source?.data?.data?.length)
return res;
}
const isEXR = name.endsWith(".exr");
const isHdr = name.endsWith(".hdr");
const isKtx2 = name.endsWith(".ktx2");
if (isEXR) {
if (!(this._loader instanceof EXRLoader))
this._loader = new EXRLoader();
}
else if (isHdr) {
if (!(this._loader instanceof RGBELoader))
this._loader = new RGBELoader();
}
else if (isKtx2) {
if (!(this._loader instanceof KTX2Loader)) {
const { ktx2Loader } = createLoaders(this.context.renderer);
this._loader = ktx2Loader;
}
}
else {
if (!(this._loader instanceof TextureLoader))
this._loader = new TextureLoader();
}
if (debug)
console.log("Loading skybox: " + url);
const loadingTask = this._loader.loadAsync(url);
registerLoadedTexture(name, loadingTask);
const envMap = await loadingTask;
return envMap;
}
apply() {
const envMap = this._prevLoadedEnvironment;
if (!envMap)
return;
if ((envMap instanceof CubeTexture || envMap instanceof CompressedCubeTexture)) {
// Nothing to do
}
else {
envMap.mapping = EquirectangularRefractionMapping;
envMap.needsUpdate = true;
}
// capture state
if (this.context.scene.background !== envMap)
this._prevBackground = this.context.scene.background;
if (this.context.scene.environment !== envMap)
this._prevEnvironment = this.context.scene.environment;
if (debug)
console.log("Set remote skybox", this.url, !Camera.backgroundShouldBeTransparent(this.context));
if (this.environment)
this.context.scene.environment = envMap;
if (this.background && !Camera.backgroundShouldBeTransparent(this.context))
this.context.scene.background = envMap;
if (this.context.mainCameraComponent?.backgroundBlurriness !== undefined)
this.context.scene.backgroundBlurriness = this.context.mainCameraComponent.backgroundBlurriness;
}
validTextureTypes = [".ktx2", ".hdr", ".exr", ".jpg", ".jpeg", ".png"];
isRemoteTexture(url) {
return url.startsWith("http://") || url.startsWith("https://");
}
isValidTextureType(url) {
for (const type of this.validTextureTypes) {
if (url.endsWith(type))
return true;
}
return false;
}
registerDropEvents() {
this.unregisterDropEvents();
this.context.domElement.addEventListener("dragover", this.onDragOverEvent);
this.context.domElement.addEventListener("drop", this.onDrop);
}
unregisterDropEvents() {
this.context.domElement.removeEventListener("dragover", this.onDragOverEvent);
this.context.domElement.removeEventListener("drop", this.onDrop);
}
onDragOverEvent = (e) => {
if (!this.allowDrop)
return;
if (!e.dataTransfer)
return;
for (const type of e.dataTransfer.types) {
// in ondragover we dont get access to the content
// but if we have a uri list we can assume
// someone is maybe dragging a image file
// so we want to capture this
if (type === "text/uri-list" || type === "Files") {
e.preventDefault();
}
}
};
onDrop = (e) => {
if (!this.allowDrop)
return;
if (!e.dataTransfer)
return;
for (const type of e.dataTransfer.types) {
if (debug)
console.log(type);
if (type === "text/uri-list") {
const url = e.dataTransfer.getData(type);
if (debug)
console.log(type, url);
let name = new RegExp(/polyhaven.com\/asset_img\/.+?\/(?<name>.+)\.png/).exec(url)?.groups?.name;
if (!name) {
name = new RegExp(/polyhaven\.com\/a\/(?<name>.+)/).exec(url)?.groups?.name;
}
if (debug)
console.log(name);
if (name) {
const skyboxurl = "https://dl.polyhaven.org/file/ph-assets/HDRIs/exr/1k/" + name + "_1k.exr";
console.log(`[Remote Skybox] Setting skybox from url: ${skyboxurl}`);
e.preventDefault();
this.setSkybox(skyboxurl);
break;
}
else if (this.isValidTextureType(url)) {
console.log("[Remote Skybox] Setting skybox from url: " + url);
e.preventDefault();
this.setSkybox(url);
break;
}
else {
console.warn(`[RemoteSkybox] Unknown url ${url}. If you want to load a skybox from a url, make sure it is a valid image url. Url must end with${this.validTextureTypes.join(", ")}.`);
// emit custom event - users can listen to this event and handle the url themselves
const evt = new CustomEvent("dropped-unknown-url", {
detail: {
sender: this,
event: e,
url,
apply: (url) => {
e.preventDefault();
this.setSkybox(url);
}
}
});
this.dispatchEvent(evt);
}
}
else if (type == "Files") {
const file = e.dataTransfer.files.item(0);
if (debug)
console.log(type, file);
if (!file)
continue;
if (!this.isValidTextureType(file.name)) {
console.warn(`[RemoteSkybox]: File \"${file.name}\" is not supported. Supported files are ${this.validTextureTypes.join(", ")}`);
return;
}
if (tryGetPreviouslyLoadedTexture(file.name) === null) {
const blob = new Blob([file]);
const url = URL.createObjectURL(blob);
e.preventDefault();
this.setSkybox(url, file.name);
}
else {
e.preventDefault();
this.setSkybox(file.name);
}
break;
}
}
};
}
__decorate([
syncField(RemoteSkybox.prototype.urlChangedSyncField),
serializable(URL)
], RemoteSkybox.prototype, "url", void 0);
__decorate([
serializable()
], RemoteSkybox.prototype, "allowDrop", void 0);
__decorate([
serializable()
], RemoteSkybox.prototype, "background", void 0);
__decorate([
serializable()
], RemoteSkybox.prototype, "environment", void 0);
__decorate([
serializable()
], RemoteSkybox.prototype, "allowNetworking", void 0);
function tryParseMagicSkyboxName(str, environment, background) {
const useLowRes = environment && !background;
switch (str?.toLowerCase()) {
case "studio":
if (useLowRes) {
return "https://cdn.needle.tools/static/skybox/modelviewer-Neutral-small.hdr";
}
else
return "https://cdn.needle.tools/static/skybox/modelviewer-Neutral.hdr";
case "blurred-skybox":
if (useLowRes) {
return "https://cdn.needle.tools/static/skybox/blurred-skybox-small.exr";
}
return "https://cdn.needle.tools/static/skybox/blurred-skybox.exr";
case "quicklook-ar":
if (useLowRes) {
return "https://cdn.needle.tools/static/skybox/QuickLook-ARMode-small.exr";
}
return "https://cdn.needle.tools/static/skybox/QuickLook-ARMode.exr";
case "quicklook":
if (useLowRes) {
return "https://cdn.needle.tools/static/skybox/QuickLook-ObjectMode-small.exr";
}
return "https://cdn.needle.tools/static/skybox/QuickLook-ObjectMode.exr";
}
if (str === undefined)
return null;
return str;
}
//# sourceMappingURL=Skybox.js.map