@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.
705 lines • 30.1 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 { CompressedCubeTexture, CubeTexture, CubeUVReflectionMapping, EquirectangularRefractionMapping } from "three";
import { isDevEnvironment } from "../engine/debug/debug.js";
import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry.js";
import { syncField } from "../engine/engine_networking_auto.js";
import { loadPMREM } from "../engine/engine_pmrem.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { addAttributeChangeCallback, getParam, toSourceId } from "../engine/engine_utils.js";
import { registerObservableAttribute } from "../engine/webcomponents/needle-engine.extras.js";
import { Camera } from "./Camera.js";
import { Behaviour } from "./Component.js";
const debug = getParam("debugskybox");
export function initSkyboxAttributes() {
registerObservableAttribute("background-image");
registerObservableAttribute("environment-image");
installSkyboxAttributeHandlers();
}
function makeSlot() {
return { url: null, texture: null, loadId: 0 };
}
const elementHandlerState = new WeakMap();
const promises = new Array();
const DEG2RAD = Math.PI / 180;
function applyRotationAttribute(attr, target) {
const parts = attr.trim().split(/\s+/);
if (parts.length === 1) {
const y = parseFloat(parts[0]);
if (!isNaN(y))
target.set(0, y * DEG2RAD, 0);
}
else if (parts.length >= 3) {
const x = parseFloat(parts[0]), y = parseFloat(parts[1]), z = parseFloat(parts[2]);
if (!isNaN(x) && !isNaN(y) && !isNaN(z))
target.set(x * DEG2RAD, y * DEG2RAD, z * DEG2RAD);
}
}
function applyTexture(context, texture, isBackground, isEnvironment) {
if (!(texture instanceof CubeTexture || texture instanceof CompressedCubeTexture)
&& texture.mapping !== CubeUVReflectionMapping) {
texture.mapping = EquirectangularRefractionMapping;
texture.needsUpdate = true;
}
if (isEnvironment) {
context.scene.environment = texture;
}
if (isBackground && !Camera.backgroundShouldBeTransparent(context)) {
context.scene.background = texture;
}
const el = context.domElement;
if (isBackground) {
const blurriness = el.getAttribute("background-blurriness");
if (blurriness) {
const v = parseFloat(blurriness);
if (!isNaN(v))
context.scene.backgroundBlurriness = v;
}
else if (context.mainCameraComponent?.backgroundBlurriness !== undefined) {
context.scene.backgroundBlurriness = context.mainCameraComponent.backgroundBlurriness;
}
const intensity = el.getAttribute("background-intensity");
if (intensity) {
const v = parseFloat(intensity);
if (!isNaN(v))
context.scene.backgroundIntensity = v;
}
const rotation = el.getAttribute("background-rotation");
if (rotation) {
applyRotationAttribute(rotation, context.scene.backgroundRotation);
}
}
if (isEnvironment) {
const rotation = el.getAttribute("environment-rotation");
if (rotation) {
applyRotationAttribute(rotation, context.scene.environmentRotation);
}
}
}
function restoreGltfDefaults(context, slot, isBackground, isEnvironment) {
const sourceId = slot.url ? toSourceId(slot.url) : undefined;
slot.url = null;
slot.texture = null;
if (isEnvironment) {
if (!sourceId || !context.sceneLighting.internalEnableReflection(sourceId)) {
context.scene.environment = null;
}
}
if (isBackground) {
context.scene.background = sourceId ? context.lightmaps.tryGetSkybox(sourceId) : null;
}
}
function applyAttributeValue(state, slot, attribute, isBackground, isEnvironment, rawValue) {
const context = state.currentContext;
if (!context)
return null;
const value = rawValue ? tryParseMagicSkyboxName(rawValue, isEnvironment, isBackground) : null;
if (debug)
console.log(`Skybox attribute [${attribute}]: raw="${rawValue}" → resolved="${value}"`);
if (value && (value === "transparent" || value.startsWith("rgb") || value.startsWith("#"))) {
console.warn(`Needle Engine: Invalid ${attribute} value (${value}). Did you mean to set background-color instead?`);
return null;
}
const loadId = ++slot.loadId;
if (!value) {
restoreGltfDefaults(context, slot, isBackground, isEnvironment);
return null;
}
if (slot.url === value && slot.texture) {
if (debug)
console.log(`Skybox attribute [${attribute}]: cache hit, re-applying`);
applyTexture(context, slot.texture, isBackground, isEnvironment);
return null;
}
slot.url = value;
slot.texture = null;
const promise = (async () => {
if (debug)
console.log(`Skybox attribute [${attribute}]: loading ${value}`);
const texture = await loadPMREM(value, context.renderer);
if (slot.loadId !== loadId) {
if (debug)
console.warn(`Skybox attribute [${attribute}]: loadId mismatch (stale load), skipping apply`);
return;
}
if (!texture) {
if (debug)
console.warn(`Skybox attribute [${attribute}]: failed to load ${value}`);
return;
}
if (debug)
console.log(`Skybox attribute [${attribute}]: loaded, applying to scene`);
slot.texture = texture;
applyTexture(context, texture, isBackground, isEnvironment);
context.domElement.dispatchEvent(new CustomEvent(`${attribute}-loaded`, {
detail: { texture },
}));
})();
return promise;
}
function getCurrentState(args) {
const state = elementHandlerState.get(args.context.domElement);
if (!state || state.currentContext !== args.context)
return null;
return state;
}
let _skyboxHandlersInstalled = false;
function installSkyboxAttributeHandlers() {
if (_skyboxHandlersInstalled)
return;
_skyboxHandlersInstalled = true;
ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, (args) => {
const context = args.context;
const el = context.domElement;
let state = elementHandlerState.get(el);
if (!state) {
state = { currentContext: context, bg: makeSlot(), env: makeSlot(), listenersInstalled: false };
elementHandlerState.set(el, state);
}
else {
state.currentContext = context;
}
const bgLoad = applyAttributeValue(state, state.bg, "background-image", true, false, el.getAttribute("background-image"));
if (bgLoad)
promises.push(bgLoad);
const envLoad = applyAttributeValue(state, state.env, "environment-image", false, true, el.getAttribute("environment-image"));
if (envLoad)
promises.push(envLoad);
if (!state.listenersInstalled) {
state.listenersInstalled = true;
const stateRef = state;
addAttributeChangeCallback(el, "background-image", (rawValue) => {
const v = (typeof rawValue === "string" && rawValue.length > 0) ? rawValue : null;
if (debug)
console.log("background-image CHANGED TO", v);
applyAttributeValue(stateRef, stateRef.bg, "background-image", true, false, v);
});
addAttributeChangeCallback(el, "environment-image", (rawValue) => {
const v = (typeof rawValue === "string" && rawValue.length > 0) ? rawValue : null;
if (debug)
console.log("environment-image CHANGED TO", v);
applyAttributeValue(stateRef, stateRef.env, "environment-image", false, true, v);
});
}
});
ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, () => {
return Promise.all(promises).finally(() => { promises.length = 0; });
});
ContextRegistry.registerCallback(ContextEvent.ContextClearing, (args) => {
const state = getCurrentState(args);
if (!state)
return;
state.bg = makeSlot();
state.env = makeSlot();
});
ContextRegistry.registerCallback(ContextEvent.ContextDestroyed, (args) => {
const state = getCurrentState(args);
if (!state)
return;
state.currentContext = null;
state.bg.loadId++;
state.env.loadId++;
});
}
// #endregion
// #region RemoteSkybox
/**
* The [RemoteSkybox](https://engine.needle.tools/docs/api/RemoteSkybox) component allows you to set the skybox or environment texture of a scene from a URL, a local file or a static skybox name.
* It supports .hdr, .exr, .jpg, .png, and .ktx2 files.
*
* **HTML Attributes:**
* You can control skybox and environment from HTML using `<needle-engine>` attributes:
* - `background-image`: Sets the scene background/skybox image
* - `environment-image`: Sets the scene environment map (for reflections and lighting)
*
* These attributes accept URLs or magic skybox names (see examples below).
*
* **Magic Skybox Names:**
* Built-in optimized skyboxes hosted on Needle CDN:
* - `"studio"` - Neutral studio lighting (default)
* - `"blurred-skybox"` - Blurred environment
* - `"quicklook"` - Apple QuickLook object mode style
* - `"quicklook-ar"` - Apple QuickLook AR mode style
*
* ### 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 Using HTML attributes
* ```html
* <needle-engine
* background-image="https://example.com/skybox.hdr"
* environment-image="studio">
* </needle-engine>
* ```
*
* @example Using magic skybox names
* ```html
* <needle-engine background-image="studio"></needle-engine>
* <needle-engine environment-image="quicklook"></needle-engine>
* ```
*
* @example Adding via code
* ```ts
* GameObject.addComponent(gameObject, RemoteSkybox, {
* url: "https://example.com/skybox.hdr",
* background: true,
* environment: true
* });
* ```
*
* @example Handle custom dropped URL
* ```ts
* const skybox = GameObject.addComponent(gameObject, RemoteSkybox);
* 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 at runtime
* ```ts
* skybox.setSkybox("https://example.com/skybox.hdr");
* // Or use a magic name:
* skybox.setSkybox("studio");
* ```
*
* @summary Sets the skybox or environment texture of a scene
* @category Rendering
* @group Components
* @see {@link Camera} for clearFlags and background control
* @link https://engine.needle.tools/docs/html.html#needle-engine-element
*/
export class RemoteSkybox extends Behaviour {
/**
* URL to a remote skybox.
* To update the skybox/environment map use `setSkybox(url)`.
*
* The url can also be set to a magic skybox name.
* Magic name options are: "quicklook", "quicklook-ar", "studio", "blurred-skybox".
* These will resolve to built-in skyboxes hosted on the Needle CDN that are static, optimized for the web and will never change.
*
* @example
* ```ts
* skybox.url = "https://example.com/skybox.hdr";
* ```
*/
url = "studio";
/**
* 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;
_prevUrl;
_prevLoadedEnvironment;
/** Promise returned by the most recent in-flight {@link setSkybox} load.
* The URL it's loading is `_prevUrl` (set synchronously before the
* awaited work begins). When a second `setSkybox(sameUrl)` comes in
* while `_prevLoadedEnvironment` hasn't been populated yet, we return
* this promise instead of kicking off a duplicate `loadPMREM` — same
* URL, same texture, no reason to race two loads.
*
* Without this, the create path fetches the same URL twice:
* `createRemoteSkyboxComponent` calls `setSkybox(url)` eagerly to get
* a promise for the ContextCreationStart barrel, AND the component's
* `onEnable()` lifecycle also calls `setSkybox(this.url)` — both
* before `_prevLoadedEnvironment` is populated, so the fast-path cache
* check doesn't fire and `loadPMREM` runs twice. */
_inFlightLoad;
_prevEnvironment = null;
_prevBackground = null;
/**
* If our texture is still installed in `scene.environment` /
* `scene.background`, restore the snapshot {@link apply} took before it
* wrote there. Slots owned by something else are left alone.
*
* @returns `true` if either slot was ours (and was reverted).
*/
revertAppliedSceneState() {
if (!this.context || !this._prevLoadedEnvironment)
return false;
let reverted = false;
if (this.context.scene.environment === this._prevLoadedEnvironment) {
this.context.scene.environment = this._prevEnvironment;
reverted = true;
}
if (this.context.scene.background === this._prevLoadedEnvironment) {
// Don't restore a stale concrete background when the camera config
// wants transparency — leave the slot for the transparent path to
// manage.
if (!Camera.backgroundShouldBeTransparent(this.context)) {
this.context.scene.background = this._prevBackground;
}
reverted = true;
}
return reverted;
}
/**
* Discard the result of any in-flight {@link setSkybox} call AND revert
* any already-applied skybox state from this RemoteSkybox so the scene
* looks as if this component never ran.
*
* Automatically called when the `background-image` / `environment-image`
* HTML attribute is cleared — callers normally don't need to invoke it
* directly.
*
* Covers two races:
*
* 1. **In-flight load.** `setSkybox` re-checks `_prevUrl` after `await
* loadPMREM(...)` and bails if it changed; resetting it here trips
* that check on the in-flight call.
* 2. **Already-applied load.** If `loadPMREM` resolved fast (HTTP cache
* hit), `apply()` already ran. The in-flight check above doesn't
* help — `revertAppliedSceneState` restores the prior snapshot.
*
* NOTE: this does NOT abort the underlying network fetch / PMREM
* generation — `loadPMREM` has no cancellation. It only flips state so
* `setSkybox` bails after the await and skips `apply()`. The texture
* returned by `loadPMREM` in those bail-out branches (`_prevUrl !== url`
* and disabled/destroyed) is still dropped without `dispose()` — a small
* GPU leak that should be fixed at those sites.
* @internal
*/
discardPendingLoad() {
this._prevUrl = undefined;
// Clear the in-flight tracking so a subsequent setSkybox(sameUrl)
// doesn't latch onto the now-discarded load via the dedup path. The
// running `await load` is not cancelled (loadPMREM has no cancel
// surface) but its post-await `_prevUrl !== url` check trips and
// short-circuits before apply().
this._inFlightLoad = undefined;
if (!this.context) {
// Context is gone — the cached texture has no owner. Release GPU
// memory rather than orphan it.
this._prevLoadedEnvironment?.dispose();
this._prevLoadedEnvironment = undefined;
return;
}
this.revertAppliedSceneState();
this._prevLoadedEnvironment = undefined;
}
/** @internal */
onEnable() {
this.setSkybox(this.url);
this.registerDropEvents();
}
/** @internal */
onDisable() {
if (this.revertAppliedSceneState()) {
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 || this.gameObject?.name || "context"));
}
if (debug)
console.log("Set RemoteSkybox url: " + url);
if (this._prevUrl === url) {
// Same URL as last/current setSkybox call.
if (this._prevLoadedEnvironment) {
// Load already completed — replay apply() against the cached texture.
this.apply();
return true;
}
if (this._inFlightLoad) {
// Load still running — return its promise instead of starting
// a second loadPMREM. Same URL = same texture = nothing to gain
// from racing two loads. See _inFlightLoad doc for the create-
// path callers that hit this.
return this._inFlightLoad;
}
// Same URL, no cached result, no in-flight load → fall through
// (e.g. discardPendingLoad cleared _inFlightLoad but a stale
// _prevUrl assignment is rare; treat as a fresh start).
}
// Different URL (or same URL with no live load) — release the previous
// result and start fresh.
this._prevLoadedEnvironment?.dispose();
this._prevLoadedEnvironment = undefined;
this._prevUrl = url;
const load = (async () => {
const texture = await loadPMREM(url, this.context.renderer);
if (!texture) {
if (debug)
console.warn("RemoteSkybox: Failed to load texture from url", url);
return false;
}
// Check if we're not disabled or destroyed
if (!this.enabled || this.destroyed) {
if (debug)
console.warn("RemoteSkybox: Component is disabled or destroyed");
return false;
}
// Check if the url has changed while loading (covers both another
// setSkybox(differentUrl) and discardPendingLoad clearing _prevUrl)
if (this._prevUrl !== url) {
if (debug)
console.warn("RemoteSkybox: URL changed while loading texture, aborting setSkybox");
return false;
}
// Update the current url
this.url = url;
this._prevLoadedEnvironment = texture;
this.apply();
return true;
})();
this._inFlightLoad = load;
try {
return await load;
}
finally {
// Only clear if we're still the active in-flight load — another
// setSkybox call with a different URL may have replaced us, and
// we shouldn't wipe that newer caller's tracking.
if (this._inFlightLoad === load) {
this._inFlightLoad = undefined;
}
}
}
apply() {
const envMap = this._prevLoadedEnvironment;
if (!envMap)
return;
if ((envMap instanceof CubeTexture || envMap instanceof CompressedCubeTexture) || envMap.mapping == CubeUVReflectionMapping) {
// Nothing to do
}
else {
envMap.mapping = EquirectangularRefractionMapping;
envMap.needsUpdate = true;
}
if (this.destroyed)
return;
if (!this.context) {
console.warn("RemoteSkybox: Context is not available - can not apply skybox.");
return;
}
// 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 RemoteSkybox (" + ((this.environment && this.background) ? "environment and background" : this.environment ? "environment" : this.background ? "background" : "none") + ")", 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;
}
validProtocols = ["file:", "blob:", "data:"];
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.includes(type))
return true;
}
for (const protocol of this.validProtocols) {
if (url.startsWith(protocol))
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);
const MagicSkyboxNames = {
"studio": {
url: "https://cdn.needle.tools/static/skybox/modelviewer-Neutral.pmrem4x4.ktx2?pmrem",
url_low: "https://cdn.needle.tools/static/skybox/modelviewer-Neutral-small.pmrem4x4.ktx2?pmrem"
},
"blurred-skybox": {
url: "https://cdn.needle.tools/static/skybox/blurred-skybox.pmrem4x4.ktx2?pmrem",
url_low: "https://cdn.needle.tools/static/skybox/blurred-skybox-small.pmrem4x4.ktx2?pmrem"
},
"quicklook-ar": {
url: "https://cdn.needle.tools/static/skybox/QuickLook-ARMode.pmrem4x4.ktx2?pmrem",
url_low: "https://cdn.needle.tools/static/skybox/QuickLook-ARMode-small.pmrem4x4.ktx2?pmrem"
},
"quicklook": {
url: "https://cdn.needle.tools/static/skybox/QuickLook-ObjectMode.pmrem4x4.ktx2?pmrem",
url_low: "https://cdn.needle.tools/static/skybox/QuickLook-ObjectMode-small.pmrem4x4.ktx2?pmrem"
}
};
function tryParseMagicSkyboxName(str, environment, background) {
if (str === null || str === undefined)
return null;
const useLowRes = environment && !background;
const value = MagicSkyboxNames[str.toLowerCase()];
if (value) {
return useLowRes ? value.url_low : value.url;
}
else if (typeof str === "string" && str?.length && (isDevEnvironment() || debug)) {
// Only warn if the string looks like it was meant to be a magic skybox name.
// Strings that contain "/" or "." are paths or URLs, not magic names.
const looksLikePath = str.includes("/") || str.includes(".");
if (!looksLikePath) {
console.warn(`RemoteSkybox: Unknown magic skybox name "${str}". Valid names are: ${Object.keys(MagicSkyboxNames).map(n => `"${n}"`).join(", ")}`);
}
}
return str;
}
//# sourceMappingURL=Skybox.js.map