[3d] better solution to support reading extra resource/texture (#4209)

This commit is contained in:
Terry Jia
2025-07-02 00:25:18 -04:00
committed by GitHub
parent f57f97cfcd
commit 35ff882ff2
8 changed files with 206 additions and 642 deletions

View File

@@ -206,7 +206,11 @@ const handleBackgroundImageUpdate = async (file: File | null) => {
return
}
backgroundImage.value = await Load3dUtils.uploadFile(file)
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
const subfolder = resourceFolder.trim() ? `3d/${resourceFolder.trim()}` : '3d'
backgroundImage.value = await Load3dUtils.uploadFile(file, subfolder)
node.properties['Background Image'] = backgroundImage.value
}
@@ -218,7 +222,14 @@ const handleUploadTexture = async (file: File) => {
}
try {
const texturePath = await Load3dUtils.uploadFile(file)
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
const subfolder = resourceFolder.trim()
? `3d/${resourceFolder.trim()}`
: '3d'
const texturePath = await Load3dUtils.uploadFile(file, subfolder)
await load3DSceneRef.value.load3d.applyTexture(texturePath)
node.properties['Texture'] = texturePath

View File

@@ -238,7 +238,11 @@ const handleBackgroundImageUpdate = async (file: File | null) => {
return
}
backgroundImage.value = await Load3dUtils.uploadFile(file)
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
const subfolder = resourceFolder.trim() ? `3d/${resourceFolder.trim()}` : '3d'
backgroundImage.value = await Load3dUtils.uploadFile(file, subfolder)
node.properties['Background Image'] = backgroundImage.value
}

View File

@@ -1,7 +1,4 @@
import type {
IComboWidget,
IStringWidget
} from '@comfyorg/litegraph/dist/types/widgets'
import type { IStringWidget } from '@comfyorg/litegraph/dist/types/widgets'
import { nextTick } from 'vue'
import Load3D from '@/components/load3d/Load3D.vue'
@@ -17,6 +14,80 @@ import { useExtensionService } from '@/services/extensionService'
import { useLoad3dService } from '@/services/load3dService'
import { useToastStore } from '@/stores/toastStore'
async function handleModelUpload(files: FileList, node: any) {
if (!files?.length) return
const modelWidget = node.widgets?.find(
(w: any) => w.name === 'model_file'
) as IStringWidget
node.properties['Texture'] = undefined
try {
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
const subfolder = resourceFolder.trim()
? `3d/${resourceFolder.trim()}`
: '3d'
const uploadPath = await Load3dUtils.uploadFile(files[0], subfolder)
if (!uploadPath) {
useToastStore().addAlert(t('toastMessages.fileUploadFailed'))
return
}
const modelUrl = api.apiURL(
Load3dUtils.getResourceURL(
...Load3dUtils.splitFilePath(uploadPath),
'input'
)
)
await useLoad3dService().getLoad3d(node)?.loadModel(modelUrl)
if (uploadPath && modelWidget) {
if (!modelWidget.options?.values?.includes(uploadPath)) {
modelWidget.options?.values?.push(uploadPath)
}
modelWidget.value = uploadPath
}
} catch (error) {
console.error('Model upload failed:', error)
useToastStore().addAlert(t('toastMessages.fileUploadFailed'))
}
}
async function handleResourcesUpload(files: FileList, node: any) {
if (!files?.length) return
try {
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
const subfolder = resourceFolder.trim()
? `3d/${resourceFolder.trim()}`
: '3d'
await Load3dUtils.uploadMultipleFiles(files, subfolder)
} catch (error) {
console.error('Extra resources upload failed:', error)
useToastStore().addAlert(t('toastMessages.extraResourcesUploadFailed'))
}
}
function createFileInput(
accept: string,
multiple: boolean = false
): HTMLInputElement {
const input = document.createElement('input')
input.type = 'file'
input.accept = accept
input.multiple = multiple
input.style.display = 'none'
return input
}
useExtensionService().registerExtension({
name: 'Comfy.Load3D',
settings: [
@@ -110,49 +181,34 @@ useExtensionService().registerExtension({
getCustomWidgets() {
return {
LOAD_3D(node) {
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.accept = '.gltf,.glb,.obj,.fbx,.stl'
fileInput.style.display = 'none'
const fileInput = createFileInput('.gltf,.glb,.obj,.fbx,.stl', false)
node.properties['Resource Folder'] = ''
fileInput.onchange = async () => {
if (fileInput.files?.length) {
const modelWidget = node.widgets?.find(
(w) => w.name === 'model_file'
) as IComboWidget & { options: { values: string[] } }
node.properties['Texture'] = undefined
const uploadPath = await Load3dUtils.uploadFile(
fileInput.files[0]
).catch((error) => {
console.error('File upload failed:', error)
useToastStore().addAlert(t('toastMessages.fileUploadFailed'))
})
const modelUrl = api.apiURL(
Load3dUtils.getResourceURL(
...Load3dUtils.splitFilePath(uploadPath),
'input'
)
)
await useLoad3dService().getLoad3d(node)?.loadModel(modelUrl)
if (uploadPath && modelWidget) {
if (!modelWidget.options?.values?.includes(uploadPath)) {
modelWidget.options?.values?.push(uploadPath)
}
modelWidget.value = uploadPath
}
}
await handleModelUpload(fileInput.files!, node)
}
node.addWidget('button', 'upload 3d model', 'upload3dmodel', () => {
fileInput.click()
})
const resourcesInput = createFileInput('*', true)
resourcesInput.onchange = async () => {
await handleResourcesUpload(resourcesInput.files!, node)
resourcesInput.value = ''
}
node.addWidget(
'button',
'upload extra resources',
'uploadExtraResources',
() => {
resourcesInput.click()
}
)
node.addWidget('button', 'clear', 'clear', () => {
useLoad3dService().getLoad3d(node)?.clearModel()
@@ -264,46 +320,34 @@ useExtensionService().registerExtension({
getCustomWidgets() {
return {
LOAD_3D_ANIMATION(node) {
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.accept = '.gltf,.glb,.fbx'
fileInput.style.display = 'none'
const fileInput = createFileInput('.gltf,.glb,.fbx', false)
node.properties['Resource Folder'] = ''
fileInput.onchange = async () => {
if (fileInput.files?.length) {
const modelWidget = node.widgets?.find(
(w) => w.name === 'model_file'
) as IStringWidget
const uploadPath = await Load3dUtils.uploadFile(
fileInput.files[0]
).catch((error) => {
console.error('File upload failed:', error)
useToastStore().addAlert(t('toastMessages.fileUploadFailed'))
})
const modelUrl = api.apiURL(
Load3dUtils.getResourceURL(
...Load3dUtils.splitFilePath(uploadPath),
'input'
)
)
await useLoad3dService().getLoad3d(node)?.loadModel(modelUrl)
if (uploadPath && modelWidget) {
if (!modelWidget.options?.values?.includes(uploadPath)) {
modelWidget.options?.values?.push(uploadPath)
}
modelWidget.value = uploadPath
}
}
await handleModelUpload(fileInput.files!, node)
}
node.addWidget('button', 'upload 3d model', 'upload3dmodel', () => {
fileInput.click()
})
const resourcesInput = createFileInput('*', true)
resourcesInput.onchange = async () => {
await handleResourcesUpload(resourcesInput.files!, node)
resourcesInput.value = ''
}
node.addWidget(
'button',
'upload extra resources',
'uploadExtraResources',
() => {
resourcesInput.click()
}
)
node.addWidget('button', 'clear', 'clear', () => {
useLoad3dService().getLoad3d(node)?.clearModel()

View File

@@ -128,6 +128,9 @@ class Load3DConfiguration {
if (!value) return
const filename = value as string
this.setResourceFolder(filename)
const modelUrl = api.apiURL(
Load3dUtils.getResourceURL(
...Load3dUtils.splitFilePath(filename),
@@ -173,6 +176,21 @@ class Load3DConfiguration {
}
}
}
private setResourceFolder(filename: string): void {
const pathParts = filename.split('/').filter((part) => part.trim())
if (pathParts.length <= 2) {
return
}
const subfolderParts = pathParts.slice(1, -1)
const subfolder = subfolderParts.join('/')
if (subfolder) {
this.load3d.node.properties['Resource Folder'] = subfolder
}
}
}
export default Load3DConfiguration

View File

@@ -118,11 +118,7 @@ class Load3d {
options
)
this.loaderManager = new LoaderManager(
this.modelManager,
this.eventManager,
options
)
this.loaderManager = new LoaderManager(this.modelManager, this.eventManager)
this.recordingManager = new RecordingManager(
this.sceneManager.scene,

View File

@@ -34,13 +34,14 @@ class Load3dUtils {
return await resp.json()
}
static async uploadFile(file: File) {
static async uploadFile(file: File, subfolder: string) {
let uploadPath
try {
const body = new FormData()
body.append('image', file)
body.append('subfolder', '3d')
body.append('subfolder', subfolder)
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
@@ -96,6 +97,14 @@ class Load3dUtils {
return `/view?${params}`
}
static async uploadMultipleFiles(files: FileList, subfolder: string = '3d') {
const uploadPromises = Array.from(files).map((file) =>
this.uploadFile(file, subfolder)
)
await Promise.all(uploadPromises)
}
}
export default Load3dUtils

View File

@@ -1,16 +1,15 @@
import * as THREE from 'three'
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { OverrideMTLLoader } from '@/extensions/core/load3d/threejsOverride/OverrideMTLLoader'
import { t } from '@/i18n'
import { useToastStore } from '@/stores/toastStore'
import {
EventManagerInterface,
Load3DOptions,
LoaderManagerInterface,
ModelManagerInterface
} from './interfaces'
@@ -18,7 +17,7 @@ import {
export class LoaderManager implements LoaderManagerInterface {
gltfLoader: GLTFLoader
objLoader: OBJLoader
mtlLoader: OverrideMTLLoader
mtlLoader: MTLLoader
fbxLoader: FBXLoader
stlLoader: STLLoader
@@ -27,21 +26,14 @@ export class LoaderManager implements LoaderManagerInterface {
constructor(
modelManager: ModelManagerInterface,
eventManager: EventManagerInterface,
options: Load3DOptions
eventManager: EventManagerInterface
) {
let loadRootFolder = 'input'
if (options && options.inputSpec?.isPreview) {
loadRootFolder = 'output'
}
this.modelManager = modelManager
this.eventManager = eventManager
this.gltfLoader = new GLTFLoader()
this.objLoader = new OBJLoader()
this.mtlLoader = new OverrideMTLLoader(loadRootFolder)
this.mtlLoader = new MTLLoader()
this.fbxLoader = new FBXLoader()
this.stlLoader = new STLLoader()
}
@@ -100,9 +92,31 @@ export class LoaderManager implements LoaderManagerInterface {
): Promise<THREE.Object3D | null> {
let model: THREE.Object3D | null = null
const params = new URLSearchParams(url.split('?')[1])
const filename = params.get('filename')
if (!filename) {
console.error('Missing filename in URL:', url)
return null
}
const loadRootFolder = params.get('type') === 'output' ? 'output' : 'input'
const subfolder = params.get('subfolder') ?? ''
const path =
'api/view?type=' +
loadRootFolder +
'&subfolder=' +
encodeURIComponent(subfolder) +
'&filename='
switch (fileExtension) {
case 'stl':
const geometry = await this.stlLoader.loadAsync(url)
this.stlLoader.setPath(path)
const geometry = await this.stlLoader.loadAsync(filename)
this.modelManager.setOriginalModel(geometry)
geometry.computeVertexNormals()
@@ -117,7 +131,10 @@ export class LoaderManager implements LoaderManagerInterface {
break
case 'fbx':
const fbxModel = await this.fbxLoader.loadAsync(url)
this.fbxLoader.setPath(path)
const fbxModel = await this.fbxLoader.loadAsync(filename)
this.modelManager.setOriginalModel(fbxModel)
model = fbxModel
@@ -130,18 +147,12 @@ export class LoaderManager implements LoaderManagerInterface {
case 'obj':
if (this.modelManager.materialMode === 'original') {
const mtlUrl = url.replace(/(filename=.*?)\.obj/, '$1.mtl')
const subfolderMatch = url.match(/[?&]subfolder=([^&]*)/)
const subfolder = subfolderMatch
? decodeURIComponent(subfolderMatch[1])
: '3d'
this.mtlLoader.setSubfolder(subfolder)
try {
const materials = await this.mtlLoader.loadAsync(mtlUrl)
this.mtlLoader.setPath(path)
const mtlFileName = filename.replace(/\.obj$/, '.mtl')
const materials = await this.mtlLoader.loadAsync(mtlFileName)
materials.preload()
this.objLoader.setMaterials(materials)
} catch (e) {
@@ -151,7 +162,8 @@ export class LoaderManager implements LoaderManagerInterface {
}
}
model = await this.objLoader.loadAsync(url)
this.objLoader.setPath(path)
model = await this.objLoader.loadAsync(filename)
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
this.modelManager.originalMaterials.set(child, child.material)
@@ -161,7 +173,10 @@ export class LoaderManager implements LoaderManagerInterface {
case 'gltf':
case 'glb':
const gltf = await this.gltfLoader.loadAsync(url)
this.gltfLoader.setPath(path)
const gltf = await this.gltfLoader.loadAsync(filename)
this.modelManager.setOriginalModel(gltf)
model = gltf.scene

View File

@@ -1,533 +0,0 @@
import {
Color,
ColorManagement,
DefaultLoadingManager,
FileLoader,
FrontSide,
Loader,
LoaderUtils,
MeshPhongMaterial,
RepeatWrapping,
SRGBColorSpace,
TextureLoader,
Vector2
} from 'three'
/**
* A loader for the MTL format.
*
* The Material Template Library format (MTL) or .MTL File Format is a companion file format
* to OBJ that describes surface shading (material) properties of objects within one or more
* OBJ files.
*
* ```js
* const loader = new MTLLoader();
* const materials = await loader.loadAsync( 'models/obj/male02/male02.mtl' );
*
* const objLoader = new OBJLoader();
* objLoader.setMaterials( materials );
* ```
*
* @augments Loader
* @three_import import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
*/
class OverrideMTLLoader extends Loader {
constructor(loadRootFolder, manager) {
super(manager)
this.loadRootFolder = loadRootFolder
}
setSubfolder(subfolder) {
this.subfolder = subfolder
}
/**
* Starts loading from the given URL and passes the loaded MTL asset
* to the `onLoad()` callback.
*
* @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
* @param {function(MaterialCreator)} onLoad - Executed when the loading process has been finished.
* @param {onProgressCallback} onProgress - Executed while the loading is in progress.
* @param {onErrorCallback} onError - Executed when errors occur.
*/
load(url, onLoad, onProgress, onError) {
const scope = this
const path = this.path === '' ? LoaderUtils.extractUrlBase(url) : this.path
const loader = new FileLoader(this.manager)
loader.setPath(this.path)
loader.setRequestHeader(this.requestHeader)
loader.setWithCredentials(this.withCredentials)
loader.load(
url,
function (text) {
try {
onLoad(scope.parse(text, path))
} catch (e) {
if (onError) {
onError(e)
} else {
console.error(e)
}
scope.manager.itemError(url)
}
},
onProgress,
onError
)
}
/**
* Sets the material options.
*
* @param {MTLLoader~MaterialOptions} value - The material options.
* @return {MTLLoader} A reference to this loader.
*/
setMaterialOptions(value) {
this.materialOptions = value
return this
}
/**
* Parses the given MTL data and returns the resulting material creator.
*
* @param {string} text - The raw MTL data as a string.
* @param {string} path - The URL base path.
* @return {MaterialCreator} The material creator.
*/
parse(text, path) {
const lines = text.split('\n')
let info = {}
const delimiter_pattern = /\s+/
const materialsInfo = {}
for (let i = 0; i < lines.length; i++) {
let line = lines[i]
line = line.trim()
if (line.length === 0 || line.charAt(0) === '#') {
// Blank line or comment ignore
continue
}
const pos = line.indexOf(' ')
let key = pos >= 0 ? line.substring(0, pos) : line
key = key.toLowerCase()
let value = pos >= 0 ? line.substring(pos + 1) : ''
value = value.trim()
if (key === 'newmtl') {
// New material
info = { name: value }
materialsInfo[value] = info
} else {
if (key === 'ka' || key === 'kd' || key === 'ks' || key === 'ke') {
const ss = value.split(delimiter_pattern, 3)
info[key] = [parseFloat(ss[0]), parseFloat(ss[1]), parseFloat(ss[2])]
} else {
info[key] = value
}
}
}
const materialCreator = new OverrideMaterialCreator(
this.resourcePath || path,
this.materialOptions,
this.loadRootFolder,
this.subfolder
)
materialCreator.setCrossOrigin(this.crossOrigin)
materialCreator.setManager(this.manager)
materialCreator.setMaterials(materialsInfo)
return materialCreator
}
}
/**
* Material options of `MTLLoader`.
*
* @typedef {Object} MTLLoader~MaterialOptions
* @property {(FrontSide|BackSide|DoubleSide)} [side=FrontSide] - Which side to apply the material.
* @property {(RepeatWrapping|ClampToEdgeWrapping|MirroredRepeatWrapping)} [wrap=RepeatWrapping] - What type of wrapping to apply for textures.
* @property {boolean} [normalizeRGB=false] - Whether RGB colors should be normalized to `0-1` from `0-255`.
* @property {boolean} [ignoreZeroRGBs=false] - Ignore values of RGBs (Ka,Kd,Ks) that are all 0's.
*/
class OverrideMaterialCreator {
constructor(baseUrl = '', options = {}, loadRootFolder, subfolder) {
this.baseUrl = baseUrl
this.options = options
this.materialsInfo = {}
this.materials = {}
this.materialsArray = []
this.nameLookup = {}
this.loadRootFolder = loadRootFolder
this.subfolder = subfolder
this.crossOrigin = 'anonymous'
this.side = this.options.side !== undefined ? this.options.side : FrontSide
this.wrap =
this.options.wrap !== undefined ? this.options.wrap : RepeatWrapping
}
setCrossOrigin(value) {
this.crossOrigin = value
return this
}
setManager(value) {
this.manager = value
}
setMaterials(materialsInfo) {
this.materialsInfo = this.convert(materialsInfo)
this.materials = {}
this.materialsArray = []
this.nameLookup = {}
}
convert(materialsInfo) {
if (!this.options) return materialsInfo
const converted = {}
for (const mn in materialsInfo) {
// Convert materials info into normalized form based on options
const mat = materialsInfo[mn]
const covmat = {}
converted[mn] = covmat
for (const prop in mat) {
let save = true
let value = mat[prop]
const lprop = prop.toLowerCase()
switch (lprop) {
case 'kd':
case 'ka':
case 'ks':
// Diffuse color (color under white light) using RGB values
if (this.options && this.options.normalizeRGB) {
value = [value[0] / 255, value[1] / 255, value[2] / 255]
}
if (this.options && this.options.ignoreZeroRGBs) {
if (value[0] === 0 && value[1] === 0 && value[2] === 0) {
// ignore
save = false
}
}
break
default:
break
}
if (save) {
covmat[lprop] = value
}
}
}
return converted
}
preload() {
for (const mn in this.materialsInfo) {
this.create(mn)
}
}
getIndex(materialName) {
return this.nameLookup[materialName]
}
getAsArray() {
let index = 0
for (const mn in this.materialsInfo) {
this.materialsArray[index] = this.create(mn)
this.nameLookup[mn] = index
index++
}
return this.materialsArray
}
create(materialName) {
if (this.materials[materialName] === undefined) {
this.createMaterial_(materialName)
}
return this.materials[materialName]
}
createMaterial_(materialName) {
// Create material
const scope = this
const mat = this.materialsInfo[materialName]
const params = {
name: materialName,
side: this.side
}
/**
* Override for ComfyUI api url
*/
function resolveURL(baseUrl, url, loadRootFolder, subfolder) {
if (typeof url !== 'string' || url === '') return ''
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1)
}
if (!baseUrl.endsWith('api')) {
baseUrl = '/api'
}
baseUrl =
baseUrl +
'/view?filename=' +
url +
'&type=' +
loadRootFolder +
'&subfolder=' +
subfolder
return baseUrl
}
function setMapForType(mapType, value) {
if (params[mapType]) return // Keep the first encountered texture
const texParams = scope.getTextureParams(value, params)
const map = scope.loadTexture(
resolveURL(
scope.baseUrl,
texParams.url,
scope.loadRootFolder,
scope.subfolder
)
)
map.repeat.copy(texParams.scale)
map.offset.copy(texParams.offset)
map.wrapS = scope.wrap
map.wrapT = scope.wrap
if (mapType === 'map' || mapType === 'emissiveMap') {
map.colorSpace = SRGBColorSpace
}
params[mapType] = map
}
for (const prop in mat) {
const value = mat[prop]
let n
if (value === '') continue
switch (prop.toLowerCase()) {
// Ns is material specular exponent
case 'kd':
// Diffuse color (color under white light) using RGB values
params.color = ColorManagement.toWorkingColorSpace(
new Color().fromArray(value),
SRGBColorSpace
)
break
case 'ks':
// Specular color (color when light is reflected from shiny surface) using RGB values
params.specular = ColorManagement.toWorkingColorSpace(
new Color().fromArray(value),
SRGBColorSpace
)
break
case 'ke':
// Emissive using RGB values
params.emissive = ColorManagement.toWorkingColorSpace(
new Color().fromArray(value),
SRGBColorSpace
)
break
case 'map_kd':
// Diffuse texture map
setMapForType('map', value)
break
case 'map_ks':
// Specular map
setMapForType('specularMap', value)
break
case 'map_ke':
// Emissive map
setMapForType('emissiveMap', value)
break
case 'norm':
setMapForType('normalMap', value)
break
case 'map_bump':
case 'bump':
// Bump texture map
setMapForType('bumpMap', value)
break
case 'disp':
// Displacement texture map
setMapForType('displacementMap', value)
break
case 'map_d':
// Alpha map
setMapForType('alphaMap', value)
params.transparent = true
break
case 'ns':
// The specular exponent (defines the focus of the specular highlight)
// A high exponent results in a tight, concentrated highlight. Ns values normally range from 0 to 1000.
params.shininess = parseFloat(value)
break
case 'd':
n = parseFloat(value)
if (n < 1) {
params.opacity = n
params.transparent = true
}
break
case 'tr':
n = parseFloat(value)
if (this.options && this.options.invertTrProperty) n = 1 - n
if (n > 0) {
params.opacity = 1 - n
params.transparent = true
}
break
default:
break
}
}
this.materials[materialName] = new MeshPhongMaterial(params)
return this.materials[materialName]
}
getTextureParams(value, matParams) {
const texParams = {
scale: new Vector2(1, 1),
offset: new Vector2(0, 0)
}
const items = value.split(/\s+/)
let pos
pos = items.indexOf('-bm')
if (pos >= 0) {
matParams.bumpScale = parseFloat(items[pos + 1])
items.splice(pos, 2)
}
pos = items.indexOf('-mm')
if (pos >= 0) {
matParams.displacementBias = parseFloat(items[pos + 1])
matParams.displacementScale = parseFloat(items[pos + 2])
items.splice(pos, 3)
}
pos = items.indexOf('-s')
if (pos >= 0) {
texParams.scale.set(
parseFloat(items[pos + 1]),
parseFloat(items[pos + 2])
)
items.splice(pos, 4) // we expect 3 parameters here!
}
pos = items.indexOf('-o')
if (pos >= 0) {
texParams.offset.set(
parseFloat(items[pos + 1]),
parseFloat(items[pos + 2])
)
items.splice(pos, 4) // we expect 3 parameters here!
}
texParams.url = items.join(' ').trim()
return texParams
}
loadTexture(url, mapping, onLoad, onProgress, onError) {
const manager =
this.manager !== undefined ? this.manager : DefaultLoadingManager
let loader = manager.getHandler(url)
if (loader === null) {
loader = new TextureLoader(manager)
}
if (loader.setCrossOrigin) loader.setCrossOrigin(this.crossOrigin)
const texture = loader.load(url, onLoad, onProgress, onError)
if (mapping !== undefined) texture.mapping = mapping
return texture
}
}
export { OverrideMTLLoader }