I suspect this question may have been asked, but I’ll try.
I’m experiencing an issue when loading GLB models (exported from Sketchfab) directly into three.js viewer. Although the geometry appears correct, some models display as completely white—without any of their intended textures.
In three editor problem the same , look at the pic below
For a better understanding I will give a photo of the files, structure and the
Folder structure
In the source folder only .glb file
In textures pic below
The initial download code thanks to @donmccurdy is :
import { useEffect, useState, useMemo } from 'react'
import { LoaderUtils, Cache } from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { FBXLoader, TGALoader } from 'three/examples/jsm/Addons.js'
import { DRACOLoader } from 'three/examples/jsm/Addons.js'
import { KTX2Loader } from 'three/examples/jsm/Addons.js'
import { LoadingManager, REVISION } from 'three'
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js'
import { useLoader, useThree } from '@react-three/fiber'
interface LoaderProps {
url: string
rootPath: string
assetMap: Map<string, File>
fileType: string
rootFile: File | string
}
const useModelLoader = ({
url,
rootPath,
assetMap,
fileType,
rootFile,
}: LoaderProps): any => {
const { gl, scene } = useThree()
const [content, setContent] = useState<any>({
scene: null,
clips: null,
})
const MANAGER = useMemo(() => new LoadingManager(), [])
const THREE_PATH = `https://unpkg.com/three@0.${REVISION}.x`
const DRACO_LOADER = useMemo(
() =>
new DRACOLoader(MANAGER).setDecoderPath(
`${THREE_PATH}/examples/jsm/libs/draco/gltf/`
),
[MANAGER]
)
const KTX2_LOADER = useMemo(
() =>
new KTX2Loader(MANAGER).setTranscoderPath(
`${THREE_PATH}/examples/jsm/libs/basis/`
),
[MANAGER]
)
const traverseMaterials = (object: any, callback: (mat: any) => void) => {
object.traverse((node: any) => {
if (!node.geometry) return
const materials = Array.isArray(node.material)
? node.material
: [node.material]
materials.forEach(callback)
})
}
// Функция очистки модели
const cleanup = (model: any) => {
if (!model) return
scene.remove(model)
model.traverse((node: any) => {
if (node.geometry) {
node.geometry.dispose()
}
})
if (model.animations) {
model.animations.forEach((animation: any) => {
if (animation.clip) {
animation.clip.dispose()
}
})
}
traverseMaterials(model, (material: any) => {
if (material.dispose) {
material.dispose()
}
for (const key in material) {
if (
key !== 'envMap' &&
material[key] &&
material[key].isTexture &&
material[key].dispose
) {
material[key].dispose()
}
}
})
}
useEffect(() => {
if (!url) return
const blobURLs: string[] = []
if (content.scene) {
cleanup(content.scene)
setContent({ scene: null, clips: null })
}
// Настройка менеджера для обработки дополнительных ресурсов
MANAGER.setURLModifier((someUrl: string) => {
const baseURL = LoaderUtils.extractUrlBase(url)
const normalizedURL =
rootPath +
decodeURI(someUrl)
.replace(baseURL, '')
.replace(/^(\.?\/)/, '')
if (assetMap.has(normalizedURL)) {
const blob = assetMap.get(normalizedURL)
const blobURL = URL.createObjectURL(blob!)
blobURLs.push(blobURL)
return blobURL
}
return someUrl
})
let loader: GLTFLoader | FBXLoader | null = null
if (fileType === 'gltf/glb') {
loader = new GLTFLoader(MANAGER)
.setCrossOrigin('anonymous')
.setDRACOLoader(DRACO_LOADER)
.setKTX2Loader(KTX2_LOADER.detectSupport(gl))
.setMeshoptDecoder(MeshoptDecoder)
MANAGER.onLoad = () => {
console.log('All textures loaded successfully')
}
MANAGER.onError = url => {
console.error('Error loading texture:', url)
}
} else if (fileType === 'fbx') {
loader = new FBXLoader(MANAGER).setCrossOrigin('anonymous')
}
if (loader) {
loader.load(
url,
(object: any) => {
console.log('Loaded model:', object)
let loadedScene = null
let clips = null
if (fileType === 'gltf/glb') {
loadedScene = object.scene || object.scenes[0]
clips = object.animations || []
} else if (fileType === 'fbx') {
loadedScene = object
clips = object.animations || []
}
if (!loadedScene) {
console.error('No scene found in the model')
return
}
setContent({ scene: loadedScene, clips })
blobURLs.forEach(url => URL.revokeObjectURL(url))
},
undefined,
error => {
console.error('Error loading model:', error)
}
)
}
return () => {
blobURLs.forEach(url => URL.revokeObjectURL(url))
DRACO_LOADER.dispose()
KTX2_LOADER.dispose()
useLoader.clear(GLTFLoader, url)
if (content.scene) cleanup(content.scene)
Cache.clear()
}
}, [url, fileType, rootPath, assetMap, rootFile])
return content
}
export default useModelLoader
Here’s what I’ve observed:
- When I import a GLB file directly (by uploading the folder containing the model and its textures), the textures do not appear.
- However, if I open the same GLB file in Blender, enable viewport shading (which appears to properly interpret the model’s material settings), and then export it again, the model displays with its textures correctly in my viewer.
- Websites like 3dviewer.net and fetchcfd.com/3d-viewer can load these models and display textures correctly without any manual re-exporting.
This leads me to believe that the original GLB files might have non-standard or incomplete material definitions (perhaps due to non-PBR setups or vertex-color materials) that three.js isn’t handling out of the box. Blender, on the other hand, seems to “fix” or convert these materials into a standard PBR format during re-export.
My questions are:
- What might be causing the texture data in the original GLB files to not display correctly in three.js? Are there known issues with Sketchfab-exported GLBs regarding material settings or texture embedding?
- How are advanced viewers (like 3dviewer.net and fetchcfd.com) able to automatically convert or interpret these materials correctly? Do they apply a specific post-processing or material conversion step?
- Is there a recommended approach or workaround within three.js to automatically handle these discrepancies? For example, can I programmatically adjust the material properties after loading the GLB so that the textures display correctly without needing to re-export via Blender?
Any insights, best practices, or workarounds would be greatly appreciated!
Looking forward to your help.