UNPKG

@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
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; } } ;