| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- import {Matrix3, Plane as PlaneThree, Vector4, Vector4Tuple} from 'three'
- import {AViewerPluginSync, ThreeViewer} from '../../viewer'
- import {uiFolderContainer, UiObjectConfig, uiToggle} from 'uiconfig.js'
- import {serialize} from 'ts-browser-helpers'
- import {IMaterial, IMaterialUserData, IObject3D, PhysicalMaterial} from '../../core'
- import {MaterialExtension, updateMaterialDefines} from '../../materials'
- import {shaderReplaceString, ThreeSerialization} from '../../utils'
- import {GLTFLoader2, GLTFWriter2} from '../../assetmanager'
- import type {GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader'
- import FragmentClippingExtensionPluginPars from './shaders/FragmentClippingExtensionPlugin.pars.glsl'
- import FragmentClippingExtensionPluginPatch from './shaders/FragmentClippingExtensionPlugin.patch.glsl'
-
- /**
- * FragmentClipping Materials Extension
- * Adds a material extension to PhysicalMaterial to add support for fragment clipping.
- * Fragment clipping allows to clip fragments of the material in screen space or world space based on a circle, rectangle, plane, sphere, etc.
- * It uses fixed SDFs with params defined by the user for clipping.
- * It also adds a UI to the material to edit the settings.
- * It uses WEBGI_materials_fragment_clipping_extension glTF extension to save the settings in glTF files.
- * @category Plugins
- */
- @uiFolderContainer('FragmentClipping Materials')
- export class FragmentClippingExtensionPlugin extends AViewerPluginSync<''> {
- static readonly PluginType = 'FragmentClippingExtensionPlugin1'
-
- @uiToggle('Enabled', (that: FragmentClippingExtensionPlugin)=>({onChange: that.setDirty}))
- @serialize() enabled = true
-
- private _defines: any = {
- ['FRAG_CLIPPING_DEBUG']: 0,
- }
- private _uniforms: any = {
- fragClippingPosition: {value: new Vector4()}, // point on plane, center of sphere, center of cylinder, etc
- fragClippingParams: {value: new Vector4()}, // normal of plane, radius of sphere, radius of cylinder, etc
- fragClippingCamAspect: {value: 1},
- }
-
- public static AddFragmentClipping(material: IMaterial, params?: IMaterialUserData['_fragmentClippingExt']): boolean {
- const ud = material?.userData
- if (!ud) return false
- if (!ud._fragmentClippingExt) {
- ud._fragmentClippingExt = {}
- }
- const tf = ud._fragmentClippingExt
- tf.clipEnabled = true
- if (tf.clipPosition === undefined) tf.clipPosition = [0, 0, 0, 0]
- if (tf.clipParams === undefined) tf.clipParams = [0, 0, 0, 0]
- if (tf.clipMode === undefined !== undefined) tf.clipMode = FragmentClippingMode.Circle
- if (tf.clipInvert === undefined !== undefined) tf.clipInvert = false
- Object.assign(tf, params)
- if (material.setDirty) material.setDirty()
- return true
- }
-
- private _plane = new PlaneThree()
- private _viewNormalMatrix = new Matrix3()
- private _v4 = new Vector4()
-
- readonly materialExtension: MaterialExtension = {
- parsFragmentSnippet: (_, material: PhysicalMaterial)=>{
- if (!this.enabled || !material?.userData._fragmentClippingExt?.clipEnabled) return ''
- return Object.entries(FragmentClippingMode)
- .map(v=>['FragmentClippingMode.' + v[0], '' + v[1]])// replace enum with integer values in the shader
- .reduce((a, v)=>a.replace(v[0], v[1]), FragmentClippingExtensionPluginPars)
- },
- shaderExtender: (shader, material: PhysicalMaterial) => {
- if (!this.enabled || !material?.userData._fragmentClippingExt?.clipEnabled) return
- shader.fragmentShader = shaderReplaceString(shader.fragmentShader, '#glMarker mainStart', Object.entries(FragmentClippingMode)
- .map(v=>['FragmentClippingMode.' + v[0], '' + v[1]]) // replace enum with integer values in the shader
- .reduce((a, v)=>a.replace(v[0], v[1]), '\n' + FragmentClippingExtensionPluginPatch), {append: true})
- },
- onObjectRender: (object: IObject3D, material) => {
- let tfUd = material.userData._fragmentClippingExt
- if (material.userData.isGBufferMaterial && object && object.material && !Array.isArray(object.material)) { // todo isGBufferMaterial
- tfUd = object.material?.userData._fragmentClippingExt
- }
- if (!tfUd?.clipEnabled) return
-
- if (Array.isArray(tfUd.clipPosition))
- this._uniforms.fragClippingPosition.value.fromArray(tfUd.clipPosition)
- else
- this._uniforms.fragClippingPosition.value.copy(tfUd.clipPosition)
-
- if (tfUd.clipMode === FragmentClippingMode.Plane && tfUd.clipParams) {
- const clipParams = Array.isArray(tfUd.clipParams) ? this._v4.fromArray(tfUd.clipParams) : this._v4.copy(tfUd.clipParams)
- const viewMatrix = this._viewer!.scene.mainCamera.matrixWorldInverse
- this._plane.normal.set(clipParams.x, clipParams.y, clipParams.z)
- this._plane.constant = clipParams.w
- this._viewNormalMatrix.getNormalMatrix(viewMatrix)
- this._plane.applyMatrix4(viewMatrix, this._viewNormalMatrix)
- this._uniforms.fragClippingParams.value.set(this._plane.normal.x, this._plane.normal.y, this._plane.normal.z, this._plane.constant)
- } else {
- if (Array.isArray(tfUd.clipPosition))
- this._uniforms.fragClippingParams.value.fromArray(tfUd.clipParams)
- else
- this._uniforms.fragClippingParams.value.copy(tfUd.clipParams)
- }
- if (this._viewer?.scene.mainCamera.isPerspectiveCamera)
- this._uniforms.fragClippingCamAspect.value = this._viewer?.scene.mainCamera.aspect
- else this._uniforms.fragClippingCamAspect.value = 1.0
-
- updateMaterialDefines({
- ...this._defines,
- // ['FRAGMENT_CLIPPING_EXTENSION_ENABLED']: this.enabled,
- ['FRAG_CLIPPING_MODE']: +(tfUd.clipMode ?? FragmentClippingMode.Circle),
- ['FRAG_CLIPPING_INVERSE']: +(tfUd.clipInvert ?? false),
- }, material)
- },
- extraUniforms: {
- ...this._uniforms,
- },
- computeCacheKey: (material1: PhysicalMaterial) => {
- return (this.enabled ? '1' : '0') + (material1.userData._fragmentClippingExt?.clipEnabled ? '1' : '0')
- },
- isCompatible: (material1: PhysicalMaterial) => {
- return material1.isPhysicalMaterial || material1.userData.isGBufferMaterial // todo isGBufferMaterial
- },
- getUiConfig: material => {
- const viewer = this._viewer!
- if (material.userData._fragmentClippingExt === undefined) material.userData._fragmentClippingExt = {}
- const state = material.userData._fragmentClippingExt
- const config: UiObjectConfig = {
- type: 'folder',
- label: 'Fragment Clipping',
- onChange: (ev)=>{
- if (!ev.config) return
- this.setDirty()
- },
- children: [
- {
- type: 'checkbox',
- label: 'Enabled',
- get value() {
- return state.clipEnabled || false
- },
- set value(v) {
- if (v === state.clipEnabled) return
- if (v) {
- if (!FragmentClippingExtensionPlugin.AddFragmentClipping(material))
- viewer.dialog.alert('Cannot add FragmentClippingExtension.')
- } else {
- state.clipEnabled = false
- if (material.setDirty) material.setDirty()
- }
- config.uiRefresh?.(true, 'postFrame')
- },
- },
- {
- type: 'dropdown',
- label: 'Mode',
- children: Object.entries(FragmentClippingMode)
- // .filter(key => !isNaN(Number(FragmentClippingMode[key])))
- .map(v => ({label: v[0], value: v[1]})),
- hidden: () => !state.clipEnabled,
- property: [state, 'clipMode'],
- },
- {
- type: 'vec4',
- label: 'Position',
- bounds: [-1, 1],
- hidden: () => !state.clipEnabled,
- property: [state, 'clipPosition'],
- },
- {
- type: 'vec4',
- label: 'Params',
- bounds: [0, 1],
- hidden: () => !state.clipEnabled,
- property: [state, 'clipParams'],
- },
- {
- type: 'toggle',
- label: 'Invert',
- hidden: () => !state.clipEnabled,
- property: [state, 'clipInvert'],
- },
- ],
- }
- return config
- },
-
- }
-
- setDirty = (): void => {
- this.materialExtension.setDirty?.()
- this._viewer?.setDirty()
- }
-
- private _loaderCreate({loader}: {loader: GLTFLoader2}) {
- if (!loader.isGLTFLoader2) return
- loader.register((p) => new GLTFMaterialsFragmentClippingExtensionImport(p))
- }
-
- constructor() {
- super()
- this._loaderCreate = this._loaderCreate.bind(this)
- }
-
- onAdded(v: ThreeViewer) {
- super.onAdded(v)
- // v.addEventListener('preRender', this._preRender)
- v.assetManager.materials.registerMaterialExtension(this.materialExtension)
- v.assetManager.importer.addEventListener('loaderCreate', this._loaderCreate as any)
- v.assetManager.exporter.getExporter('gltf', 'glb')?.extensions?.push(glTFMaterialsFragmentClippingExtensionExport)
- // v.getPlugin(GBufferPlugin)?.material?.registerMaterialExtensions([this.materialExtension])
-
- }
-
- onRemove(v: ThreeViewer) {
- v.assetManager.materials?.unregisterMaterialExtension(this.materialExtension)
- v.assetManager.importer?.removeEventListener('loaderCreate', this._loaderCreate as any)
- const exporter = v.assetManager.exporter.getExporter('gltf', 'glb')
- if (exporter) {
- const index = exporter.extensions?.indexOf(glTFMaterialsFragmentClippingExtensionExport)
- if (index !== undefined && index >= 0) exporter.extensions?.splice(index, 1)
- }
- // v.getPlugin(GBufferPlugin)?.material?.unregisterMaterialExtensions([this.materialExtension])
- return super.onRemove(v)
- }
-
- public static readonly FRAGMENT_CLIPPING_EXTENSION_GLTF_EXTENSION = 'WEBGI_materials_fragment_clipping_extension'
-
- }
-
- declare module '../../core/IMaterial' {
- interface IMaterialUserData {
- _fragmentClippingExt?: {
- clipEnabled?: boolean
- clipPosition?: Vector4|Vector4Tuple
- clipParams?: Vector4|Vector4Tuple
- clipMode?: FragmentClippingMode
- clipInvert?: boolean
- }
- }
- }
-
-
- export enum FragmentClippingMode {
- Circle = 0,
- Ellipse = 1,
- Rectangle = 2,
- Plane = 3,
- Sphere = 4
- }
-
- /**
- * FragmentClipping Materials Extension
- *
- * Specification: https://webgi.xyz/docs/gltf-extensions/WEBGI_materials_fragment_clipping_extension.html
- */
- class GLTFMaterialsFragmentClippingExtensionImport implements GLTFLoaderPlugin {
- public name: string
- public parser: GLTFParser
-
- constructor(parser: GLTFParser) {
- this.parser = parser
- this.name = FragmentClippingExtensionPlugin.FRAGMENT_CLIPPING_EXTENSION_GLTF_EXTENSION
- }
-
- async extendMaterialParams(materialIndex: number, materialParams: any) {
- const parser = this.parser
- const materialDef = parser.json.materials[materialIndex]
- if (!materialDef.extensions || !materialDef.extensions[this.name]) return
- const extension = materialDef.extensions[this.name]
- if (!materialParams.userData) materialParams.userData = {}
- FragmentClippingExtensionPlugin.AddFragmentClipping(materialParams)
- ThreeSerialization.Deserialize(extension, materialParams.userData._fragmentClippingExt)
- }
- }
-
- const glTFMaterialsFragmentClippingExtensionExport = (w: GLTFWriter2)=> ({
- writeMaterial: (material: any, materialDef: any) => {
- if (!material.isMeshStandardMaterial || !material.userData._fragmentClippingExt?.clipEnabled) return
- materialDef.extensions = materialDef.extensions || {}
-
- const extensionDef: any = ThreeSerialization.Serialize(material.userData._fragmentClippingExt)
-
- materialDef.extensions[ FragmentClippingExtensionPlugin.FRAGMENT_CLIPPING_EXTENSION_GLTF_EXTENSION ] = extensionDef
- w.extensionsUsed[ FragmentClippingExtensionPlugin.FRAGMENT_CLIPPING_EXTENSION_GLTF_EXTENSION ] = true
- },
- })
|