@needle-tools/gltf-progressive
Version:
three.js support for loading glTF or GLB files that contain progressive loading data
864 lines (863 loc) • 38.2 kB
JavaScript
import { BufferGeometry, Mesh, Texture, TextureLoader } from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { addDracoAndKTX2Loaders } from "./loaders.js";
import { getParam, resolveUrl } from "./utils.internal.js";
import { getRaycastMesh, registerRaycastMesh } from "./utils.js";
// All of this has to be removed
// import { getRaycastMesh, setRaycastMesh } from "../../engine_physics.js";
// import { PromiseAllWithErrors, resolveUrl } from "../../engine_utils.js";
import { plugins } from "./plugins/plugin.js";
export const EXTENSION_NAME = "NEEDLE_progressive";
const debug = getParam("debugprogressive");
const $progressiveTextureExtension = Symbol("needle-progressive-texture");
const debug_toggle_maps = new Map();
const debug_materials = new Set();
if (debug) {
let currentDebugLodLevel = -1;
let maxLevel = 2;
let wireframe = false;
function debugToggleProgressive() {
currentDebugLodLevel += 1;
console.log("Toggle LOD level", currentDebugLodLevel, debug_toggle_maps);
debug_toggle_maps.forEach((arr, obj) => {
for (const key of arr.keys) {
const cur = obj[key];
// if it's null or undefined we skip it
if (cur == null) {
continue;
}
if (cur.isBufferGeometry === true) {
const info = NEEDLE_progressive.getMeshLODInformation(cur);
const level = !info ? 0 : Math.min(currentDebugLodLevel, info.lods.length);
obj["DEBUG:LOD"] = level;
// NEEDLE_progressive.assignMeshLOD(obj as Mesh, level);
if (info)
maxLevel = Math.max(maxLevel, info.lods.length - 1);
}
else if (obj.isMaterial === true) {
obj["DEBUG:LOD"] = currentDebugLodLevel;
// NEEDLE_progressive.assignTextureLOD(obj as Material, currentDebugLodLevel);
}
}
});
if (currentDebugLodLevel >= maxLevel) {
currentDebugLodLevel = -1;
}
}
window.addEventListener("keyup", evt => {
if (evt.key === "p")
debugToggleProgressive();
if (evt.key === "w") {
wireframe = !wireframe;
if (debug_materials) {
debug_materials.forEach(mat => {
// we don't want to change the skybox material
if (mat.name == "BackgroundCubeMaterial")
return;
if (mat["glyphMap"] != undefined)
return;
if ("wireframe" in mat) {
mat.wireframe = wireframe;
}
});
}
}
});
}
function registerDebug(obj, key, sourceId) {
if (!debug)
return;
if (!debug_toggle_maps.has(obj)) {
debug_toggle_maps.set(obj, { keys: [], sourceId });
}
const existing = debug_toggle_maps.get(obj);
if (existing?.keys?.includes(key) == false) {
existing.keys.push(key);
}
}
/**
* The NEEDLE_progressive extension for the GLTFLoader is responsible for loading progressive LODs for meshes and textures.
* This extension can be used to load different resolutions of a mesh or texture at runtime (e.g. for LODs or progressive textures).
* @example
* ```javascript
* const loader = new GLTFLoader();
* loader.register(new NEEDLE_progressive());
* loader.load("model.glb", (gltf) => {
* const mesh = gltf.scene.children[0] as Mesh;
* NEEDLE_progressive.assignMeshLOD(context, sourceId, mesh, 1).then(mesh => {
* console.log("Mesh with LOD level 1 loaded", mesh);
* });
* });
* ```
*/
export class NEEDLE_progressive {
/** The name of the extension */
get name() {
return EXTENSION_NAME;
}
static getMeshLODInformation(geo) {
const info = this.getAssignedLODInformation(geo);
if (info?.key) {
return this.lodInfos.get(info.key);
}
return null;
}
static getMaterialMinMaxLODsCount(material, minmax) {
const self = this;
// we can cache this material min max data because it wont change at runtime
const cacheKey = "LODS:minmax";
const cached = material[cacheKey];
if (cached != undefined)
return cached;
if (!minmax) {
minmax = {
min_count: Infinity,
max_count: 0,
lods: [],
};
}
if (Array.isArray(material)) {
for (const mat of material) {
this.getMaterialMinMaxLODsCount(mat, minmax);
}
material[cacheKey] = minmax;
return minmax;
}
if (debug === "verbose")
console.log("getMaterialMinMaxLODsCount", material);
if (material.type === "ShaderMaterial" || material.type === "RawShaderMaterial") {
const mat = material;
for (const slot of Object.keys(mat.uniforms)) {
const val = mat.uniforms[slot].value;
if (val?.isTexture === true) {
processTexture(val, minmax);
}
}
}
else if (material.isMaterial) {
for (const slot of Object.keys(material)) {
const val = material[slot];
if (val?.isTexture === true) {
processTexture(val, minmax);
}
}
}
material[cacheKey] = minmax;
return minmax;
function processTexture(tex, minmax) {
const info = self.getAssignedLODInformation(tex);
if (info) {
const model = self.lodInfos.get(info.key);
if (model && model.lods) {
minmax.min_count = Math.min(minmax.min_count, model.lods.length);
minmax.max_count = Math.max(minmax.max_count, model.lods.length);
for (let i = 0; i < model.lods.length; i++) {
const lod = model.lods[i];
if (lod.width) {
minmax.lods[i] = minmax.lods[i] || { min_height: Infinity, max_height: 0 };
minmax.lods[i].min_height = Math.min(minmax.lods[i].min_height, lod.height);
minmax.lods[i].max_height = Math.max(minmax.lods[i].max_height, lod.height);
}
}
}
}
}
}
/** Check if a LOD level is available for a mesh or a texture
* @param obj the mesh or texture to check
* @param level the level of detail to check for (0 is the highest resolution). If undefined, the function checks if any LOD level is available
* @returns true if the LOD level is available (or if any LOD level is available if level is undefined)
*/
static hasLODLevelAvailable(obj, level) {
if (Array.isArray(obj)) {
for (const mat of obj) {
if (this.hasLODLevelAvailable(mat, level))
return true;
}
return false;
}
if (obj.isMaterial === true) {
for (const slot of Object.keys(obj)) {
const val = obj[slot];
if (val && val.isTexture) {
if (this.hasLODLevelAvailable(val, level))
return true;
}
}
return false;
}
else if (obj.isGroup === true) {
for (const child of obj.children) {
if (child.isMesh === true) {
if (this.hasLODLevelAvailable(child, level))
return true;
}
}
}
let lodObject;
let lodInformation;
if (obj.isMesh) {
lodObject = obj.geometry;
}
else if (obj.isBufferGeometry) {
lodObject = obj;
}
else if (obj.isTexture) {
lodObject = obj;
}
if (lodObject) {
if (lodObject?.userData?.LODS) {
const lods = lodObject.userData.LODS;
lodInformation = this.lodInfos.get(lods.key);
if (level === undefined)
return lodInformation != undefined;
if (lodInformation) {
if (Array.isArray(lodInformation.lods)) {
return level < lodInformation.lods.length;
}
return level === 0;
}
}
}
return false;
}
/** Load a different resolution of a mesh (if available)
* @param context the context
* @param source the sourceid of the file from which the mesh is loaded (this is usually the component's sourceId)
* @param mesh the mesh to load the LOD for
* @param level the level of detail to load (0 is the highest resolution)
* @returns a promise that resolves to the mesh with the requested LOD level
* @example
* ```javascript
* const mesh = this.gameObject as Mesh;
* NEEDLE_progressive.assignMeshLOD(context, sourceId, mesh, 1).then(mesh => {
* console.log("Mesh with LOD level 1 loaded", mesh);
* });
* ```
*/
static assignMeshLOD(mesh, level) {
if (!mesh)
return Promise.resolve(null);
if (mesh instanceof Mesh || mesh.isMesh === true) {
const currentGeometry = mesh.geometry;
const lodinfo = this.getAssignedLODInformation(currentGeometry);
if (!lodinfo) {
return Promise.resolve(null);
}
for (const plugin of plugins) {
plugin.onBeforeGetLODMesh?.(mesh, level);
}
// const info = this.onProgressiveLoadStart(context, source, mesh, null);
mesh["LOD:requested level"] = level;
return NEEDLE_progressive.getOrLoadLOD(currentGeometry, level).then(geo => {
if (Array.isArray(geo)) {
const index = lodinfo.index || 0;
geo = geo[index];
}
if (mesh["LOD:requested level"] === level) {
delete mesh["LOD:requested level"];
if (geo && currentGeometry != geo) {
const isGeometry = geo?.isBufferGeometry;
// if (debug == "verbose") console.log("Progressive Mesh " + mesh.name + " loaded", currentGeometry, "→", geo, "\n", mesh)
if (isGeometry) {
mesh.geometry = geo;
if (debug)
registerDebug(mesh, "geometry", lodinfo.url);
}
else if (debug) {
console.error("Invalid LOD geometry", geo);
}
}
}
// this.onProgressiveLoadEnd(info);
return geo;
}).catch(err => {
// this.onProgressiveLoadEnd(info);
console.error("Error loading mesh LOD", mesh, err);
return null;
});
}
else if (debug) {
console.error("Invalid call to assignMeshLOD: Request mesh LOD but the object is not a mesh", mesh);
}
return Promise.resolve(null);
}
static assignTextureLOD(materialOrTexture, level = 0) {
if (!materialOrTexture)
return Promise.resolve(null);
if (materialOrTexture.isMesh === true) {
const mesh = materialOrTexture;
if (Array.isArray(mesh.material)) {
const arr = new Array();
for (const mat of mesh.material) {
const promise = this.assignTextureLOD(mat, level);
arr.push(promise);
}
return Promise.all(arr).then(res => {
const textures = new Array();
for (const tex of res) {
if (Array.isArray(tex)) {
textures.push(...tex);
}
}
return textures;
});
}
else {
return this.assignTextureLOD(mesh.material, level);
}
}
if (materialOrTexture.isMaterial === true) {
const material = materialOrTexture;
const promises = [];
const slots = new Array();
if (debug)
debug_materials.add(material);
// Handle custom shaders / uniforms progressive textures. This includes support for VRM shaders
if (material.uniforms && (material.isRawShaderMaterial || material.isShaderMaterial === true)) {
// iterate uniforms of custom shaders
const shaderMaterial = material;
for (const slot of Object.keys(shaderMaterial.uniforms)) {
const val = shaderMaterial.uniforms[slot].value;
if (val?.isTexture === true) {
const task = this.assignTextureLODForSlot(val, level, material, slot).then(res => {
if (res && shaderMaterial.uniforms[slot].value != res) {
shaderMaterial.uniforms[slot].value = res;
shaderMaterial.uniformsNeedUpdate = true;
}
return res;
});
promises.push(task);
slots.push(slot);
}
}
}
else {
for (const slot of Object.keys(material)) {
const val = material[slot];
if (val?.isTexture === true) {
const task = this.assignTextureLODForSlot(val, level, material, slot);
promises.push(task);
slots.push(slot);
}
}
}
return Promise.all(promises).then(res => {
const textures = new Array();
for (let i = 0; i < res.length; i++) {
const tex = res[i];
const slot = slots[i];
if (tex && tex.isTexture === true) {
textures.push({ material, slot, texture: tex, level });
}
else {
textures.push({ material, slot, texture: null, level });
}
}
return textures;
});
}
if (materialOrTexture instanceof Texture || materialOrTexture.isTexture === true) {
const texture = materialOrTexture;
return this.assignTextureLODForSlot(texture, level, null, null);
}
return Promise.resolve(null);
}
static assignTextureLODForSlot(current, level, material, slot) {
if (current?.isTexture !== true) {
return Promise.resolve(null);
}
if (slot === "glyphMap") {
return Promise.resolve(current);
}
return NEEDLE_progressive.getOrLoadLOD(current, level).then(tex => {
// this can currently not happen
if (Array.isArray(tex))
return null;
if (tex?.isTexture === true) {
if (tex != current) {
if (material && slot) {
const assigned = material[slot];
// Check if the assigned texture LOD is higher quality than the current LOD
// This is necessary for cases where e.g. a texture is updated via an explicit call to assignTextureLOD
if (assigned && !debug) {
const assignedLOD = this.getAssignedLODInformation(assigned);
if (assignedLOD && assignedLOD?.level < level) {
if (debug === "verbose")
console.warn("Assigned texture level is already higher: ", assignedLOD.level, level, material, assigned, tex);
return null;
}
// assigned.dispose();
}
material[slot] = tex;
}
if (debug && slot && material) {
const lodinfo = this.getAssignedLODInformation(current);
if (lodinfo)
registerDebug(material, slot, lodinfo.url);
else
console.warn("No LOD info for texture", current);
}
// check if the old texture is still used by other objects
// if not we dispose it...
// this could also be handled elsewhere and not be done immediately
// const users = getResourceUserCount(current);
// if (!users) {
// if (debug) console.log("Progressive: Dispose texture", current.name, current.source.data, current.uuid);
// current?.dispose();
// }
}
// this.onProgressiveLoadEnd(info);
return tex;
}
else if (debug == "verbose") {
console.warn("No LOD found for", current, level);
}
// this.onProgressiveLoadEnd(info);
return null;
}).catch(err => {
// this.onProgressiveLoadEnd(info);
console.error("Error loading LOD", current, err);
return null;
});
}
parser;
url;
constructor(parser, url) {
if (debug)
console.log("Progressive extension registered for", url);
this.parser = parser;
this.url = url;
}
_isLoadingMesh;
loadMesh = (meshIndex) => {
if (this._isLoadingMesh)
return null;
const ext = this.parser.json.meshes[meshIndex]?.extensions?.[EXTENSION_NAME];
if (!ext)
return null;
this._isLoadingMesh = true;
return this.parser.getDependency("mesh", meshIndex).then(mesh => {
this._isLoadingMesh = false;
if (mesh) {
NEEDLE_progressive.registerMesh(this.url, ext.guid, mesh, ext.lods?.length, undefined, ext);
}
return mesh;
});
};
afterRoot(gltf) {
if (debug)
console.log("AFTER", this.url, gltf);
this.parser.json.textures?.forEach((textureInfo, index) => {
if (textureInfo?.extensions) {
const ext = textureInfo?.extensions[EXTENSION_NAME];
if (ext) {
if (!ext.lods) {
if (debug)
console.warn("Texture has no LODs", ext);
return;
}
let found = false;
for (const key of this.parser.associations.keys()) {
if (key.isTexture === true) {
const val = this.parser.associations.get(key);
if (val?.textures === index) {
found = true;
NEEDLE_progressive.registerTexture(this.url, key, ext.lods?.length, index, ext);
}
}
}
// If textures aren't used there are no associations - we still want to register the LOD info so we create one instance
if (!found) {
this.parser.getDependency("texture", index).then(tex => {
if (tex) {
NEEDLE_progressive.registerTexture(this.url, tex, ext.lods?.length, index, ext);
}
});
}
}
}
});
this.parser.json.meshes?.forEach((meshInfo, index) => {
if (meshInfo?.extensions) {
const ext = meshInfo?.extensions[EXTENSION_NAME];
if (ext && ext.lods) {
let found = false;
for (const entry of this.parser.associations.keys()) {
if (entry.isMesh) {
const val = this.parser.associations.get(entry);
if (val?.meshes === index) {
found = true;
NEEDLE_progressive.registerMesh(this.url, ext.guid, entry, ext.lods.length, val.primitives, ext);
}
}
}
// Note: we use loadMesh rather than this method so the mesh is surely registered at the right time when the mesh is created
// // If meshes aren't used there are no associations - we still want to register the LOD info so we create one instance
// if (!found) {
// this.parser.getDependency("mesh", index).then(mesh => {
// if (mesh) {
// NEEDLE_progressive.registerMesh(this.url, ext.guid, mesh as Mesh, ext.lods.length, undefined, ext);
// }
// });
// }
}
}
});
return null;
}
/**
* Register a texture with LOD information
*/
static registerTexture = (url, tex, level, index, ext) => {
if (debug)
console.log("> Progressive: register texture", index, tex.name, tex.uuid, tex, ext);
if (!tex) {
if (debug)
console.error("gltf-progressive: Register texture without texture");
return;
}
// Put the extension info into the source (seems like tiled textures are cloned and the userdata etc is not properly copied BUT the source of course is not cloned)
// see https://github.com/needle-tools/needle-engine-support/issues/133
if (tex.source)
tex.source[$progressiveTextureExtension] = ext;
const LODKEY = ext.guid;
NEEDLE_progressive.assignLODInformation(url, tex, LODKEY, level, index, undefined);
NEEDLE_progressive.lodInfos.set(LODKEY, ext);
NEEDLE_progressive.lowresCache.set(LODKEY, tex);
};
/**
* Register a mesh with LOD information
*/
static registerMesh = (url, key, mesh, level, index, ext) => {
if (debug)
console.log("> Progressive: register mesh", index, mesh.name, ext, mesh.uuid, mesh);
const geometry = mesh.geometry;
if (!geometry) {
if (debug)
console.warn("gltf-progressive: Register mesh without geometry");
return;
}
if (!geometry.userData)
geometry.userData = {};
NEEDLE_progressive.assignLODInformation(url, geometry, key, level, index, ext.density);
NEEDLE_progressive.lodInfos.set(key, ext);
let existing = NEEDLE_progressive.lowresCache.get(key);
if (existing)
existing.push(mesh.geometry);
else
existing = [mesh.geometry];
NEEDLE_progressive.lowresCache.set(key, existing);
if (level > 0 && !getRaycastMesh(mesh)) {
registerRaycastMesh(mesh, geometry);
}
for (const plugin of plugins) {
plugin.onRegisteredNewMesh?.(mesh, ext);
}
};
/** A map of key = asset uuid and value = LOD information */
static lodInfos = new Map();
/** cache of already loaded mesh lods */
static previouslyLoaded = new Map();
/** this contains the geometry/textures that were originally loaded */
static lowresCache = new Map();
static async getOrLoadLOD(current, level) {
const debugverbose = debug == "verbose";
/** this key is used to lookup the LOD information */
const LOD = current.userData.LODS;
if (!LOD) {
return null;
}
const LODKEY = LOD?.key;
let progressiveInfo;
// See https://github.com/needle-tools/needle-engine-support/issues/133
if (current.isTexture === true) {
const tex = current;
if (tex.source && tex.source[$progressiveTextureExtension])
progressiveInfo = tex.source[$progressiveTextureExtension];
}
if (!progressiveInfo)
progressiveInfo = NEEDLE_progressive.lodInfos.get(LODKEY);
if (progressiveInfo) {
if (level > 0) {
let useLowRes = false;
const hasMultipleLevels = Array.isArray(progressiveInfo.lods);
if (hasMultipleLevels && level >= progressiveInfo.lods.length) {
useLowRes = true;
}
else if (!hasMultipleLevels) {
useLowRes = true;
}
if (useLowRes) {
const lowres = this.lowresCache.get(LODKEY);
return lowres;
}
}
/** the unresolved LOD url */
const unresolved_lod_url = Array.isArray(progressiveInfo.lods) ? progressiveInfo.lods[level]?.path : progressiveInfo.lods;
// check if we have a uri
if (!unresolved_lod_url) {
if (debug && !progressiveInfo["missing:uri"]) {
progressiveInfo["missing:uri"] = true;
console.warn("Missing uri for progressive asset for LOD " + level, progressiveInfo);
}
return null;
}
/** the resolved LOD url */
const lod_url = resolveUrl(LOD.url, unresolved_lod_url);
// check if the requested file needs to be loaded via a GLTFLoader
if (lod_url.endsWith(".glb") || lod_url.endsWith(".gltf")) {
if (!progressiveInfo.guid) {
console.warn("missing pointer for glb/gltf texture", progressiveInfo);
return null;
}
// check if the requested file has already been loaded
const KEY = lod_url + "_" + progressiveInfo.guid;
// check if the requested file is currently being loaded
const existing = this.previouslyLoaded.get(KEY);
if (existing !== undefined) {
if (debugverbose)
console.log(`LOD ${level} was already loading/loaded: ${KEY}`);
let res = await existing.catch(err => {
console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
return null;
});
let resouceIsDisposed = false;
if (res == null) {
// if the resource is null the last loading result didnt succeed (maybe because the url doesnt exist)
// in which case we don't attempt to load it again
}
else if (res instanceof Texture && current instanceof Texture) {
// check if the texture has been disposed or not
if (res.image?.data || res.source?.data) {
res = this.copySettings(current, res);
}
// if it has been disposed we need to load it again
else {
resouceIsDisposed = true;
this.previouslyLoaded.delete(KEY);
}
}
else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
if (res.attributes.position?.array) {
// the geometry is OK
}
else {
resouceIsDisposed = true;
this.previouslyLoaded.delete(KEY);
}
}
if (!resouceIsDisposed) {
return res;
}
}
const ext = progressiveInfo;
const request = new Promise(async (resolve, _) => {
const loader = new GLTFLoader();
addDracoAndKTX2Loaders(loader);
if (debug) {
await new Promise(resolve => setTimeout(resolve, 1000));
if (debugverbose)
console.warn("Start loading (delayed) " + lod_url, ext.guid);
}
let url = lod_url;
if (ext && Array.isArray(ext.lods)) {
const lodinfo = ext.lods[level];
if (lodinfo.hash) {
url += "?v=" + lodinfo.hash;
}
}
const gltf = await loader.loadAsync(url).catch(err => {
console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
return null;
});
if (!gltf)
return null;
const parser = gltf.parser;
if (debugverbose)
console.log("Loading finished " + lod_url, ext.guid);
let index = 0;
if (gltf.parser.json.textures) {
let found = false;
for (const tex of gltf.parser.json.textures) {
// find the texture index
if (tex?.extensions) {
const other = tex?.extensions[EXTENSION_NAME];
if (other?.guid) {
if (other.guid === ext.guid) {
found = true;
break;
}
}
}
index++;
}
if (found) {
let tex = await parser.getDependency("texture", index);
if (tex) {
NEEDLE_progressive.assignLODInformation(LOD.url, tex, LODKEY, level, undefined, undefined);
}
if (debugverbose)
console.log("change \"" + current.name + "\" → \"" + tex.name + "\"", lod_url, index, tex, KEY);
if (current instanceof Texture)
tex = this.copySettings(current, tex);
if (tex) {
tex.guid = ext.guid;
}
return resolve(tex);
}
else if (debug) {
console.warn("Could not find texture with guid", ext.guid, gltf.parser.json);
}
}
index = 0;
if (gltf.parser.json.meshes) {
let found = false;
for (const mesh of gltf.parser.json.meshes) {
// find the mesh index
if (mesh?.extensions) {
const other = mesh?.extensions[EXTENSION_NAME];
if (other?.guid) {
if (other.guid === ext.guid) {
found = true;
break;
}
}
}
index++;
}
if (found) {
const mesh = await parser.getDependency("mesh", index);
const meshExt = ext;
if (debugverbose)
console.log(`Loaded Mesh \"${mesh.name}\"`, lod_url, index, mesh, KEY);
if (mesh.isMesh === true) {
const geo = mesh.geometry;
NEEDLE_progressive.assignLODInformation(LOD.url, geo, LODKEY, level, undefined, meshExt.density);
return resolve(geo);
}
else {
const geometries = new Array();
for (let i = 0; i < mesh.children.length; i++) {
const child = mesh.children[i];
if (child.isMesh === true) {
const geo = child.geometry;
NEEDLE_progressive.assignLODInformation(LOD.url, geo, LODKEY, level, i, meshExt.density);
geometries.push(geo);
}
}
return resolve(geometries);
}
}
else if (debug) {
console.warn("Could not find mesh with guid", ext.guid, gltf.parser.json);
}
}
// we could not find a texture or mesh with the given guid
return resolve(null);
});
this.previouslyLoaded.set(KEY, request);
const res = await request;
return res;
}
else {
if (current instanceof Texture) {
if (debugverbose)
console.log("Load texture from uri: " + lod_url);
const loader = new TextureLoader();
const tex = await loader.loadAsync(lod_url);
if (tex) {
tex.guid = progressiveInfo.guid;
tex.flipY = false;
tex.needsUpdate = true;
tex.colorSpace = current.colorSpace;
if (debugverbose)
console.log(progressiveInfo, tex);
}
else if (debug)
console.warn("failed loading", lod_url);
return tex;
}
}
}
else {
if (debug)
console.warn(`Can not load LOD ${level}: no LOD info found for \"${LODKEY}\" ${current.name}`, current.type);
}
return null;
}
static assignLODInformation(url, res, key, level, index, density) {
if (!res)
return;
if (!res.userData)
res.userData = {};
const info = new LODInformation(url, key, level, index, density);
res.userData.LODS = info;
}
static getAssignedLODInformation(res) {
return res?.userData?.LODS || null;
}
// private static readonly _copiedTextures: WeakMap<Texture, Texture> = new Map();
static copySettings(source, target) {
if (!target) {
return source;
}
// const existingCopy = source["LODS:COPY"];
// don't copy again if the texture was processed before
// we clone the source if it's animated
// const existingClone = this._copiedTextures.get(source);
// if (existingClone) {
// return existingClone;
// }
// We need to clone e.g. when the same texture is used multiple times (but with e.g. different wrap settings)
// This is relatively cheap since it only stores settings
// This should only happen once ever for every texture
// const original = target;
{
if (debug)
console.warn("Copy texture settings\n", source.uuid, "\n", target.uuid);
target = target.clone();
}
// else {
// source = existingCopy;
// }
// this._copiedTextures.set(original, target);
// we re-use the offset and repeat settings because it might be animated
target.offset = source.offset;
target.repeat = source.repeat;
target.colorSpace = source.colorSpace;
target.magFilter = source.magFilter;
target.minFilter = source.minFilter;
target.wrapS = source.wrapS;
target.wrapT = source.wrapT;
target.flipY = source.flipY;
target.anisotropy = source.anisotropy;
if (!target.mipmaps)
target.generateMipmaps = source.generateMipmaps;
// if (!target.userData) target.userData = {};
// target["LODS:COPY"] = source;
// related: NE-4937
return target;
}
}
// declare type GetLODInformation = () => LODInformation | null;
class LODInformation {
url;
/** the key to lookup the LOD information */
key;
level;
/** For multi objects (e.g. a group of meshes) this is the index of the object */
index;
/** the mesh density */
density;
constructor(url, key, level, index, density) {
this.url = url;
this.key = key;
this.level = level;
if (index != undefined)
this.index = index;
if (density != undefined)
this.density = density;
}
}
;