Просмотр исходного кода

Add GLTFMeshOptDecodePlugin, AssetExporterPlugin and examples.

master
Palash Bansal 2 лет назад
Родитель
Сommit
0859c5fafb
Аккаунт пользователя с таким Email не найден

+ 36
- 0
examples/asset-exporter-plugin/index.html Просмотреть файл

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Asset Exporter Plugin</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>

<script type="importmap">
{
"imports": {
"threepipe": "./../../dist/index.mjs",
"@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs"
}
}

</script>
<style id="example-style">
html, body, #canvas-container, #mcanvas {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
}
</style>
<script type="module" src="../examples-utils/simple-code-preview.mjs"></script>
<script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script>
</head>
<body>
<div id="canvas-container">
<canvas id="mcanvas"></canvas>
</div>

</body>

+ 39
- 0
examples/asset-exporter-plugin/script.ts Просмотреть файл

@@ -0,0 +1,39 @@
import {
_testFinish,
AssetExporterPlugin,
IObject3D,
LoadingScreenPlugin,
SceneUiConfigPlugin,
ThreeViewer,
} from 'threepipe'
import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
plugins: [LoadingScreenPlugin, AssetExporterPlugin, SceneUiConfigPlugin],
})

const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true))

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr', {
setBackground: true,
})
const result = await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', {
autoCenter: true,
autoScale: true,
})

ui.setupPluginUi(AssetExporterPlugin, {expanded: true})
ui.setupPluginUi(SceneUiConfigPlugin)

const model = result?.getObjectByName('node_damagedHelmet_-6514')
const config = model?.uiConfig
if (config) ui.appendChild(config)


}

init().finally(_testFinish)

+ 2
- 0
examples/glb-export/script.ts Просмотреть файл

@@ -5,6 +5,8 @@ const viewer = new ThreeViewer({canvas: document.getElementById('mcanvas') as HT

async function init() {

// Note: see asset-exporter-plugin example as well

// load obj + mtl
await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')
const helmet = await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', {

+ 2
- 0
examples/gltf-load/script.ts Просмотреть файл

@@ -13,6 +13,8 @@ async function init() {
autoSetBackground: true,
},
},
// add optional plugins to load gltf with extensions like meshopt_compression, ktx2 etc
// plugins: [GLTFMeshOptDecodePlugin, KTX2LoadPlugin, GLTFKHRMaterialVariantsPlugin, ...],
})

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr', {

+ 35
- 0
examples/gltf-meshopt-compression/index.html Просмотреть файл

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GLTF MeshOpt Decode Plugin (EXT_meshopt_compression)</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>

<script type="importmap">
{
"imports": {
"threepipe": "./../../dist/index.mjs"
}
}

</script>
<style id="example-style">
html, body, #canvas-container, #mcanvas {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
}
</style>
<script type="module" src="../examples-utils/simple-code-preview.mjs"></script>
<script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script>
</head>
<body>
<div id="canvas-container">
<canvas id="mcanvas"></canvas>
</div>

</body>

+ 39
- 0
examples/gltf-meshopt-compression/script.ts Просмотреть файл

@@ -0,0 +1,39 @@
import {
_testFinish,
GLTFMeshOptDecodePlugin,
IObject3D,
KTX2LoadPlugin,
LoadingScreenPlugin,
ThreeViewer,
} from 'threepipe'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
dropzone: {
addOptions: {
disposeSceneObjects: true,
},
},
plugins: [LoadingScreenPlugin, GLTFMeshOptDecodePlugin, KTX2LoadPlugin],
})

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')

const options = {
autoCenter: true,
autoScale: true,
};
(await Promise.all([
viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/coffeemat.glb', options),
viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/facecap.glb', options),
])).forEach((result, i) => {
if (!result) return
result.position.x = i * 2 - 1
})

}

init().finally(_testFinish)

+ 4
- 4
src/assetmanager/import/GLTFLoader2.ts Просмотреть файл

@@ -139,11 +139,11 @@ export class GLTFLoader2 extends GLTFLoader implements ILoader<GLTF, Object3D|un
}
const needsMeshOpt = parser.json?.extensionsUsed?.includes?.('EXT_meshopt_compression')
if (needsMeshOpt) {
if ((window as any).MeshoptDecoder) { // added by the plugin or by the user
this.setMeshoptDecoder((window as any).MeshoptDecoder)
parser.options.meshoptDecoder = (window as any).MeshoptDecoder as any
if (window.MeshoptDecoder) { // added by the plugin or by the user
this.setMeshoptDecoder(window.MeshoptDecoder)
parser.options.meshoptDecoder = window.MeshoptDecoder
} else {
console.error('Add GLTFMeshOptPlugin to viewer to enable EXT_meshopt_compression decode')
console.error('Add GLTFMeshOptPlugin(and initialize it) to viewer to enable EXT_meshopt_compression decode')
}
}
const needsBasisU = parser.json?.extensionsUsed?.includes?.('KHR_texture_basisu')

+ 209
- 0
src/plugins/export/AssetExporterPlugin.ts Просмотреть файл

@@ -0,0 +1,209 @@
import {serialize} from 'ts-browser-helpers'
import {BlobExt, ExportFileOptions} from '../../assetmanager'
import {AViewerPluginSync, ThreeViewer} from '../../viewer'
import {UiObjectConfig} from 'uiconfig.js'
import {PickingPlugin} from '../interaction/PickingPlugin'

// export enum EncoderMethod {
// EDGEBREAKER = 1,
// SEQUENTIAL = 0
// }

export interface ExportAssetOptions extends ExportFileOptions {
convertMeshToIndexed?: boolean // convert mesh to indexed geometry
name?: string
compress?: boolean,
}

// todo deprecate the plugin and add functionality to AssetManager maybe
/**
* Asset Exporter Plugin
* Provides options and methods to export the scene, object GLB or Viewer Config.
* All the functionality is available in the viewer directly, this provides only a ui-config and maintains state of the options.
*/
export class AssetExporterPlugin extends AViewerPluginSync<''> {
public static readonly PluginType = 'AssetExporterPlugin'
enabled = true

constructor() {
super()
this.exportScene = this.exportScene.bind(this)
}

onAdded(viewer: ThreeViewer) {
super.onAdded(viewer)
// todo Convert all non-indexed geometries to indexed geometries, because DRACO compression requires indexed geometry.
// this.exporter.processors.add('model', {
// forAssetType: 'model',
// processAsync: async(obj: IObject3D, options) => {
// if (options.convertMeshToIndexed)
// obj?.traverse((o: IObject3D)=>{
// if (!o.geometry) return
// if (o.geometry.attributes.index) return
// o.geometry = toIndexedGeometry(o.geometry)
// })
// return obj
// },
// })
}

onRemove(viewer: ThreeViewer) {
return super.onRemove(viewer)
}

@serialize() exportOptions: ExportAssetOptions = {
name: 'scene',
viewerConfig: true,
encodeUint16Rgbe: false,
convertMeshToIndexed: false,
embedUrlImagePreviews: false,
embedUrlImages: false,
// compress: false,
// dracoOptions: {
// encodeSpeed: 5,
// method: EncoderMethod.EDGEBREAKER,
// quantizationVolume: 'mesh',
// quantizationBits: {
// ['POSITION']: 14,
// ['NORMAL']: 10,
// ['COLOR']: 8,
// ['TEX_COORD']: 12,
// ['GENERIC']: 12,
// },
// } as EncoderOptions,
encrypt: false,
encryptKey: '',
}

async exportScene(options?: ExportAssetOptions): Promise<BlobExt | undefined> {
return this._viewer?.assetManager.exporter?.exportObject(this._viewer?.scene.modelRoot, options || {...this.exportOptions})
}

async downloadSceneGlb() {
const blob = await this.exportScene(this.exportOptions)
if (blob) await this._viewer?.exportBlob(blob, this.exportOptions.name + '.' + blob.ext)
}

async exportSelected(options?: ExportAssetOptions, download = true) {
const selected = this._viewer?.getPlugin<PickingPlugin>('PickingPlugin')?.getSelectedObject() as any
if (!selected) {
alert('Nothing selected')
return
}
const blob = await this._viewer!.assetManager.exporter.exportObject(selected, options ?? this.exportOptions)
if (blob && download) await this._viewer?.exportBlob(blob, 'object.' + blob.ext)
return blob
}

uiConfig: UiObjectConfig = {
type: 'folder',
label: 'Asset Export',
children: [
{
type: 'input',
property: [this.exportOptions, 'name'],
},
{
type: 'folder',
label: 'GLB Export',
children: [
// compress ? {
// type: 'checkbox',
// label: 'DRACO Compress',
// property: [this.exportOptions, 'compress'],
// onChange: ()=>this.uiConfig.uiRefresh?.(true),
// } : {},
// compress && this.exportOptions.dracoOptions ? {
// type: 'folder',
// hidden: ()=>!this.exportOptions.compress,
// label: 'DRACO Options',
// children: [
// {
// type: 'slider',
// label: 'Encode Speed',
// bounds: [1, 10],
// property: [this.exportOptions.dracoOptions, 'encodeSpeed'],
// },
// {
// type: 'dropdown',
// label: 'Encoder Method',
// property: [this.exportOptions.dracoOptions, 'method'],
// children: Object.entries(EncoderMethod).map(([k, v]) => ({label: k, value: v})),
// },
// {
// type: 'dropdown',
// label: 'Quantization Volume',
// property: [this.exportOptions.dracoOptions, 'quantizationVolume'],
// children: ['mesh', 'scene', 'bbox'].map(v => ({label: v})),
// },
// {
// type: 'folder',
// label: 'Quantization Bits',
// children: Object.keys(this.exportOptions.dracoOptions.quantizationBits!).map(k => ({
// type: 'slider',
// label: k,
// bounds: [1, 16],
// stepSize: 1,
// property: [this.exportOptions.dracoOptions.quantizationBits, k],
// })),
// },
// ],
// } : {},
{
type: 'checkbox',
label: 'Viewer Config (All Settings)',
property: [this.exportOptions, 'viewerConfig'],
onChange: ()=>this.uiConfig.uiRefresh?.(true),
},
{
type: 'checkbox',
label: 'Embed Image Previews',
property: [this.exportOptions, 'embedUrlImagePreviews'],
},
{
type: 'checkbox',
label: 'Encrypt',
property: [this.exportOptions, 'encrypt'],
onChange: ()=>this.uiConfig.uiRefresh?.(true),
},
{
type: 'checkbox',
label: 'Encrypt Password',
hidden: ()=>!this.exportOptions.encrypt,
property: [this.exportOptions, 'encryptKey'],
},
{
type: 'checkbox',
label: 'Compress hdr env maps',
hidden: ()=>!this.exportOptions.viewerConfig,
property: [this.exportOptions, 'encodeUint16Rgbe'],
},
// { // todo
// type: 'checkbox',
// label: 'Convert to indexed',
// property: [this.exportOptions, 'convertMeshToIndexed'],
// },
{
type: 'button',
label: 'Export GLB',
property: [this, 'downloadSceneGlb'],
},
],
},
{
type: 'button',
label: 'Export Viewer Config',
value: async()=>{
const blob = new Blob([JSON.stringify(this._viewer?.exportConfig(false), null, 2)], {type: 'application/json'})
if (blob) await this._viewer?.exportBlob(blob, this.exportOptions.name + '.' + ThreeViewer.ConfigTypeSlug)
},
},
{
type: 'button',
label: 'Export Selected',
hidden: ()=>!this._viewer?.getPlugin<PickingPlugin>('PickingPlugin'),
value: async()=>this.exportSelected(this.exportOptions, true),
},
],
}
}

+ 70
- 0
src/plugins/import/GLTFMeshOptDecodePlugin.ts Просмотреть файл

@@ -0,0 +1,70 @@
import {IViewerPluginSync} from '../../viewer'
import {SimpleEventDispatcher} from 'ts-browser-helpers'

/**
* Loads the MeshOpt Decoder module from [meshoptimizer](https://github.com/zeux/meshoptimizer) library at runtime from a customisable cdn url.
* The loaded module is set in window.MeshoptDecoder and then used by {@link GLTFLoader2} to decode files using [EXT_meshopt_compression](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/EXT_meshopt_compression/README.md) extension
*
* The plugin name includes GLTF, but its not really GLTF specific, it can be used to decode any meshopt compressed files.
*/
export class GLTFMeshOptDecodePlugin extends SimpleEventDispatcher<''> implements IViewerPluginSync {
declare ['constructor']: typeof GLTFMeshOptDecodePlugin
public static readonly PluginType = 'GLTFMeshOptDecodePlugin'
enabled = true
toJSON: any = undefined

constructor(initialize = true, public readonly rootNode = document.head) {
super()
// todo: check if compatible?
if (initialize) this.initialize()
}

get initialized() {
return !!window.MeshoptDecoder
}

// static DECODER_URL = 'https://cdn.jsdelivr.net/gh/zeux/meshoptimizer@master/js/meshopt_decoder.module.js'
static DECODER_URL = 'https://unpkg.com/meshoptimizer@0.20.0/meshopt_decoder.module.js'

protected _script?: HTMLScriptElement

protected _initializing?: Promise<void> = undefined

async initialize() {
if (this.initialized) return
if (this._initializing) return await this._initializing
const s = document.createElement('script')
s.type = 'module'
const ev = Math.random().toString(36).substring(7)
s.innerHTML = `
import { MeshoptDecoder } from '${GLTFMeshOptDecodePlugin.DECODER_URL}';
window.MeshoptDecoder = MeshoptDecoder; // setting it before ready as GLTFLoader supports it.
MeshoptDecoder.ready.then(() => {
window.dispatchEvent(new CustomEvent('${ev}'))
});
`
this._initializing = new Promise<void>((res) => {
window.addEventListener(ev, ()=>res(), {once: true})
this.rootNode.appendChild(s)
this._script = s
})
return await this._initializing
}

dispose() {
if (this._script) {
this._script.remove()
delete window.MeshoptDecoder
}
this._script = undefined
}

onAdded(): void { return }
onRemove(): void { return }
}

declare global{
interface Window{
MeshoptDecoder?: any
}
}

+ 2
- 0
src/plugins/index.ts Просмотреть файл

@@ -43,8 +43,10 @@ export {PLYLoadPlugin} from './import/PLYLoadPlugin'
export {STLLoadPlugin} from './import/STLLoadPlugin'
export {KTXLoadPlugin} from './import/KTXLoadPlugin'
export {KTX2LoadPlugin} from './import/KTX2LoadPlugin'
export {GLTFMeshOptDecodePlugin} from './import/GLTFMeshOptDecodePlugin'

// export
export {AssetExporterPlugin} from './export/AssetExporterPlugin'
export {CanvasSnapshotPlugin, CanvasSnipperPlugin} from './export/CanvasSnapshotPlugin'
export {FileTransferPlugin} from './export/FileTransferPlugin'


+ 20
- 6
src/viewer/ThreeViewer.ts Просмотреть файл

@@ -54,8 +54,13 @@ import {
import {IViewerPlugin, IViewerPluginSync} from './IViewerPlugin'
import {uiConfig, UiObjectConfig, uiPanelContainer} from 'uiconfig.js'
import {IRenderTarget} from '../rendering'
import type {CanvasSnapshotPlugin, FileTransferPlugin} from '../plugins'
import {CameraViewPlugin, ProgressivePlugin} from '../plugins'
import {
AssetExporterPlugin,
CameraViewPlugin,
CanvasSnapshotPlugin,
FileTransferPlugin,
ProgressivePlugin,
} from '../plugins'
// noinspection ES6PreferShortImport
import {DropzonePlugin, DropzonePluginOptions} from '../plugins/interaction/DropzonePlugin'
// noinspection ES6PreferShortImport
@@ -515,7 +520,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
* @param options
*/
async export(obj?: IObject3D|IMaterial|ITexture|IRenderTarget|IViewerPlugin|(typeof this), options?: ExportFileOptions) {
if (!obj) obj = this._scene // this will export the glb with the scene and viewer config
if (!obj) obj = this._scene.modelRoot // this will export the glb with the scene and viewer config
if ((<typeof this>obj).type === this.type) return jsonToBlob((<typeof this>obj).exportConfig())
if ((<IViewerPlugin>obj).constructor?.PluginType) return jsonToBlob(this.exportPluginConfig(<IViewerPlugin>obj))
return await this.assetManager.exporter.exportObject(<IObject3D|IMaterial|ITexture|IRenderTarget>obj, options)
@@ -524,11 +529,20 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
/**
* Export the scene to a file (default: glb with viewer config) and return a blob
* @param options
* @param useExporterPlugin - uses the {@link AssetExporterPlugin} if available. This is useful to use the options configured by the user in the plugin.
*/
async exportScene(options?: ExportFileOptions): Promise<BlobExt | undefined> {
async exportScene(options?: ExportFileOptions, useExporterPlugin = true): Promise<BlobExt | undefined> {
const exporter = useExporterPlugin ? this.getPlugin<AssetExporterPlugin>('AssetExporterPlugin') : undefined
if (exporter) return exporter.exportScene(options)
return this.assetManager.exporter.exportObject(this._scene.modelRoot, options)
}

/**
* Returns a blob with the screenshot of the canvas.
* If {@link CanvasSnapshotPlugin} is added, it will be used, otherwise canvas.toBlob will be used directly.
* @param mimeType default image/jpeg
* @param quality between 0 and 100
*/
async getScreenshotBlob({mimeType = 'image/jpeg', quality = 90} = {}): Promise<Blob | null | undefined> {
const plugin = this.getPlugin<CanvasSnapshotPlugin>('CanvasSnapshotPlugin')
if (plugin) {
@@ -1077,7 +1091,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
/**
* Serialize all the viewer and plugin settings and versions.
* @param binary - Indicate that the output will be converted and saved as binary data. (default: true)
* @param pluginFilter - List of PluginType to include. If empty, no plugins will be serialized. If undefined, all plugins will be serialized.
* @param pluginFilter - List of PluginType to include. If empty, no plugins will be serialized. If undefined/not-passed, all plugins will be serialized.
* @returns {any} - Serializable JSON object.
*/
toJSON(binary = true, pluginFilter?: string[]): ISerializedViewerConfig {
@@ -1202,7 +1216,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes

/**
* Uses the {@link FileTransferPlugin} to export a Blob/File. If the plugin is not available, it will download the blob.
* `FileTransferPlugin` can be configured by other plugins to export the blob to a specific location like local file system, cloud storage, etc.
* {@link FileTransferPlugin} can be configured by other plugins to export the blob to a specific location like local file system, cloud storage, etc.
* @param blob - The blob or file to export/download
* @param name - name of the file, if not provided, the name of the file is used if it's a file.
*/

Загрузка…
Отмена
Сохранить