threepipe
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

FragmentClippingExtensionPlugin.ts 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import {Matrix3, Plane as PlaneThree, Vector4, Vector4Tuple} from 'three'
  2. import {AViewerPluginSync, ThreeViewer} from '../../viewer'
  3. import {uiFolderContainer, UiObjectConfig, uiToggle} from 'uiconfig.js'
  4. import {serialize} from 'ts-browser-helpers'
  5. import {IMaterial, IMaterialUserData, IObject3D, PhysicalMaterial} from '../../core'
  6. import {MaterialExtension, updateMaterialDefines} from '../../materials'
  7. import {shaderReplaceString, ThreeSerialization} from '../../utils'
  8. import {GLTFLoader2, GLTFWriter2} from '../../assetmanager'
  9. import type {GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader'
  10. import FragmentClippingExtensionPluginPars from './shaders/FragmentClippingExtensionPlugin.pars.glsl'
  11. import FragmentClippingExtensionPluginPatch from './shaders/FragmentClippingExtensionPlugin.patch.glsl'
  12. /**
  13. * FragmentClipping Materials Extension
  14. * Adds a material extension to PhysicalMaterial to add support for fragment clipping.
  15. * Fragment clipping allows to clip fragments of the material in screen space or world space based on a circle, rectangle, plane, sphere, etc.
  16. * It uses fixed SDFs with params defined by the user for clipping.
  17. * It also adds a UI to the material to edit the settings.
  18. * It uses WEBGI_materials_fragment_clipping_extension glTF extension to save the settings in glTF files.
  19. * @category Plugins
  20. */
  21. @uiFolderContainer('Fragment Clipping (MatExt)')
  22. export class FragmentClippingExtensionPlugin extends AViewerPluginSync<''> {
  23. static readonly PluginType = 'FragmentClippingExtensionPlugin1'
  24. @uiToggle('Enabled', (that: FragmentClippingExtensionPlugin)=>({onChange: that.setDirty}))
  25. @serialize() enabled = true
  26. private _defines: any = {
  27. ['FRAG_CLIPPING_DEBUG']: 0,
  28. }
  29. private _uniforms: any = {
  30. fragClippingPosition: {value: new Vector4()}, // point on plane, center of sphere, center of cylinder, etc
  31. fragClippingParams: {value: new Vector4()}, // normal of plane, radius of sphere, radius of cylinder, etc
  32. fragClippingCamAspect: {value: 1},
  33. }
  34. public static AddFragmentClipping(material: IMaterial, params?: IMaterialUserData['_fragmentClippingExt']): boolean {
  35. const ud = material?.userData
  36. if (!ud) return false
  37. if (!ud._fragmentClippingExt) {
  38. ud._fragmentClippingExt = {}
  39. }
  40. const tf = ud._fragmentClippingExt
  41. tf.clipEnabled = true
  42. if (tf.clipPosition === undefined) tf.clipPosition = [0, 0, 0, 0]
  43. if (tf.clipParams === undefined) tf.clipParams = [0, 0, 0, 0]
  44. if (tf.clipMode === undefined !== undefined) tf.clipMode = FragmentClippingMode.Circle
  45. if (tf.clipInvert === undefined !== undefined) tf.clipInvert = false
  46. params && Object.assign(tf, params)
  47. if (material.setDirty) material.setDirty()
  48. return true
  49. }
  50. private _plane = new PlaneThree()
  51. private _viewNormalMatrix = new Matrix3()
  52. private _v4 = new Vector4()
  53. readonly materialExtension: MaterialExtension = {
  54. parsFragmentSnippet: (_, material: PhysicalMaterial)=>{
  55. if (!this.enabled || !material?.userData._fragmentClippingExt?.clipEnabled) return ''
  56. return Object.entries(FragmentClippingMode)
  57. .map(v=>['FragmentClippingMode.' + v[0], '' + v[1]])// replace enum with integer values in the shader
  58. .reduce((a, v)=>a.replace(v[0], v[1]), FragmentClippingExtensionPluginPars)
  59. },
  60. shaderExtender: (shader, material: PhysicalMaterial) => {
  61. if (!this.enabled || !material?.userData._fragmentClippingExt?.clipEnabled) return
  62. shader.fragmentShader = shaderReplaceString(shader.fragmentShader, '#glMarker mainStart', Object.entries(FragmentClippingMode)
  63. .map(v=>['FragmentClippingMode.' + v[0], '' + v[1]]) // replace enum with integer values in the shader
  64. .reduce((a, v)=>a.replace(v[0], v[1]), '\n' + FragmentClippingExtensionPluginPatch), {append: true})
  65. },
  66. onObjectRender: (object: IObject3D, material) => {
  67. let tfUd = material.userData._fragmentClippingExt
  68. if (material.userData.isGBufferMaterial && object && object.material && !Array.isArray(object.material)) { // todo isGBufferMaterial
  69. tfUd = object.material?.userData._fragmentClippingExt
  70. }
  71. if (!tfUd?.clipEnabled) return
  72. if (Array.isArray(tfUd.clipPosition))
  73. this._uniforms.fragClippingPosition.value.fromArray(tfUd.clipPosition)
  74. else
  75. this._uniforms.fragClippingPosition.value.copy(tfUd.clipPosition)
  76. if (tfUd.clipMode === FragmentClippingMode.Plane && tfUd.clipParams) {
  77. const clipParams = Array.isArray(tfUd.clipParams) ? this._v4.fromArray(tfUd.clipParams) : this._v4.copy(tfUd.clipParams)
  78. const viewMatrix = this._viewer!.scene.mainCamera.matrixWorldInverse
  79. this._plane.normal.set(clipParams.x, clipParams.y, clipParams.z)
  80. this._plane.constant = clipParams.w
  81. this._viewNormalMatrix.getNormalMatrix(viewMatrix)
  82. this._plane.applyMatrix4(viewMatrix, this._viewNormalMatrix)
  83. this._uniforms.fragClippingParams.value.set(this._plane.normal.x, this._plane.normal.y, this._plane.normal.z, this._plane.constant)
  84. } else {
  85. if (Array.isArray(tfUd.clipPosition))
  86. this._uniforms.fragClippingParams.value.fromArray(tfUd.clipParams)
  87. else
  88. this._uniforms.fragClippingParams.value.copy(tfUd.clipParams)
  89. }
  90. if (this._viewer?.scene.mainCamera.isPerspectiveCamera)
  91. this._uniforms.fragClippingCamAspect.value = this._viewer?.scene.mainCamera.aspect
  92. else this._uniforms.fragClippingCamAspect.value = 1.0
  93. updateMaterialDefines({
  94. ...this._defines,
  95. // ['FRAGMENT_CLIPPING_EXTENSION_ENABLED']: this.enabled,
  96. ['FRAG_CLIPPING_MODE']: +(tfUd.clipMode ?? FragmentClippingMode.Circle),
  97. ['FRAG_CLIPPING_INVERSE']: +(tfUd.clipInvert ?? false),
  98. }, material)
  99. },
  100. extraUniforms: {
  101. // ...this._uniforms, // done in constructor
  102. },
  103. computeCacheKey: (material1: PhysicalMaterial) => {
  104. return (this.enabled ? '1' : '0') + (material1.userData._fragmentClippingExt?.clipEnabled ? '1' : '0')
  105. },
  106. isCompatible: (material1: PhysicalMaterial) => {
  107. return material1.isPhysicalMaterial || material1.userData.isGBufferMaterial // todo isGBufferMaterial
  108. },
  109. getUiConfig: material => { // todo use uiConfigMaterialExtension
  110. const viewer = this._viewer!
  111. if (material.userData._fragmentClippingExt === undefined) material.userData._fragmentClippingExt = {}
  112. const state = material.userData._fragmentClippingExt
  113. const config: UiObjectConfig = {
  114. type: 'folder',
  115. label: 'Fragment Clipping',
  116. onChange: (ev)=>{
  117. if (!ev.config) return
  118. this.setDirty()
  119. },
  120. children: [
  121. {
  122. type: 'checkbox',
  123. label: 'Enabled',
  124. get value() {
  125. return state.clipEnabled || false
  126. },
  127. set value(v) {
  128. if (v === state.clipEnabled) return
  129. if (v) {
  130. if (!FragmentClippingExtensionPlugin.AddFragmentClipping(material))
  131. viewer.dialog.alert('Cannot add FragmentClippingExtension.')
  132. } else {
  133. state.clipEnabled = false
  134. if (material.setDirty) material.setDirty()
  135. }
  136. config.uiRefresh?.(true, 'postFrame')
  137. },
  138. },
  139. {
  140. type: 'dropdown',
  141. label: 'Mode',
  142. children: Object.entries(FragmentClippingMode)
  143. // .filter(key => !isNaN(Number(FragmentClippingMode[key])))
  144. .map(v => ({label: v[0], value: v[1]})),
  145. hidden: () => !state.clipEnabled,
  146. property: [state, 'clipMode'],
  147. },
  148. {
  149. type: 'vec4',
  150. label: 'Position',
  151. bounds: [-1, 1],
  152. hidden: () => !state.clipEnabled,
  153. property: [state, 'clipPosition'],
  154. },
  155. {
  156. type: 'vec4',
  157. label: 'Params',
  158. bounds: [0, 1],
  159. hidden: () => !state.clipEnabled,
  160. property: [state, 'clipParams'],
  161. },
  162. {
  163. type: 'toggle',
  164. label: 'Invert',
  165. hidden: () => !state.clipEnabled,
  166. property: [state, 'clipInvert'],
  167. },
  168. ],
  169. }
  170. return config
  171. },
  172. }
  173. setDirty = (): void => {
  174. this.materialExtension.setDirty?.()
  175. this._viewer?.setDirty()
  176. }
  177. private _loaderCreate({loader}: {loader: GLTFLoader2}) {
  178. if (!loader.isGLTFLoader2) return
  179. loader.register((p) => new GLTFMaterialsFragmentClippingExtensionImport(p))
  180. }
  181. constructor() {
  182. super()
  183. this._loaderCreate = this._loaderCreate.bind(this)
  184. Object.assign(this.materialExtension.extraUniforms!, this._uniforms)
  185. }
  186. onAdded(v: ThreeViewer) {
  187. super.onAdded(v)
  188. // v.addEventListener('preRender', this._preRender)
  189. v.assetManager.materials.registerMaterialExtension(this.materialExtension)
  190. v.assetManager.importer.addEventListener('loaderCreate', this._loaderCreate as any)
  191. v.assetManager.exporter.getExporter('gltf', 'glb')?.extensions?.push(glTFMaterialsFragmentClippingExtensionExport)
  192. // v.getPlugin(GBufferPlugin)?.material?.registerMaterialExtensions([this.materialExtension])
  193. }
  194. onRemove(v: ThreeViewer) {
  195. v.assetManager.materials?.unregisterMaterialExtension(this.materialExtension)
  196. v.assetManager.importer?.removeEventListener('loaderCreate', this._loaderCreate as any)
  197. const exporter = v.assetManager.exporter.getExporter('gltf', 'glb')
  198. if (exporter) {
  199. const index = exporter.extensions?.indexOf(glTFMaterialsFragmentClippingExtensionExport)
  200. if (index !== undefined && index >= 0) exporter.extensions?.splice(index, 1)
  201. }
  202. // v.getPlugin(GBufferPlugin)?.material?.unregisterMaterialExtensions([this.materialExtension])
  203. return super.onRemove(v)
  204. }
  205. public static readonly FRAGMENT_CLIPPING_EXTENSION_GLTF_EXTENSION = 'WEBGI_materials_fragment_clipping_extension'
  206. }
  207. declare module '../../core/IMaterial' {
  208. interface IMaterialUserData {
  209. _fragmentClippingExt?: {
  210. clipEnabled?: boolean
  211. clipPosition?: Vector4|Vector4Tuple
  212. clipParams?: Vector4|Vector4Tuple
  213. clipMode?: FragmentClippingMode
  214. clipInvert?: boolean
  215. }
  216. }
  217. }
  218. export enum FragmentClippingMode {
  219. Circle = 0,
  220. Ellipse = 1,
  221. Rectangle = 2,
  222. Plane = 3,
  223. Sphere = 4
  224. }
  225. /**
  226. * FragmentClipping Materials Extension
  227. *
  228. * Specification: https://webgi.xyz/docs/gltf-extensions/WEBGI_materials_fragment_clipping_extension.html
  229. */
  230. class GLTFMaterialsFragmentClippingExtensionImport implements GLTFLoaderPlugin {
  231. public name: string
  232. public parser: GLTFParser
  233. constructor(parser: GLTFParser) {
  234. this.parser = parser
  235. this.name = FragmentClippingExtensionPlugin.FRAGMENT_CLIPPING_EXTENSION_GLTF_EXTENSION
  236. }
  237. async extendMaterialParams(materialIndex: number, materialParams: any) {
  238. const parser = this.parser
  239. const materialDef = parser.json.materials[materialIndex]
  240. if (!materialDef.extensions || !materialDef.extensions[this.name]) return
  241. const extension = materialDef.extensions[this.name]
  242. if (!materialParams.userData) materialParams.userData = {}
  243. FragmentClippingExtensionPlugin.AddFragmentClipping(materialParams)
  244. ThreeSerialization.Deserialize(extension, materialParams.userData._fragmentClippingExt)
  245. }
  246. }
  247. const glTFMaterialsFragmentClippingExtensionExport = (w: GLTFWriter2)=> ({
  248. writeMaterial: (material: any, materialDef: any) => {
  249. if (!material.isMeshStandardMaterial || !material.userData._fragmentClippingExt?.clipEnabled) return
  250. materialDef.extensions = materialDef.extensions || {}
  251. const extensionDef: any = ThreeSerialization.Serialize(material.userData._fragmentClippingExt)
  252. materialDef.extensions[ FragmentClippingExtensionPlugin.FRAGMENT_CLIPPING_EXTENSION_GLTF_EXTENSION ] = extensionDef
  253. w.extensionsUsed[ FragmentClippingExtensionPlugin.FRAGMENT_CLIPPING_EXTENSION_GLTF_EXTENSION ] = true
  254. },
  255. })