| @@ -107,7 +107,7 @@ | |||
| "rimraf": "^5.0.1", | |||
| "rollup-plugin-glsl": "^1.3.0", | |||
| "rollup-plugin-license": "^3.0.1", | |||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.157.1004/package.tgz", | |||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.157.1007/package.tgz", | |||
| "tslib": "^2.5.0", | |||
| "typedoc": "^0.27.5", | |||
| "typescript": "5.7.2", | |||
| @@ -118,7 +118,7 @@ | |||
| "vitepress-plugin-nprogress": "^0.0.4" | |||
| }, | |||
| "dependencies": { | |||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.157.1004/package.tgz", | |||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.157.1005/package.tgz", | |||
| "@types/webxr": "^0.5.1", | |||
| "@types/wicg-file-system-access": "^2020.9.5", | |||
| "stats.js": "^0.17.0", | |||
| @@ -127,7 +127,7 @@ | |||
| "popmotion": "^11.0.5" | |||
| }, | |||
| "peerDependencies": { | |||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.157.1004/package.tgz" | |||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.157.1007/package.tgz" | |||
| }, | |||
| "peerDependenciesMeta": { | |||
| "three": { | |||
| @@ -138,10 +138,10 @@ | |||
| "dependencies": { | |||
| "uiconfig.js": "^0.1.3", | |||
| "ts-browser-helpers": "^0.16.2", | |||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.157.1004/package.tgz", | |||
| "three-f": "https://github.com/repalash/three.js-modded/archive/refs/tags/v0.157.1004.tar.gz", | |||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.157.1004/package.tgz", | |||
| "@types/three-f": "https://github.com/repalash/three-ts-types/archive/refs/tags/v0.157.1004.tar.gz", | |||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.157.1007/package.tgz", | |||
| "three-f": "https://github.com/repalash/three.js-modded/archive/refs/tags/v0.157.1007.tar.gz", | |||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.157.1005/package.tgz", | |||
| "@types/three-f": "https://github.com/repalash/three-ts-types/archive/refs/tags/v0.157.1005.tar.gz", | |||
| "@types/three-pkg": "https://gitpkg.now.sh/repalash/three-ts-types/types/three?modded_three" | |||
| }, | |||
| "local_dependencies": { | |||
| @@ -68,6 +68,8 @@ export class AssetImporter extends EventDispatcher<IAssetImporterEventMap> imple | |||
| private _fileDatabase: Map<string, IFile> = new Map<string, IFile>() | |||
| private _cachedAssets: IAsset[] = [] | |||
| static WHITE_IMAGE_DATA = new ImageData(new Uint8ClampedArray([255, 255, 255, 255]), 1, 1) | |||
| readonly importers: IImporter[] = [ | |||
| // new Importer(VideoTextureLoader, ['mp4', 'ogg', 'mov', 'data:video'], false), | |||
| new Importer(SimpleJSONLoader, ['json', 'vjson'], ['application/json'], false), | |||
| @@ -39,9 +39,17 @@ import { | |||
| } from '../core' | |||
| import {Importer} from './Importer' | |||
| import {MaterialManager} from './MaterialManager' | |||
| import {DRACOLoader2, GLTFLoader2, JSONMaterialLoader, MTLLoader2, OBJLoader2, ZipLoader} from './import' | |||
| import { | |||
| DRACOLoader2, | |||
| FBXLoader2, | |||
| GLTFLoader2, | |||
| JSONMaterialLoader, | |||
| MTLLoader2, | |||
| OBJLoader2, | |||
| SVGTextureLoader, | |||
| ZipLoader, | |||
| } from './import' | |||
| import {RGBELoader} from 'three/examples/jsm/loaders/RGBELoader.js' | |||
| import {FBXLoader} from 'three/examples/jsm/loaders/FBXLoader.js' | |||
| import {EXRLoader} from 'three/examples/jsm/loaders/EXRLoader.js' | |||
| import {Class, ValOrArr} from 'ts-browser-helpers' | |||
| import {ILoader} from './IImporter' | |||
| @@ -279,9 +287,12 @@ export class AssetManager extends EventDispatcher<AssetManagerEventMap> { | |||
| const viewer = this.viewer | |||
| if (!viewer) return | |||
| // todo fix - loading manager getHandler matches backwards? | |||
| const importers: Importer[] = [ | |||
| new Importer(TextureLoader, ['webp', 'png', 'jpeg', 'jpg', 'svg', 'ico', 'data:image', 'avif'], [ | |||
| 'image/webp', 'image/png', 'image/jpeg', 'image/svg+xml', 'image/gif', 'image/bmp', 'image/tiff', 'image/x-icon', 'image/avif', | |||
| new Importer(SVGTextureLoader, ['svg', 'data:image/svg'], ['image/svg+xml'], false), // todo: use ImageBitmapLoader if supported (better performance) | |||
| new Importer(TextureLoader, ['webp', 'png', 'jpeg', 'jpg', 'ico', 'data:image', 'avif'], [ | |||
| 'image/webp', 'image/png', 'image/jpeg', 'image/gif', 'image/bmp', 'image/tiff', 'image/x-icon', 'image/avif', | |||
| ], false), // todo: use ImageBitmapLoader if supported (better performance) | |||
| new Importer<JSONMaterialLoader>(JSONMaterialLoader, | |||
| @@ -305,7 +316,7 @@ export class AssetManager extends EventDispatcher<AssetManagerEventMap> { | |||
| } | |||
| }, ['exr'], ['image/x-exr'], false), | |||
| new Importer(FBXLoader, ['fbx'], ['model/fbx'], true), | |||
| new Importer(FBXLoader2, ['fbx'], ['model/fbx'], true), | |||
| new Importer(ZipLoader, ['zip', 'glbz', 'gltfz'], ['application/zip', 'model/gltf+zip', 'model/zip'], true), // gltfz and glbz are invented zip files with gltf/glb inside along with resources | |||
| new Importer(OBJLoader2 as any as Class<ILoader>, ['obj'], ['model/obj'], true), | |||
| @@ -158,8 +158,10 @@ export class GLTFExporter2 extends GLTFExporter implements IExportParser { | |||
| maxTextureSize: options.maxTextureSize ?? Infinity, | |||
| animations: options.animations ?? [], | |||
| includeCustomExtensions: options.includeCustomExtensions ?? true, | |||
| forceIndices: options.forceIndices ?? false, | |||
| forceIndices: options.forceIndices ?? false, // todo implement | |||
| exporterOptions: options, | |||
| ignoreInvalidMorphTargetTracks: options.ignoreInvalidMorphTargetTracks, | |||
| ignoreEmptyTextures: options.ignoreEmptyTextures, | |||
| } | |||
| if (options.exportExt === 'glb') { | |||
| gltfOptions.binary = true | |||
| @@ -228,6 +228,7 @@ export class GLTFWriter2 extends GLTFExporter.Utils.GLTFWriter { | |||
| } | |||
| if (textureDef.source < 0) { | |||
| console.error('textureDef.source cannot be saved', textureDef, map) | |||
| delete textureDef.source // gltf spec allows undefined, not -1 | |||
| } | |||
| return processed | |||
| @@ -88,7 +88,7 @@ class GLTFMaterialsAlphaMapExtensionExport { | |||
| const extensionDef: any = {} | |||
| if (material.alphaMap) { | |||
| if (material.alphaMap && writer.checkEmptyMap(material.alphaMap)) { | |||
| const alphaMapDef = {index: writer.processTexture(material.alphaMap)} | |||
| writer.applyTextureTransform(alphaMapDef, material.alphaMap) | |||
| @@ -95,7 +95,7 @@ class GLTFMaterialsBumpMapExtensionExport { | |||
| extensionDef.bumpScale = material.bumpScale | |||
| if (material.bumpMap) { | |||
| if (material.bumpMap && writer.checkEmptyMap(material.bumpMap)) { | |||
| const bumpMapDef = {index: writer.processTexture(material.bumpMap)} | |||
| writer.applyTextureTransform(bumpMapDef, material.bumpMap) | |||
| @@ -90,7 +90,7 @@ class GLTFMaterialsDisplacementMapExtensionExport { | |||
| extensionDef.displacementScale = material.displacementScale | |||
| extensionDef.displacementBias = material.displacementBias | |||
| if (material.displacementMap) { | |||
| if (material.displacementMap && writer.checkEmptyMap(material.displacementMap)) { | |||
| const displacementMapDef = {index: writer.processTexture(material.displacementMap)} | |||
| writer.applyTextureTransform(displacementMapDef, material.displacementMap) | |||
| @@ -95,7 +95,7 @@ class GLTFMaterialsLightMapExtensionExport { | |||
| extensionDef.lightMapIntensity = material.lightMapIntensity | |||
| if (material.lightMap) { | |||
| if (material.lightMap && writer.checkEmptyMap(material.lightMap)) { | |||
| const lightMapDef = {index: writer.processTexture(material.lightMap)} | |||
| writer.applyTextureTransform(lightMapDef, material.lightMap) | |||
| @@ -0,0 +1,23 @@ | |||
| import {FBXLoader} from 'three/examples/jsm/loaders/FBXLoader.js' | |||
| import {Group, Texture} from 'three' | |||
| import {AssetImporter} from '../AssetImporter' | |||
| /** | |||
| * Extended FBXLoader that sets the default image from AssetImporter (for invalid/missing textures) | |||
| */ | |||
| export class FBXLoader2 extends FBXLoader { | |||
| async loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise<Group> { | |||
| const val = Texture.DEFAULT_IMAGE | |||
| // this will be used when doing new Texture(). Which is done for not found images or when some error happens in loading. See FBXLoader. | |||
| // todo save the path of invalid textures, check if they can be found in the loaded libs, and ask the user in UI to remap it to something else manually | |||
| if (!Texture.DEFAULT_IMAGE) Texture.DEFAULT_IMAGE = AssetImporter.WHITE_IMAGE_DATA | |||
| const res = await super.loadAsync(url, onProgress) | |||
| Texture.DEFAULT_IMAGE = val | |||
| return res | |||
| } | |||
| } | |||
| @@ -1,6 +1,6 @@ | |||
| import type {GLTF, GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader.js' | |||
| import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader.js' | |||
| import {LoadingManager, Object3D, OrthographicCamera} from 'three' | |||
| import {LoadingManager, Object3D, OrthographicCamera, Texture} from 'three' | |||
| import {AnyOptions, safeSetProperty} from 'ts-browser-helpers' | |||
| import {ThreeViewer} from '../../viewer' | |||
| import {generateUUID} from '../../three' | |||
| @@ -27,6 +27,10 @@ import { | |||
| UnlitLineMaterial, | |||
| UnlitMaterial, | |||
| } from '../../core' | |||
| import {AssetImporter} from '../AssetImporter' | |||
| // todo move somewhere | |||
| const supportedEmbeddedFiles = ['hdr', 'exr', 'webp', 'avif', 'ktx', 'hdrpng', 'svg', 'cube'] // ktx2, drc handled separately | |||
| export class GLTFLoader2 extends GLTFLoader implements ILoader<GLTF, Object3D|undefined> { | |||
| isGLTFLoader2 = true | |||
| @@ -69,7 +73,18 @@ export class GLTFLoader2 extends GLTFLoader implements ILoader<GLTF, Object3D|un | |||
| parse(data: ArrayBuffer | string, path: string, onLoad: (gltf: GLTF) => void, onError?: (event: ErrorEvent) => void, url?: string) { | |||
| this.preparse.call(this, data, url || path) | |||
| .then((res: ArrayBuffer | string) => res ? super.parse(res, path, onLoad, onError) : onError && onError(new ErrorEvent('no data'))) | |||
| .then((res: ArrayBuffer|string) => { | |||
| const val = Texture.DEFAULT_IMAGE | |||
| // this will be used when doing new Texture(). Which is done for not found images or when some error happens in loading. See FBXLoader. | |||
| // todo save the path of invalid textures, check if they can be found in the loaded libs, and ask the user in UI to remap it to something else manually | |||
| if (!Texture.DEFAULT_IMAGE) Texture.DEFAULT_IMAGE = AssetImporter.WHITE_IMAGE_DATA | |||
| return res ? super.parse(res, path, (ret)=>{ | |||
| Texture.DEFAULT_IMAGE = val | |||
| onLoad && onLoad(ret) | |||
| }, onError) : onError && onError(new ErrorEvent('no data')) | |||
| }) | |||
| .catch((e: any) => { | |||
| console.error(e) | |||
| if (onError) onError(e ?? new ErrorEvent('unknown error')) | |||
| @@ -146,17 +161,27 @@ export class GLTFLoader2 extends GLTFLoader implements ILoader<GLTF, Object3D|un | |||
| console.error('Add GLTFMeshOptPlugin(and initialize it) to viewer to enable EXT_meshopt_compression decode') | |||
| } | |||
| } | |||
| const needsBasisU = parser.json?.extensionsUsed?.includes?.('KHR_texture_basisu') | |||
| if (needsBasisU) { | |||
| const ktx2 = viewer.assetManager.importer.registerFile(tempPathKtx2) | |||
| if (ktx2) { | |||
| this.setKTX2Loader(ktx2 as any) // todo: check class? | |||
| parser.options.ktx2Loader = ktx2 as any | |||
| } | |||
| // create ktx2 loader so it can be used with getHandler, we need to do this even when extension is not used since we dont know | |||
| const ktx2 = viewer.assetManager.importer.registerFile(tempPathKtx2) | |||
| // const needsBasisU = parser.json?.extensionsUsed?.includes?.('KHR_texture_basisu') | |||
| // if (needsBasisU) { | |||
| // const ktx2 = viewer.assetManager.importer.registerFile(tempPathKtx2) | |||
| if (ktx2) { | |||
| this.setKTX2Loader(ktx2 as any) // todo: check class? | |||
| parser.options.ktx2Loader = ktx2 as any | |||
| } | |||
| // } | |||
| // registering temp file creates and makes a loader available to the loading manager of that type | |||
| const tempFiles = supportedEmbeddedFiles.map(f=>generateUUID() + '.' + f) | |||
| tempFiles.forEach(f=>viewer.assetManager.importer.registerFile(f)) | |||
| return {name: 'GLTF2_HELPER_PLUGIN', afterRoot: async(result: GLTF) => { | |||
| if (needsDrc) viewer.assetManager.importer.unregisterFile(tempPathDrc) | |||
| if (needsBasisU) viewer.assetManager.importer.unregisterFile(tempPathKtx2) | |||
| if (ktx2) viewer.assetManager.importer.unregisterFile(tempPathKtx2) | |||
| tempFiles.forEach(f=>viewer.assetManager.importer.unregisterFile(f)) | |||
| await GLTFViewerConfigExtension.ImportViewerConfig(parser, viewer, result.scenes || [result.scene]) | |||
| }} | |||
| } | |||
| @@ -40,7 +40,7 @@ export class Rhino3dmLoader2 extends Rhino3dmLoader { | |||
| }) | |||
| return super._createMaterial(material) | |||
| } | |||
| private _compareMaterials!: (material: Material) => Material | |||
| private declare _compareMaterials: (material: Material) => Material | |||
| async loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise<Object3D> { | |||
| const ret = await super.loadAsync(url, onProgress) | |||
| @@ -0,0 +1,69 @@ | |||
| import {CanvasTexture, ImageLoader, Loader, LoadingManager, Texture} from 'three' | |||
| import {getUrlQueryParam} from 'ts-browser-helpers' | |||
| /** | |||
| * Same as TextureLoader but loads SVG images, fixes issues with windows not loading svg files without a defined size. | |||
| * See - https://github.com/mrdoob/three.js/issues/30899 | |||
| * | |||
| * todo - create example for test, see sample code in gh issue. | |||
| */ | |||
| class SVGTextureLoader extends Loader { | |||
| constructor(manager: LoadingManager) { | |||
| super(manager) | |||
| } | |||
| static USE_CANVAS_TEXTURE = getUrlQueryParam('svg-load-disable-canvas') !== 'true' | |||
| load(url: string, onLoad: (texture: Texture) => void, onProgress?: (event: ProgressEvent) => void, onError?: (err: unknown) => void): Texture { | |||
| const canvas = SVGTextureLoader.USE_CANVAS_TEXTURE ? document.createElement('canvas') : undefined | |||
| const texture = SVGTextureLoader.USE_CANVAS_TEXTURE ? new CanvasTexture(canvas!) : new Texture() | |||
| const loader = new ImageLoader(this.manager) | |||
| loader.setCrossOrigin(this.crossOrigin) | |||
| loader.setPath(this.path) | |||
| loader.load(url, function(image) { | |||
| if (canvas) { | |||
| SVGTextureLoader.CopyImageToCanvas(canvas, image) | |||
| } else { | |||
| texture.image = image | |||
| } | |||
| texture.needsUpdate = true | |||
| if (onLoad !== undefined) { | |||
| onLoad(texture) | |||
| } | |||
| }, onProgress, onError) | |||
| return texture | |||
| } | |||
| static CopyImageToCanvas(canvas: HTMLCanvasElement, image: HTMLImageElement) { | |||
| // size can be scaled here, this is based on the viewBox aspect ratio and minimum size of 150hx300w | |||
| canvas.width = image.naturalWidth || image.width || 512 | |||
| canvas.height = image.naturalHeight || image.height || 512 | |||
| const ctx = canvas.getContext('2d') | |||
| if (ctx) { | |||
| ctx.clearRect(0, 0, canvas.width, canvas.height) | |||
| ctx.drawImage(image, 0, 0, canvas.width, canvas.height) | |||
| } else { | |||
| console.error('SVGTextureLoader: Failed to get canvas context.') | |||
| } | |||
| } | |||
| } | |||
| export {SVGTextureLoader} | |||
| @@ -61,6 +61,8 @@ export class AssetExporterPlugin extends AViewerPluginSync { | |||
| embedUrlImages: false, | |||
| encrypt: false, | |||
| encryptKey: '', | |||
| ignoreInvalidMorphTargetTracks: true, | |||
| ignoreEmptyTextures: true, | |||
| } | |||
| async exportScene(options?: ExportAssetOptions): Promise<BlobExt | undefined> { | |||
| @@ -129,6 +131,16 @@ export class AssetExporterPlugin extends AViewerPluginSync { | |||
| // label: 'Convert to indexed', | |||
| // property: [this.exportOptions, 'convertMeshToIndexed'], | |||
| // }, | |||
| { | |||
| type: 'checkbox', | |||
| label: 'Ignore invalid animations', | |||
| property: [this.exportOptions, 'ignoreInvalidMorphTargetTracks'], | |||
| }, | |||
| { | |||
| type: 'checkbox', | |||
| label: 'Ignore invalid textures', | |||
| property: [this.exportOptions, 'ignoreInvalidTextures'], | |||
| }, | |||
| { | |||
| type: 'button', | |||
| label: 'Export GLB', | |||
| @@ -281,7 +281,7 @@ const glTFMaterialsCustomBumpMapExport = (w: GLTFWriter2)=> ({ | |||
| extensionDef.customBumpScale = material.userData._customBumpScale || 1.0 | |||
| if (material.userData._customBumpMap) { | |||
| if (w.checkEmptyMap(material.userData._customBumpMap)) { | |||
| const customBumpMapDef = {index: w.processTexture(material.userData._customBumpMap)} | |||
| w.applyTextureTransform(customBumpMapDef, material.userData._customBumpMap) | |||