threepipe
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

CustomBumpMapPlugin.ts 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. import {Matrix3, SRGBColorSpace, Texture} 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, IObject3D, ITexture, PhysicalMaterial} from '../../core'
  6. import {MaterialExtension, updateMaterialDefines} from '../../materials'
  7. import {shaderReplaceString} from '../../utils'
  8. import {GLTFLoader2, GLTFWriter2} from '../../assetmanager'
  9. import type {GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader'
  10. import CustomBumpMapPluginShader from './shaders/CustomBumpMapPlugin.glsl'
  11. import {matDefine} from '../../three'
  12. import {makeSamplerUi} from '../../ui/image-ui'
  13. /**
  14. * Custom Bump Map Plugin
  15. * Adds a material extension to PhysicalMaterial to support custom bump maps.
  16. * A Custom bump map is similar to the built-in bump map, but allows using an extra bump map and scale to give a combined effect.
  17. * This plugin also has support for bicubic filtering of the custom bump map and is enabled by default.
  18. * It also adds a UI to the material to edit the settings.
  19. * It uses WEBGI_materials_custom_bump_map glTF extension to save the settings in glTF files.
  20. * @category Plugins
  21. */
  22. @uiFolderContainer('Custom BumpMap (MatExt)')
  23. export class CustomBumpMapPlugin extends AViewerPluginSync<''> {
  24. static readonly PluginType = 'CustomBumpMapPlugin'
  25. @uiToggle('Enabled', (that: CustomBumpMapPlugin)=>({onChange: that.setDirty}))
  26. @serialize() enabled = true
  27. @uiToggle('Bicubic', (that: CustomBumpMapPlugin)=>({onChange: that.setDirty}))
  28. @matDefine('CUSTOM_BUMP_MAP_BICUBIC', undefined, true, CustomBumpMapPlugin.prototype.setDirty)
  29. @serialize() bicubicFiltering = true
  30. private _defines: any = {
  31. ['CUSTOM_BUMP_MAP_DEBUG']: false,
  32. ['CUSTOM_BUMP_MAP_BICUBIC']: true,
  33. }
  34. private _uniforms: any = {
  35. customBumpUvTransform: {value: new Matrix3()},
  36. customBumpScale: {value: 0.001},
  37. customBumpMap: {value: null},
  38. }
  39. public enableCustomBump(material: IMaterial, map?: ITexture, scale?: number): boolean {
  40. const ud = material?.userData
  41. if (!ud) return false
  42. if (ud._hasCustomBump === undefined) {
  43. const meshes = material.appliedMeshes
  44. let possible = true
  45. if (meshes) for (const {geometry} of meshes) {
  46. if (geometry && (!geometry.attributes.position || !geometry.attributes.normal || !geometry.attributes.uv)) {
  47. possible = false
  48. }
  49. // if (possible && !geometry.attributes.tangent) {
  50. // geometry.computeTangents()
  51. // }
  52. }
  53. if (!possible) {
  54. return false
  55. }
  56. }
  57. ud._hasCustomBump = true
  58. ud._customBumpScale = scale ?? ud._customBumpScale ?? 0.001
  59. ud._customBumpMap = map ?? ud._customBumpMap ?? null
  60. if (material.setDirty) material.setDirty()
  61. return true
  62. }
  63. readonly materialExtension: MaterialExtension = {
  64. parsFragmentSnippet: (_, material: PhysicalMaterial)=>{
  65. if (this.isDisabled() || !material?.userData._hasCustomBump) return ''
  66. return CustomBumpMapPluginShader
  67. },
  68. shaderExtender: (shader, material: PhysicalMaterial) => {
  69. if (this.isDisabled() || !material?.userData._hasCustomBump) return
  70. const customBumpMap = material.userData._customBumpMap
  71. if (!customBumpMap) return
  72. shader.fragmentShader = shaderReplaceString(shader.fragmentShader, '#glMarker beforeAccumulation',
  73. `
  74. #if defined(CUSTOM_BUMP_MAP_ENABLED) && CUSTOM_BUMP_MAP_ENABLED > 0
  75. normal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd_cb(), faceDirection );
  76. #endif
  77. `, {prepend: true}
  78. )
  79. shader.vertexShader = shaderReplaceString(shader.vertexShader, '#include <uv_pars_vertex>',
  80. `
  81. #if defined(CUSTOM_BUMP_MAP_ENABLED) && CUSTOM_BUMP_MAP_ENABLED > 0
  82. varying vec2 vCustomBumpUv;
  83. uniform mat3 customBumpUvTransform;
  84. #endif
  85. `, {prepend: true},
  86. )
  87. shader.vertexShader = shaderReplaceString(shader.vertexShader, '#include <uv_vertex>',
  88. `
  89. #if defined(CUSTOM_BUMP_MAP_ENABLED) && CUSTOM_BUMP_MAP_ENABLED > 0
  90. vCustomBumpUv = ( customBumpUvTransform * vec3( uv, 1 ) ).xy;
  91. #endif
  92. `, {prepend: true},
  93. )
  94. shader.defines.USE_UV = ''
  95. },
  96. onObjectRender: (object: IObject3D, material) => {
  97. const userData = material.userData
  98. if (!userData?._hasCustomBump) return
  99. if (!object.isMesh || !object.geometry) return
  100. const tex = userData._customBumpMap?.isTexture ? userData._customBumpMap : null
  101. this._uniforms.customBumpMap.value = tex
  102. this._uniforms.customBumpScale.value = tex ? userData._customBumpScale ?? 0 : 0
  103. if (tex) {
  104. tex.updateMatrix()
  105. this._uniforms.customBumpUvTransform.value.copy(tex.matrix)
  106. }
  107. updateMaterialDefines({
  108. ...this._defines,
  109. ['CUSTOM_BUMP_MAP_ENABLED']: +this.enabled,
  110. }, material)
  111. },
  112. extraUniforms: {
  113. // ...this._uniforms, // done in constructor
  114. },
  115. computeCacheKey: (material1: PhysicalMaterial) => {
  116. return (this.enabled ? '1' : '0') + (material1.userData._hasCustomBump ? '1' : '0') + material1.userData?._customBumpMap?.uuid
  117. },
  118. isCompatible: (material1: PhysicalMaterial) => material1.isPhysicalMaterial,
  119. getUiConfig: material => { // todo use uiConfigMaterialExtension
  120. const viewer = this._viewer!
  121. const enableCustomBump = this.enableCustomBump.bind(this)
  122. const state = material.userData
  123. const config: UiObjectConfig = {
  124. type: 'folder',
  125. label: 'CustomBumpMap',
  126. onChange: (ev)=>{
  127. if (!ev.config) return
  128. this.setDirty()
  129. },
  130. children: [
  131. {
  132. type: 'checkbox',
  133. label: 'Enabled',
  134. get value() {
  135. return state._hasCustomBump || false
  136. },
  137. set value(v) {
  138. if (v === state._hasCustomBump) return
  139. if (v) {
  140. if (!enableCustomBump(material))
  141. viewer.dialog.alert('Cannot add CustomBumpMap.')
  142. } else {
  143. state._hasCustomBump = false
  144. if (material.setDirty) material.setDirty()
  145. }
  146. config.uiRefresh?.(true, 'postFrame')
  147. },
  148. },
  149. {
  150. type: 'slider',
  151. label: 'Bump Scale',
  152. bounds: [-1, 1],
  153. hidden: () => !state._hasCustomBump,
  154. property: [state, '_customBumpScale'],
  155. onChange: this.setDirty,
  156. },
  157. {
  158. type: 'image',
  159. label: 'Bump Map',
  160. hidden: () => !state._hasCustomBump,
  161. property: [state, '_customBumpMap'],
  162. onChange: ()=>{
  163. material.setDirty()
  164. },
  165. },
  166. makeSamplerUi(state as any, '_customBumpMap'),
  167. ],
  168. }
  169. return config
  170. },
  171. }
  172. setDirty = (): void => {
  173. this.materialExtension.setDirty?.()
  174. this._viewer?.setDirty()
  175. }
  176. private _loaderCreate({loader}: {loader: GLTFLoader2}) {
  177. if (!loader.isGLTFLoader2) return
  178. loader.register((p) => new GLTFMaterialsCustomBumpMapImport(p))
  179. }
  180. constructor() {
  181. super()
  182. this._loaderCreate = this._loaderCreate.bind(this)
  183. Object.assign(this.materialExtension.extraUniforms!, this._uniforms)
  184. }
  185. onAdded(v: ThreeViewer) {
  186. super.onAdded(v)
  187. // v.addEventListener('preRender', this._preRender)
  188. v.assetManager.materials.registerMaterialExtension(this.materialExtension)
  189. v.assetManager.importer.addEventListener('loaderCreate', this._loaderCreate as any)
  190. v.assetManager.exporter.getExporter('gltf', 'glb')?.extensions?.push(glTFMaterialsCustomBumpMapExport)
  191. // v.getPlugin(GBufferPlugin)?.material?.registerMaterialExtensions([this.materialExtension])
  192. }
  193. onRemove(v: ThreeViewer) {
  194. v.assetManager.materials?.unregisterMaterialExtension(this.materialExtension)
  195. v.assetManager.importer?.removeEventListener('loaderCreate', this._loaderCreate as any)
  196. const exporter = v.assetManager.exporter.getExporter('gltf', 'glb')
  197. if (exporter) {
  198. const index = exporter.extensions?.indexOf(glTFMaterialsCustomBumpMapExport)
  199. if (index !== undefined && index >= 0) exporter.extensions?.splice(index, 1)
  200. }
  201. // v.getPlugin(GBufferPlugin)?.material?.unregisterMaterialExtensions([this.materialExtension])
  202. return super.onRemove(v)
  203. }
  204. public static readonly CUSTOM_BUMP_MAP_GLTF_EXTENSION = 'WEBGI_materials_custom_bump_map'
  205. }
  206. declare module '../../core/IMaterial' {
  207. interface IMaterialUserData {
  208. _hasCustomBump?: boolean
  209. _customBumpMap?: ITexture | null
  210. _customBumpScale?: number
  211. }
  212. }
  213. /**
  214. * FragmentClipping Materials Extension
  215. *
  216. * Specification: https://webgi.xyz/docs/gltf-extensions/WEBGI_materials_fragment_clipping_extension.html
  217. */
  218. class GLTFMaterialsCustomBumpMapImport implements GLTFLoaderPlugin {
  219. public name: string
  220. public parser: GLTFParser
  221. constructor(parser: GLTFParser) {
  222. this.parser = parser
  223. this.name = CustomBumpMapPlugin.CUSTOM_BUMP_MAP_GLTF_EXTENSION
  224. }
  225. async extendMaterialParams(materialIndex: number, materialParams: any) {
  226. const parser = this.parser
  227. const materialDef = parser.json.materials[materialIndex]
  228. if (!materialDef.extensions || !materialDef.extensions[this.name]) return
  229. const extension = materialDef.extensions[this.name]
  230. if (!materialParams.userData) materialParams.userData = {}
  231. materialParams.userData._hasCustomBump = true // single _ so that its saved when cloning but not when saving
  232. materialParams.userData._customBumpScale = extension.customBumpScale ?? 0.0
  233. const pending = []
  234. const tex = extension.customBumpMap
  235. if (tex) {
  236. pending.push(parser.assignTexture(materialParams.userData, '_customBumpMap', tex).then((t: Texture) => {
  237. // t.format = RGBFormat
  238. t.colorSpace = SRGBColorSpace
  239. }))
  240. }
  241. return Promise.all(pending)
  242. }
  243. // do any mesh or geometry processing here
  244. // afterRoot(result: GLTF): Promise<void> | null {
  245. // result.scene.traverse((object: any) => {
  246. // const mat = object.material?.userData?._hasCustomBump
  247. // if (!mat) return
  248. // const geom = object.geometry
  249. // if (!geom.attributes.tangent) {
  250. // geom.computeTangents()
  251. // geom.attributes.tangent.needsUpdate = true
  252. // }
  253. // })
  254. // return null
  255. // }
  256. }
  257. const glTFMaterialsCustomBumpMapExport = (w: GLTFWriter2)=> ({
  258. writeMaterial: (material: any, materialDef: any) => {
  259. if (!material.isMeshStandardMaterial || !material.userData._hasCustomBump) return
  260. if ((material.userData._customBumpScale || 0) < 0.001) return // todo: is this correct?
  261. materialDef.extensions = materialDef.extensions || {}
  262. const extensionDef: any = {}
  263. extensionDef.customBumpScale = material.userData._customBumpScale || 1.0
  264. if (material.userData._customBumpMap) {
  265. const customBumpMapDef = {index: w.processTexture(material.userData._customBumpMap)}
  266. w.applyTextureTransform(customBumpMapDef, material.userData._customBumpMap)
  267. extensionDef.customBumpMap = customBumpMapDef
  268. }
  269. materialDef.extensions[ CustomBumpMapPlugin.CUSTOM_BUMP_MAP_GLTF_EXTENSION ] = extensionDef
  270. w.extensionsUsed[ CustomBumpMapPlugin.CUSTOM_BUMP_MAP_GLTF_EXTENSION ] = true
  271. },
  272. })