threepipe
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

NoiseBumpMaterialPlugin.ts 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. import {Vector2, Vector2Tuple, Vector3, Vector3Tuple, 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 NoiseBumpMaterialPluginPars from './shaders/NoiseBumpMaterialPlugin.pars.glsl'
  11. import NoiseBumpMaterialPluginPatch from './shaders/NoiseBumpMaterialPlugin.patch.glsl'
  12. /**
  13. * NoiseBump Materials Extension
  14. * Adds a material extension to PhysicalMaterial to add support for sparkle bump / noise bump by creating procedural bump map from noise to simulate sparkle flakes.
  15. * It uses voronoise function from blender along with several additions to generate the noise for the generation.
  16. * It also adds a UI to the material to edit the settings.
  17. * It uses WEBGI_materials_noise_bump glTF extension to save the settings in glTF files.
  18. * @category Plugins
  19. */
  20. @uiFolderContainer('Noise/Sparkle Bump (MatExt)')
  21. export class NoiseBumpMaterialPlugin extends AViewerPluginSync<''> {
  22. static readonly PluginType = 'NoiseBumpMaterialPlugin'
  23. @uiToggle('Enabled', (that: NoiseBumpMaterialPlugin)=>({onChange: that.setDirty}))
  24. @serialize() enabled = true
  25. // private _defines: any = {
  26. // }
  27. private _uniforms: any = {
  28. noiseBumpParams: {value: new Vector2()}, // u scale, v scale,
  29. noiseBumpScale: {value: 0.05},
  30. noiseBumpFlakeScale: {value: 1000.0},
  31. noiseFlakeClamp: {value: 1.0},
  32. noiseFlakeRadius: {value: 0.5},
  33. flakeParams: {value: new Vector4(0, 1, 3, 0)},
  34. flakeFallOffParams: {value: new Vector3(0, 1, 0)},
  35. useColorFlakes: {value: false},
  36. }
  37. public static AddNoiseBumpMaterial(material: IMaterial, params?: IMaterialUserData['_noiseBumpMat']): boolean {
  38. const ud = material?.userData
  39. if (!ud) return false
  40. if (!ud._noiseBumpMat) {
  41. ud._noiseBumpMat = {}
  42. }
  43. const tf = ud._noiseBumpMat
  44. tf.hasBump = true
  45. if (tf.bumpNoiseParams === undefined) tf.bumpNoiseParams = new Vector2(0.5, 0.5)
  46. if (tf.bumpScale === undefined) tf.bumpScale = 0.05
  47. if (tf.flakeScale === undefined) tf.flakeScale = 0.05
  48. if (tf.flakeClamp === undefined) tf.flakeClamp = 1
  49. if (tf.flakeRadius === undefined) tf.flakeRadius = 0.3
  50. if (tf.useColorFlakes === undefined) tf.useColorFlakes = false
  51. if (tf.flakeParams === undefined) tf.flakeParams = new Vector4(0, 1, 3, 0)
  52. if (tf.flakeFallOffParams === undefined) tf.flakeFallOffParams = new Vector3(0, 1, 0)
  53. params && Object.assign(tf, params)
  54. if (material.setDirty) material.setDirty()
  55. return true
  56. }
  57. readonly materialExtension: MaterialExtension = {
  58. parsFragmentSnippet: (_, material: PhysicalMaterial)=>{
  59. if (this.isDisabled() || !material?.userData._noiseBumpMat?.hasBump) return ''
  60. return NoiseBumpMaterialPluginPars
  61. },
  62. shaderExtender: (shader, material: PhysicalMaterial) => {
  63. if (this.isDisabled() || !material?.userData._noiseBumpMat?.hasBump) return
  64. shader.fragmentShader = shaderReplaceString(shader.fragmentShader, '#glMarker beforeAccumulation', NoiseBumpMaterialPluginPatch, {prepend: true})
  65. shader.defines.USE_UV = ''
  66. shader.extensionDerivatives = true
  67. },
  68. onObjectRender: (_: IObject3D, material) => {
  69. const tfUd = material.userData._noiseBumpMat
  70. if (!tfUd?.hasBump) return
  71. if (Array.isArray(tfUd.bumpNoiseParams)) this._uniforms.noiseBumpParams.value.fromArray(tfUd.bumpNoiseParams)
  72. else this._uniforms.noiseBumpParams.value.copy(tfUd.bumpNoiseParams)
  73. this._uniforms.noiseBumpScale.value = tfUd.bumpScale
  74. this._uniforms.noiseBumpFlakeScale.value = tfUd.flakeScale
  75. this._uniforms.noiseFlakeClamp.value = tfUd.flakeClamp
  76. this._uniforms.noiseFlakeRadius.value = tfUd.flakeRadius
  77. if (Array.isArray(tfUd.flakeParams)) this._uniforms.flakeParams.value.fromArray(tfUd.flakeParams)
  78. else this._uniforms.flakeParams.value.copy(tfUd.flakeParams)
  79. if (Array.isArray(tfUd.flakeFallOffParams)) this._uniforms.flakeFallOffParams.value.fromArray(tfUd.flakeFallOffParams)
  80. else this._uniforms.flakeFallOffParams.value.copy(tfUd.flakeFallOffParams)
  81. this._uniforms.useColorFlakes.value = tfUd.useColorFlakes
  82. updateMaterialDefines({
  83. // ...this._defines,
  84. ['NOISE_BUMP_MATERIAL_ENABLED']: +!this.isDisabled(),
  85. }, material)
  86. },
  87. extraUniforms: {
  88. ...this._uniforms,
  89. },
  90. computeCacheKey: (material1: PhysicalMaterial) => {
  91. return (this.isDisabled() ? '0' : '1') + (material1.userData._noiseBumpMat?.hasBump ? '1' : '0')
  92. },
  93. isCompatible: (material1: PhysicalMaterial) => material1.isPhysicalMaterial,
  94. getUiConfig: material => {
  95. const viewer = this._viewer!
  96. if (material.userData._noiseBumpMat === undefined) material.userData._noiseBumpMat = {}
  97. const state = material.userData._noiseBumpMat
  98. const config: UiObjectConfig = {
  99. type: 'folder',
  100. label: 'SparkleBump (NoiseBump)',
  101. onChange: (ev)=>{
  102. if (!ev.config) return
  103. this.setDirty()
  104. },
  105. children: [
  106. {
  107. type: 'checkbox',
  108. label: 'Enabled',
  109. get value() {
  110. return state.hasBump || false
  111. },
  112. set value(v) {
  113. if (v === state.hasBump) return
  114. if (v) {
  115. if (!NoiseBumpMaterialPlugin.AddNoiseBumpMaterial(material))
  116. viewer.dialog.alert('Cannot add NoiseBumpMaterial.')
  117. } else {
  118. state.hasBump = false
  119. if (material.setDirty) material.setDirty()
  120. }
  121. config.uiRefresh?.(true, 'postFrame')
  122. },
  123. },
  124. {
  125. type: 'vec4',
  126. label: 'Bump Noise Params',
  127. bounds: [0, 1],
  128. hidden: () => !state.hasBump,
  129. property: [state, 'bumpNoiseParams'],
  130. },
  131. {
  132. type: 'slider',
  133. label: 'Bump Scale',
  134. bounds: [0, 0.001],
  135. stepSize: 0.00001,
  136. hidden: () => !state.hasBump,
  137. property: [state, 'bumpScale'],
  138. },
  139. {
  140. type: 'slider',
  141. label: 'Flake Scale',
  142. bounds: [100, 10000],
  143. stepSize: 0.0001,
  144. hidden: () => !state.hasBump,
  145. property: [state, 'flakeScale'],
  146. },
  147. {
  148. type: 'slider',
  149. label: 'Flake Clamp',
  150. bounds: [0, 1],
  151. stepSize: 1,
  152. hidden: () => !state.hasBump,
  153. property: [state, 'flakeClamp'],
  154. },
  155. {
  156. type: 'slider',
  157. label: 'Flake Radius',
  158. bounds: [0.01, 1],
  159. stepSize: 0.001,
  160. hidden: () => !state.hasBump,
  161. property: [state, 'flakeRadius'],
  162. },
  163. {
  164. type: 'slider',
  165. label: 'Flake Roughness',
  166. bounds: [0., 1],
  167. stepSize: 0.01,
  168. hidden: () => !state.hasBump,
  169. property: [state.flakeParams, 'x'],
  170. },
  171. {
  172. type: 'slider',
  173. label: 'Flake Metalness',
  174. bounds: [0., 1],
  175. stepSize: 0.01,
  176. hidden: () => !state.hasBump,
  177. property: [state.flakeParams, 'y'],
  178. },
  179. {
  180. type: 'slider',
  181. label: 'Flake Strength',
  182. bounds: [0.0, 100],
  183. stepSize: 0.001,
  184. hidden: () => !state.hasBump,
  185. property: [state.flakeParams, 'z'],
  186. },
  187. {
  188. type: 'slider',
  189. label: 'Flake Threshold',
  190. bounds: [0.1, 10],
  191. stepSize: 0.001,
  192. hidden: () => !state.hasBump,
  193. property: [state.flakeParams, 'w'],
  194. },
  195. {
  196. type: 'slider',
  197. label: 'Falloff',
  198. stepSize: 1,
  199. bounds: [0, 1],
  200. hidden: () => !state.hasBump,
  201. property: [state.flakeFallOffParams, 'x'],
  202. },
  203. {
  204. type: 'slider',
  205. label: 'Linear falloff factor',
  206. bounds: [0., 10],
  207. stepSize: 0.001,
  208. hidden: () => !state.hasBump,
  209. property: [state.flakeFallOffParams, 'y'],
  210. },
  211. {
  212. type: 'slider',
  213. label: 'Quadratic falloff factor',
  214. bounds: [0., 10],
  215. stepSize: 0.001,
  216. hidden: () => !state.hasBump,
  217. property: [state.flakeFallOffParams, 'z'],
  218. },
  219. {
  220. type: 'checkbox',
  221. label: 'Colored Flakes',
  222. hidden: () => !state.hasBump,
  223. property: [state, 'useColorFlakes'],
  224. },
  225. ],
  226. }
  227. return config
  228. },
  229. }
  230. setDirty = (): void => {
  231. this.materialExtension.setDirty?.()
  232. this._viewer?.setDirty()
  233. }
  234. private _loaderCreate({loader}: {loader: GLTFLoader2}) {
  235. if (!loader.isGLTFLoader2) return
  236. loader.register((p) => new GLTFMaterialsNoiseBumpMaterialImport(p))
  237. }
  238. constructor() {
  239. super()
  240. this._loaderCreate = this._loaderCreate.bind(this)
  241. }
  242. onAdded(v: ThreeViewer) {
  243. super.onAdded(v)
  244. v.assetManager.materials.registerMaterialExtension(this.materialExtension)
  245. v.assetManager.importer.addEventListener('loaderCreate', this._loaderCreate as any)
  246. v.assetManager.exporter.getExporter('gltf', 'glb')?.extensions?.push(glTFMaterialsNoiseBumpMaterialExport)
  247. }
  248. onRemove(v: ThreeViewer) {
  249. v.assetManager.materials?.unregisterMaterialExtension(this.materialExtension)
  250. v.assetManager.importer?.removeEventListener('loaderCreate', this._loaderCreate as any)
  251. const exporter = v.assetManager.exporter.getExporter('gltf', 'glb')
  252. if (exporter) {
  253. const index = exporter.extensions?.indexOf(glTFMaterialsNoiseBumpMaterialExport)
  254. if (index !== undefined && index >= 0) exporter.extensions?.splice(index, 1)
  255. }
  256. return super.onRemove(v)
  257. }
  258. public static readonly NOISE_BUMP_MATERIAL_GLTF_EXTENSION = 'WEBGI_materials_noise_bump'
  259. }
  260. declare module '../../core/IMaterial' {
  261. interface IMaterialUserData {
  262. _noiseBumpMat?: {
  263. hasBump?: boolean
  264. bumpNoiseParams?: Vector2Tuple | Vector2
  265. bumpScale?: number
  266. flakeScale?: number
  267. flakeClamp?: number
  268. flakeRadius?: number
  269. useColorFlakes?: boolean
  270. flakeParams?: Vector4Tuple | Vector4
  271. flakeFallOffParams?: Vector3Tuple | Vector3
  272. }
  273. }
  274. }
  275. /**
  276. * FragmentClipping Materials Extension
  277. *
  278. * Specification: https://webgi.xyz/docs/gltf-extensions/WEBGI_materials_fragment_clipping_extension.html
  279. */
  280. class GLTFMaterialsNoiseBumpMaterialImport implements GLTFLoaderPlugin {
  281. public name: string
  282. public parser: GLTFParser
  283. constructor(parser: GLTFParser) {
  284. this.parser = parser
  285. this.name = NoiseBumpMaterialPlugin.NOISE_BUMP_MATERIAL_GLTF_EXTENSION
  286. }
  287. async extendMaterialParams(materialIndex: number, materialParams: any) {
  288. const parser = this.parser
  289. const materialDef = parser.json.materials[materialIndex]
  290. if (!materialDef.extensions || !materialDef.extensions[this.name]) return
  291. const extension = materialDef.extensions[this.name]
  292. if (!materialParams.userData) materialParams.userData = {}
  293. NoiseBumpMaterialPlugin.AddNoiseBumpMaterial(materialParams)
  294. ThreeSerialization.Deserialize(extension, materialParams.userData._noiseBumpMat)
  295. }
  296. }
  297. const glTFMaterialsNoiseBumpMaterialExport = (w: GLTFWriter2)=> ({
  298. writeMaterial: (material: any, materialDef: any) => {
  299. if (!material.isMeshStandardMaterial || !material.userData._noiseBumpMat?.hasBump) return
  300. materialDef.extensions = materialDef.extensions || {}
  301. const extensionDef: any = ThreeSerialization.Serialize(material.userData._noiseBumpMat)
  302. materialDef.extensions[ NoiseBumpMaterialPlugin.NOISE_BUMP_MATERIAL_GLTF_EXTENSION ] = extensionDef
  303. w.extensionsUsed[ NoiseBumpMaterialPlugin.NOISE_BUMP_MATERIAL_GLTF_EXTENSION ] = true
  304. },
  305. })