mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-01 22:09:55 +00:00
[3d] better solution to support reading extra resource/texture (#4209)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user