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

Add GLTFKHRMaterialVariantsPlugin and example, rename parallax mapping plugin example folder.

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

+ 39
- 0
README.md Просмотреть файл

@@ -123,6 +123,7 @@ To make changes and run the example, click on the CodePen button on the top righ
- [DeviceOrientationControlsPlugin](#deviceorientationcontrolsplugin) - Adds a controlsMode to the mainCamera for device orientation controls(gyroscope rotation control).
- [PointerLockControlsPlugin](#pointerlockcontrolsplugin) - Adds a controlsMode to the mainCamera for pointer lock controls.
- [ThreeFirstPersonControlsPlugin](#threefirstpersoncontrolsplugin) - Adds a controlsMode to the mainCamera for first person controls from threejs.
- [GLTFKHRMaterialVariantsPlugin](#gltfkhrmaterialvariantsplugin) - Support using for variants from KHR_materials_variants extension in gltf models.
- [Rhino3dmLoadPlugin](#rhino3dmloadplugin) - Add support for loading .3dm files
- [PLYLoadPlugin](#plyloadplugin) - Add support for loading .ply files
- [STLLoadPlugin](#stlloadplugin) - Add support for loading .stl files
@@ -3237,6 +3238,44 @@ viewer.scene.mainCamera.controlsMode = 'threeFirstPerson'
viewer.scene.mainCamera.controlsMode = 'orbit'
```

## GLTFKHRMaterialVariantsPlugin

[//]: # (todo: image)

[Example](https://threepipe.org/examples/#gltf-khr-material-variants-plugin/) —
[Source Code](./src/plugins/extras/GLTFKHRMaterialVariantsPlugin.ts) —
[API Reference](https://threepipe.org/docs/classes/GLTFKHRMaterialVariantsPlugin.html)

GLTFKHRMaterialVariantsPlugin adds support for importing and exporting glTF models with the `KHR_materials_variants` extension to load the model with different material variants/combinations. It also provides API and UI to change the current material variant.

The plugin automatically adds support for the extension when added to the viewer.

The materials are stored in `object.userData._variantMaterials` and are automatically loaded and saved when using the `GLTFLoader`.

Sample Usage
```typescript
import {ThreeViewer, GLTFKHRMaterialVariantsPlugin, Mesh2} from 'threepipe'

const viewer = new ThreeViewer({...})

const variantsPlugin = viewer.addPluginSync(GLTFKHRMaterialVariantsPlugin)

// load some model
await viewer.load(model_url)

// list of all variants in the model (names and objects)
console.log(variantsPlugin.variants)

// change the selected variant
variantsPlugin.selectedVariant = 'beach'
```

### Links

- https://www.khronos.org/blog/blender-gltf-i-o-support-for-gltf-pbr-material-extensions
- https://www.khronos.org/blog/streamlining-3d-commerce-with-material-variant-support-in-gltf-assets
- https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_variants/README.md

## Rhino3dmLoadPlugin

[Example](https://threepipe.org/examples/#rhino3dm-load/) —

+ 36
- 0
examples/gltf-khr-material-variants-plugin/index.html Просмотреть файл

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>glTF KHR Material Variants 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>

+ 29
- 0
examples/gltf-khr-material-variants-plugin/script.ts Просмотреть файл

@@ -0,0 +1,29 @@
import {_testFinish, GLTFKHRMaterialVariantsPlugin, IObject3D, SSAAPlugin, 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: [SSAAPlugin, GLTFKHRMaterialVariantsPlugin],
})

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

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

const result = await viewer.load<IObject3D>(
'https://cdn.jsdelivr.net/gh/KhronosGroup/glTF-Sample-Assets/Models/MaterialsVariantsShoe/glTF/MaterialsVariantsShoe.gltf',
{
autoCenter: true,
autoScale: true,
})

ui.setupPluginUi(GLTFKHRMaterialVariantsPlugin, {expanded: true})
ui.appendChild(result?.getObjectByName('Shoe')?.uiConfig)


}

init().finally(_testFinish)

examples/parallax-mapping/index.html → examples/parallax-mapping-plugin/index.html Просмотреть файл


examples/parallax-mapping/script.ts → examples/parallax-mapping-plugin/script.ts Просмотреть файл


+ 1
- 0
src/assetmanager/MaterialManager.ts Просмотреть файл

@@ -283,6 +283,7 @@ export class MaterialManager<T = ''> extends EventDispatcher<BaseEvent, T> {
return mat
}

// use convertToIMaterial
// processMaterial(material: IMaterial, options: AnyOptions&{useSourceMaterial?:boolean, materialTemplate?: string, register?: boolean}): IMaterial {
// if (!material.materialObject)
// material = (this._processMaterial(material, {...options, register: false}))!

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

@@ -0,0 +1,151 @@
import {AViewerPluginSync, ThreeViewer} from '../../viewer'
import {GLTFLoader2} from '../../assetmanager'
import {onChange, serialize} from 'ts-browser-helpers'
import {IMaterial, IObject3D} from '../../core'
import {UiObjectConfig} from 'uiconfig.js'
import {
GLTFMaterialsVariantsExtensionImport,
khrMaterialsVariantsGLTF,
} from './helpers/GLTFMaterialsVariantsExtensionImport'
import {gltfExporterMaterialsVariantsExtensionExport} from './helpers/GLTFMaterialsVariantsExtensionExport'

/**
* This plugin allows to import and export gltf files with KHR_materials_variants extension.
* The material data is stored in the object userData. The plugin also provides a UI to select the variant.
* @category Plugin
*/
export class GLTFKHRMaterialVariantsPlugin extends AViewerPluginSync<''> {
public static readonly PluginType = 'GLTFKHRMaterialVariantsPlugin'
enabled = true

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

onAdded(v: ThreeViewer): void {
super.onAdded(v)
// v.addEventListener('preRender', this._preRender)
v.scene.addEventListener('addSceneObject', this._objectAdded)
v.assetManager.importer.addEventListener('loaderCreate', this._loaderCreate as any)
v.assetManager.exporter.getExporter('gltf', 'glb')?.extensions?.push(gltfExporterMaterialsVariantsExtensionExport)
}

private _loaderCreate({loader}: {loader: GLTFLoader2}) {
if (!loader.isGLTFLoader2) return
loader.register((p) => new GLTFMaterialsVariantsExtensionImport(p))
}

onRemove(v: ThreeViewer): void {
v.scene.removeEventListener('addSceneObject', this._objectAdded)
v.assetManager.importer.removeEventListener('loaderCreate', this._loaderCreate as any)
const exportExts = v.assetManager.exporter.getExporter('gltf', 'glb')?.extensions || []
const i = exportExts.indexOf(gltfExporterMaterialsVariantsExtensionExport)
if (i !== -1) exportExts.splice(i, 1)
this.variants = {}
return super.onRemove(v)
}


variants: Record<string, IObject3D[]> = {} // dont serialize this

/**
* The selected variant. Changing this will automatically apply the variant to the objects.
*/
@onChange(GLTFKHRMaterialVariantsPlugin.prototype._variantChanged)
@serialize()
selectedVariant = ''

/**
* If true, the first variant will be applied to the objects when object is added and nothing is selected.
*/
@serialize()
applyFirstVariantOnLoad = true

private _variantChanged() {
this.applyVariant(this.selectedVariant || '', true)
}

/**
* Apply the variant to objects.
* It will also change the `selectedVariant` if `root` is not provided.
* @param name
* @param force
* @param root
* @param doTraverse
*/
applyVariant(name: string, force = false, root?: IObject3D[], doTraverse = true) {
if (!force && !root && this.selectedVariant === name) return
if (!name) return
if (!root) this.selectedVariant = name
const objects = root ?
Array.isArray(root) ? root : [root] :
name ? this.variants[name] || [] : Object.values(this.variants).flat()
for (const object of objects) {
const done = new Set()
const apply = (obj: IObject3D)=>{
if (!obj.userData._variantMaterials || done.has(obj)) return
const va = name ? obj.userData._variantMaterials[name]?.material : obj.userData._originalMaterial
if (va) {
if (!obj.userData._originalMaterial) obj.userData._originalMaterial = obj.material
obj.material = va
}
done.add(obj)
}
if (doTraverse) object.traverse(apply)
else apply(object)
}
}

private _objectAdded = (ev: any)=>{
const object = ev.object as IObject3D
if (!object?.isObject3D) return
if (!this._viewer) return
object.traverse((obj)=>{
if (obj.userData._variantMaterials) {
for (const val of Object.values(obj.userData._variantMaterials) as any) {
if (val?.material) val.material = this._viewer?.materialManager.convertToIMaterial(val.material, {}) || val.material
}
}

const d = obj.userData?.__importData?.[khrMaterialsVariantsGLTF]
if (!d) return
const names = d.names || [] as string[]
for (const name of names) {
if (!this.variants[name]) this.variants[name] = []
this.variants[name].push(obj)
}
delete obj.userData.__importData[khrMaterialsVariantsGLTF]
})
if (!this.selectedVariant && this.applyFirstVariantOnLoad) {
this.selectedVariant = Object.keys(this.variants)[0] || ''

}
this.uiConfig.uiRefresh?.()
return
}

uiConfig: UiObjectConfig = {
type: 'folder',
label: 'KHR Material Variants',
children: [
()=>({
children: [null, ...Object.keys(this.variants)].map((label) => !label ? {label: 'none', value: ''} : {label}),
type: 'dropdown',
label: 'Variant',
property: [this, 'selectedVariant'],
}),
],
}

}

declare module './../../core/IObject'{
interface IObject3DUserData{
/**
* Starts with `_` so that its not saved in gltf, but saved in json.
*/
_variantMaterials?: Record<string, {material: IMaterial}>
_originalMaterial?: IObject3D['material']
}
}

+ 122
- 0
src/plugins/extras/helpers/GLTFMaterialsVariantsExtensionExport.ts Просмотреть файл

@@ -0,0 +1,122 @@
/**
* Materials variants extension
* Modified from https://github.com/takahirox/three-gltf-extensions/blob/main/exporters/KHR_materials_variants/KHR_materials_variants_exporter.js
*
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_variants
*/

import {Material, Mesh, Object3D} from 'three'
import {khrMaterialsVariantsGLTF} from './GLTFMaterialsVariantsExtensionImport'
import {GLTFWriter2} from '../../../assetmanager'

/**
* @param object {THREE.Object3D}
* @return {boolean}
*/
const compatibleObject = (object: Object3D) => {
return (object as Mesh).material !== undefined && // easier than (!object.isMesh && !object.isLine && !object.isPoints)
object.userData && // just in case
object.userData._variantMaterials &&
!!Object.values(object.userData._variantMaterials).filter(m => compatibleMaterial((m as Mesh)?.material as any))
}

/**
* @param material {THREE.Material}
* @return {boolean}
*/
const compatibleMaterial = (material: Material) => {
// @TODO: support multi materials?
return material && material.isMaterial && !Array.isArray(material)
}

export class GLTFExporterMaterialsVariantsExtensionExport {
name = khrMaterialsVariantsGLTF
variantNames: string[] = []

constructor(public writer: GLTFWriter2) {
}

beforeParse(objects: Object3D[]) {
// Find all variant names and store them to the table
const variantNameTable = new Set<string>()
for (const object of objects) {
object.traverse(o => {
if (!compatibleObject(o)) {
return
}
const variantMaterials = o.userData._variantMaterials
for (const variantName in variantMaterials) {
const variantMaterial = variantMaterials[variantName]
// Ignore unloaded variant materials
if (compatibleMaterial(variantMaterial.material)) {
variantNameTable.add(variantName)
}
}
})
}
// We may want to sort?
variantNameTable.forEach(name => this.variantNames.push(name))
}

writeMesh(mesh: Mesh, meshDef: any) {
if (!compatibleObject(mesh)) {
return
}

const userData = mesh.userData
const variantMaterials = userData._variantMaterials
const mappingTable: Record<number, any> = {}
for (const variantName in variantMaterials) {
const variantMaterialInstance = variantMaterials[variantName].material
if (!compatibleMaterial(variantMaterialInstance)) {
continue
}
const variantIndex = this.variantNames.indexOf(variantName) // Shouldn't be -1
const materialIndex = this.writer.processMaterial(variantMaterialInstance)!
if (!mappingTable[materialIndex]) {
mappingTable[materialIndex] = {
material: materialIndex,
variants: [],
}
}
mappingTable[materialIndex].variants.push(variantIndex)
}

const mappingsDef = Object.values(mappingTable)
.map(m => {return (m.variants as number[]).sort((a, b) => a - b) && m})
.sort((a, b) => a.material - b.material)

if (mappingsDef.length === 0) {
return
}

const originalMaterialIndex = compatibleMaterial(userData._originalMaterial)
? this.writer.processMaterial(userData._originalMaterial) ?? -1 : -1

for (const primitiveDef of meshDef.primitives) {
// Override primitiveDef.material with original material.
if (originalMaterialIndex >= 0) {
primitiveDef.material = originalMaterialIndex
}
primitiveDef.extensions = primitiveDef.extensions || {}
primitiveDef.extensions[this.name] = {mappings: mappingsDef}
}
}

afterParse(_input: any) {
if (this.variantNames.length === 0) {
return
}

const root = this.writer.json
root.extensions = root.extensions || {}

const variantsDef = this.variantNames.map(n => {return {name: n}})
root.extensions[this.name] = {variants: variantsDef}
this.writer.extensionsUsed[this.name] = true
}
}

export function gltfExporterMaterialsVariantsExtensionExport(writer: GLTFWriter2) {
return new GLTFExporterMaterialsVariantsExtensionExport(writer)
}

+ 155
- 0
src/plugins/extras/helpers/GLTFMaterialsVariantsExtensionImport.ts Просмотреть файл

@@ -0,0 +1,155 @@
/**
* Materials variants extension
* Modified from https://github.com/takahirox/three-gltf-extensions/blob/main/loaders/KHR_materials_variants/KHR_materials_variants.js
*
* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_variants
*/

import {Material, Mesh, Object3D} from 'three'
import {GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader'
import {IObject3D} from '../../../core'

// export type OnUpdateType = ((arg0: Mesh, arg1: Material, arg2: any) => void) | null

/**
* KHR_materials_variants specification allows duplicated variant names
* but it makes handling the extension complex.
* We ensure tha names and make it easier.
* If you want to export the extension with the original names
* you are recommended to write GLTFExporter plugin to restore the names.
*
* @param variantNames {Array<string>}
* @return {Array<string>}
*/
const ensureUniqueNames = (variantNames: string[]): string[] => {
const uniqueNames = []
const knownNames = new Set()

for (const name of variantNames) {
let uniqueName = name
let suffix = 0
// @TODO: An easy solution.
// O(N^2) in the worst scenario where N is variantNames.length.
// Fix me if needed.
while (knownNames.has(uniqueName)) {
uniqueName = name + '.' + ++suffix
}
knownNames.add(uniqueName)
uniqueNames.push(uniqueName)
}

return uniqueNames
}

/**
* Convert mappings array to table object to make handling the extension easier.
*
* @param extensionDef {glTF.meshes[n].primitive.extensions.KHR_materials_variants}
* @param variantNames {Array<string>} Required to be unique names
* @return {Object}
*/
const mappingsArrayToTable = (extensionDef: any, variantNames: string[]): any => {
const table: any = {}
for (const mapping of extensionDef.mappings) {
for (const variant of mapping.variants) {
table[variantNames[variant]] = {
material: null,
gltfMaterialIndex: mapping.material,
}
}
}
return table
}

/**
* @param object {THREE.Object3D}
* @return {boolean}
*/
const compatibleObject = (object: Object3D) => {
return (object as Mesh).material !== undefined && // easier than (!object.isMesh && !object.isLine && !object.isPoints)
object.userData && // just in case
object.userData._variantMaterials
}

export const khrMaterialsVariantsGLTF = 'KHR_materials_variants'
export class GLTFMaterialsVariantsExtensionImport {
name = khrMaterialsVariantsGLTF

constructor(public parser: GLTFParser) {
}

// Note that the following properties will be overridden even if they are pre-defined
// - mesh.userData._variantMaterials
async afterRoot(gltf: any) {
const parser = this.parser
const json = parser.json

if (!json.extensions || !json.extensions[this.name]) return

const extensionDef = json.extensions[this.name]
const variantsDef = extensionDef.variants || []
const variants = ensureUniqueNames(variantsDef.map((v: any) => v.name))

// Save the _variantMaterials data under associated mesh.userData
for (const scene of gltf.scenes) {
// Save the variants data under associated mesh.userData
(scene as IObject3D).traverse(object => {
const association = parser.associations.get(object)

if (!association || association.meshes === undefined || (association as any).primitives === undefined) {
return
}

const meshDef = json.meshes[association.meshes]
const primitiveDef = meshDef.primitives[(association as any).primitives]
const extensionsDef = primitiveDef.extensions

if (!extensionsDef || !extensionsDef[this.name]) {
return
}

// object should be Mesh
object.userData._variantMaterials = mappingsArrayToTable(extensionsDef[this.name], variants)
})
}

// gltf.userData.variants = variants

/**
* @param object {THREE.Mesh}
* @return {Promise}
*/
const ensureLoadVariants = async(object: Mesh) => {
const currentMaterial = object.material as Material
const variantMaterials = object.userData._variantMaterials
const pending = []
for (const variantName in variantMaterials) {
const variantMaterial = variantMaterials[variantName]
if (variantMaterial.material) {
continue
}
const materialIndex = variantMaterial.gltfMaterialIndex
pending.push(parser.getDependency('material', materialIndex).then(material => {
object.material = material
parser.assignFinalMaterial(object)
variantMaterials[variantName].material = object.material
// delete variantMaterials[variantName].gltfMaterialIndex // todo;
}))
}
return Promise.all(pending).then(() => {
object.material = currentMaterial
})
}

await Promise.all(gltf.scenes.map(async(scene: Object3D) => {
const pending: Promise<any>[] = []
scene.traverse(o => compatibleObject(o) && pending.push(ensureLoadVariants(o as Mesh)))
if (!scene.userData.__importData) scene.userData.__importData = {}
scene.userData.__importData[khrMaterialsVariantsGLTF] = {
names: variants,
}
return Promise.all(pending)
}))

}
}

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

@@ -74,3 +74,4 @@ export {Object3DGeneratorPlugin} from './extras/Object3DGeneratorPlugin'
export {ContactShadowGroundPlugin} from './extras/ContactShadowGroundPlugin'
export {SimplifyModifierPlugin} from './extras/SimplifyModifierPlugin'
export {MeshOptSimplifyModifierPlugin} from './extras/MeshOptSimplifyModifierPlugin'
export {GLTFKHRMaterialVariantsPlugin} from './extras/GLTFKHRMaterialVariantsPlugin'

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