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.

SSAOPlugin.ts 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. import {Matrix4, Texture, TextureDataType, UnsignedByteType, Vector2, Vector3, Vector4, WebGLRenderTarget} from 'three'
  2. import {ExtendedShaderPass, IPassID, IPipelinePass} from '../../postprocessing'
  3. import {type IViewerEvent, ThreeViewer} from '../../viewer'
  4. import {PipelinePassPlugin} from '../base/PipelinePassPlugin'
  5. import {uiConfig, uiFolderContainer, uiImage, uiSlider} from 'uiconfig.js'
  6. import {ICamera, IMaterial, IRenderManager, IScene, IWebGLRenderer, PhysicalMaterial} from '../../core'
  7. import {getOrCall, glsl, onChange2, serialize, updateBit, ValOrFunc} from 'ts-browser-helpers'
  8. import {MaterialExtension} from '../../materials'
  9. import {shaderReplaceString, shaderUtils} from '../../utils'
  10. import {getTexelDecoding, matDefine, matDefineBool} from '../../three'
  11. import ssaoPass from './shaders/SSAOPlugin.pass.glsl'
  12. import ssaoPatch from './shaders/SSAOPlugin.patch.glsl'
  13. import {uiConfigMaterialExtension} from '../../materials/MaterialExtender'
  14. import {GBufferPlugin, GBufferUpdaterContext} from './GBufferPlugin'
  15. export type SSAOPluginEventTypes = ''
  16. export type SSAOPluginTarget = WebGLRenderTarget
  17. /**
  18. * SSAO Plugin
  19. *
  20. * Adds Screen Space Ambient Occlusion (SSAO) to the scene.
  21. * Adds a pass to calculate AO, which is then read by materials in the render pass.
  22. * @category Plugins
  23. */
  24. @uiFolderContainer('SSAO Plugin')
  25. export class SSAOPlugin
  26. extends PipelinePassPlugin<SSAOPluginPass, 'ssao', SSAOPluginEventTypes> {
  27. readonly passId = 'ssao'
  28. public static readonly PluginType = 'SSAOPlugin'
  29. dependencies = [GBufferPlugin]
  30. target?: SSAOPluginTarget
  31. @uiImage('SSAO Buffer' /* {readOnly: true}*/) texture?: Texture
  32. @uiConfig() protected _pass?: SSAOPluginPass
  33. // @onChange2(SSAOPlugin.prototype._createTarget)
  34. // @uiDropdown('Buffer Type', threeConstMappings.TextureDataType.uiConfig)
  35. readonly bufferType: TextureDataType // cannot be changed after creation (for now)
  36. // @onChange2(SSAOPlugin.prototype._createTarget)
  37. // @uiSlider('Buffer Size Multiplier', [0.25, 2.0], 0.25)
  38. readonly sizeMultiplier: number // cannot be changed after creation (for now)
  39. constructor(
  40. bufferType: TextureDataType = UnsignedByteType,
  41. sizeMultiplier = 1,
  42. enabled = true,
  43. ) {
  44. super()
  45. this.enabled = enabled
  46. this.bufferType = bufferType
  47. this.sizeMultiplier = sizeMultiplier
  48. }
  49. protected _createTarget(recreate = true) {
  50. if (!this._viewer) return
  51. if (recreate) this._disposeTarget()
  52. if (!this.target)
  53. this.target = this._viewer.renderManager.createTarget<SSAOPluginTarget>(
  54. {
  55. depthBuffer: false,
  56. type: this.bufferType,
  57. sizeMultiplier: this.sizeMultiplier,
  58. // magFilter: NearestFilter,
  59. // minFilter: NearestFilter,
  60. // generateMipmaps: false,
  61. // encoding: LinearEncoding,
  62. })
  63. this.texture = this.target.texture
  64. this.texture.name = 'ssaoBuffer'
  65. if (this._pass) this._pass.target = this.target
  66. }
  67. protected _disposeTarget() {
  68. if (!this._viewer) return
  69. if (this.target) {
  70. this._viewer.renderManager.disposeTarget(this.target)
  71. this.target = undefined
  72. }
  73. this.texture = undefined
  74. }
  75. private _gbufferUnpackExtension = undefined as MaterialExtension|undefined
  76. private _gbufferUnpackExtensionChanged = ()=>{
  77. if (!this._pass || !this._viewer) throw new Error('SSAOPlugin: pass/viewer not created yet')
  78. const newExtension = this._viewer.renderManager.gbufferUnpackExtension
  79. if (this._gbufferUnpackExtension === newExtension) return
  80. if (this._gbufferUnpackExtension) this._pass.material.unregisterMaterialExtensions([this._gbufferUnpackExtension])
  81. this._gbufferUnpackExtension = newExtension
  82. if (this._gbufferUnpackExtension) this._pass.material.registerMaterialExtensions([this._gbufferUnpackExtension])
  83. else this._viewer.console.warn('SSAOPlugin: GBuffer unpack extension removed')
  84. }
  85. protected _createPass() {
  86. if (!this._viewer) throw new Error('SSAOPlugin: viewer not set')
  87. if (!this._viewer.renderManager.gbufferTarget || !this._viewer.renderManager.gbufferUnpackExtension)
  88. throw new Error('SSAOPlugin: GBuffer target not created. GBufferPlugin or DepthBufferPlugin is required.')
  89. this._createTarget(true)
  90. // todo: send target as a func, so it works when changed
  91. return new SSAOPluginPass(this.passId, this.target)
  92. }
  93. onAdded(viewer: ThreeViewer) {
  94. super.onAdded(viewer)
  95. const gbuffer = viewer.getPlugin(GBufferPlugin)
  96. if (gbuffer) gbuffer.registerGBufferUpdater(this.constructor.PluginType, this.updateGBufferFlags.bind(this))
  97. else viewer.addEventListener('addPlugin', this._onPluginAdd)
  98. this._gbufferUnpackExtensionChanged()
  99. viewer.renderManager.addEventListener('gbufferUnpackExtensionChanged', this._gbufferUnpackExtensionChanged)
  100. }
  101. private _onPluginAdd = (e: IViewerEvent)=>{
  102. if (e.plugin?.constructor?.PluginType !== GBufferPlugin.PluginType) return
  103. const gbuffer = e.plugin as GBufferPlugin
  104. gbuffer.registerGBufferUpdater(this.constructor.PluginType, this.updateGBufferFlags.bind(this))
  105. this._viewer?.removeEventListener('addPlugin', this._onPluginAdd)
  106. }
  107. onRemove(viewer: ThreeViewer): void {
  108. viewer.removeEventListener('addPlugin', this._onPluginAdd)
  109. this._disposeTarget()
  110. return super.onRemove(viewer)
  111. }
  112. updateGBufferFlags(data: Vector4, c: GBufferUpdaterContext): void {
  113. if (!c.material || !c.material.userData) return
  114. const disabled = c.material.userData.ssaoCastDisabled || c.material.userData.pluginsDisabled
  115. const x = disabled ? 0 : 1
  116. data.w = updateBit(data.w, 3, x)
  117. if (disabled && this._pass) this._pass.checkGBufferFlag = true
  118. }
  119. /**
  120. * @deprecated use {@link target} instead
  121. */
  122. get aoTarget() {
  123. console.warn('SSAOPlugin: aoTarget is deprecated, use target instead')
  124. return this.target
  125. }
  126. }
  127. @uiFolderContainer('SSAO Pass')
  128. export class SSAOPluginPass extends ExtendedShaderPass implements IPipelinePass {
  129. before = ['render']
  130. after = ['gbuffer', 'depth']
  131. required = ['render'] // gbuffer required check done in plugin.
  132. // todo bilateralPass
  133. // @serialize() readonly bilateralPass: BilateralFilterPass
  134. // todo old deserialize
  135. // @serialize() readonly parameters: SSAOParams = {
  136. // intensity: 0.25,
  137. // occlusionWorldRadius: 1,
  138. // bias: 0.001,
  139. // falloff: 1.3,
  140. // }
  141. @serialize()
  142. @uiSlider('Intensity', [0, 4], 0.01)
  143. @onChange2(SSAOPluginPass.prototype.setDirty)
  144. intensity = 0.25
  145. @serialize()
  146. @uiSlider('Occlusion World Radius', [0.1, 8], 0.01)
  147. @onChange2(SSAOPluginPass.prototype.setDirty)
  148. occlusionWorldRadius = 1
  149. @serialize()
  150. @uiSlider('Bias', [0.00001, 0.01], 0.00001)
  151. @onChange2(SSAOPluginPass.prototype.setDirty)
  152. bias = 0.001
  153. @serialize()
  154. @uiSlider('Falloff', [0.01, 3], 0.01)
  155. @onChange2(SSAOPluginPass.prototype.setDirty)
  156. falloff = 1.3
  157. @serialize()
  158. @uiSlider('Num Samples', [1, 11], 1)
  159. @matDefine('NUM_SAMPLES', undefined, undefined, SSAOPluginPass.prototype.setDirty)
  160. numSamples = 8
  161. /**
  162. * Whether to check for gbuffer flag or not. This is used to disable SSAO casting by some objects. its enabled automatically by the SSAOPlugin when required.
  163. * This is disabled by default so that we dont read texture for no reason.
  164. */
  165. @matDefineBool('CHECK_GBUFFER_FLAG')
  166. checkGBufferFlag = false
  167. // todo after bilateralPass is implemented
  168. // @bindToValue({obj: 'bilateralPass', key: 'enabled', onChange: 'setDirty'})
  169. // smoothEnabled = true
  170. // todo after bilateralPass is implemented
  171. // @bindToValue({obj: 'bilateralPass', key: 'enabled', onChange: 'setDirty'})
  172. // smoothEdgeSharpness = true
  173. constructor(public readonly passId: IPassID, public target?: ValOrFunc<WebGLRenderTarget|undefined>) {
  174. super({
  175. defines: {
  176. ['LINEAR_DEPTH']: 1, // todo set from unpack extension
  177. ['NUM_SAMPLES']: 11,
  178. ['NUM_SPIRAL_TURNS']: 3,
  179. ['SSAO_PACKING']: 1, // 1 is (r: ssao, gba: depth), 2 is (rgb: ssao, a: 1), 3 is (rgba: packed_ssao), 4 is (rgb: packed_ssao, a: 1)
  180. ['PERSPECTIVE_CAMERA']: 1, // set in PerspectiveCamera2
  181. ['CHECK_GBUFFER_FLAG']: 0,
  182. },
  183. uniforms: {
  184. tLastThis: {value: null},
  185. screenSize: {value: new Vector2(0, 0)}, // set in ExtendedRenderMaterial
  186. saoData: {value: new Vector4()},
  187. frameCount: {value: 0}, // set in RenderManager
  188. cameraNearFar: {value: new Vector2(0.1, 1000)}, // set in PerspectiveCamera2
  189. projection: {value: new Matrix4()}, // set in PerspectiveCamera2
  190. saoBiasEpsilon: {value: new Vector3(1, 1, 1)},
  191. },
  192. vertexShader: shaderUtils.defaultVertex,
  193. fragmentShader: ssaoPass,
  194. }, 'tDiffuse') // why is tLastThis not here. because encoding and size doesnt matter?
  195. this.needsSwap = false
  196. this.clear = true
  197. // this.bilateralPass = new BilateralFilterPass(this._target as any, gBufferUnpack, 'rrrr')
  198. // this._multiplyPass = new GenericBlendTexturePass(this._target.texture as any, 'c = vec4((1.0-b.r) * a.xyz, a.a);')
  199. // this._getUiConfig = this._getUiConfig.bind(this)
  200. }
  201. render(renderer: IWebGLRenderer, writeBuffer: WebGLRenderTarget, readBuffer: WebGLRenderTarget, deltaTime: number, maskActive: boolean) {
  202. if (!this.enabled) return
  203. const target = getOrCall(this.target)
  204. if (!target) {
  205. console.warn('SSAOPluginPass: target not defined')
  206. return
  207. }
  208. this._updateParameters()
  209. // if (!this.material.defines.HAS_GBUFFER) {
  210. // console.warn('SSAOPluginPass: DepthNormalBuffer required for ssao')
  211. // }
  212. renderer.renderManager.blit(writeBuffer, {
  213. source: target.texture,
  214. })
  215. this.uniforms.tLastThis.value = writeBuffer.texture
  216. super.render(renderer, target, readBuffer, deltaTime, maskActive)
  217. // todo
  218. // if (this.smoothEnabled) {
  219. // this.bilateralPass.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive)
  220. // }
  221. }
  222. private _updateParameters() {
  223. // const projectionScale = 1 / (Math.tan(DEG2RAD * (camera as any).fov / 2) * 2);
  224. const saoData = this.material.uniforms.saoData.value
  225. // saoData.x = projectionScale;
  226. saoData.y = this.intensity
  227. saoData.z = this.occlusionWorldRadius
  228. // saoData.w = this.accIndex_++;
  229. const saoBiasEpsilon = this.material.uniforms.saoBiasEpsilon.value
  230. saoBiasEpsilon.x = this.bias
  231. saoBiasEpsilon.y = 0.001
  232. saoBiasEpsilon.z = this.falloff
  233. // this.material.uniforms.size.value.set(this._target.texture.image?.width, this._target.texture.image?.height)
  234. }
  235. beforeRender(_: IScene, camera: ICamera, renderManager: IRenderManager) {
  236. if (!this.enabled) return
  237. this.updateShaderProperties([camera, renderManager])
  238. }
  239. readonly materialExtension: MaterialExtension = {
  240. extraUniforms: {
  241. tSSAOMap: ()=>({value: getOrCall(this.target)?.texture ?? null}),
  242. },
  243. shaderExtender: (shader, _material, _renderer) => {
  244. if (!shader.defines.SSAO_ENABLED) return
  245. // todo: only SSAO_PACKING = 1 and 2 is supported. Not 3 and 4 right now.
  246. shader.fragmentShader = shaderReplaceString(shader.fragmentShader, '#include <aomap_fragment>', ssaoPatch)
  247. },
  248. onObjectRender: (_object, material, renderer: any) => {
  249. // const opaque = !material.transparent && (!material.transmission || material.transmission < 0.001)
  250. const x: any = this.enabled && // opaque &&
  251. renderer.userData.screenSpaceRendering !== false &&
  252. !material.userData?.pluginsDisabled &&
  253. !material.userData?.ssaoDisabled ? 1 : 0
  254. if (material.defines!.SSAO_ENABLED !== x) {
  255. material.defines!.SSAO_ENABLED = x
  256. material.needsUpdate = true
  257. }
  258. },
  259. parsFragmentSnippet: (renderer)=>glsl`
  260. uniform sampler2D tSSAOMap;
  261. ${getTexelDecoding('tSSAOMap', getOrCall(this.target)?.texture, renderer!.capabilities.isWebGL2)}
  262. #include <simpleCameraHelpers>
  263. `,
  264. computeCacheKey: () => {
  265. return this.enabled ? '1' : '0' + getOrCall(this.target)?.texture?.colorSpace
  266. },
  267. uuid: SSAOPlugin.PluginType,
  268. ...uiConfigMaterialExtension(this._getUiConfig.bind(this), SSAOPlugin.PluginType),
  269. isCompatible: material => {
  270. return (material as PhysicalMaterial).isPhysicalMaterial
  271. },
  272. }
  273. /**
  274. * Returns a uiConfig to toggle SSAO on a material.
  275. * This uiConfig is added to each material by extension
  276. * @param material
  277. * @private
  278. */
  279. protected _getUiConfig(material: IMaterial) {
  280. return {
  281. type: 'folder',
  282. label: 'SSAO',
  283. children: [
  284. {
  285. type: 'checkbox',
  286. label: 'Enabled',
  287. get value() {
  288. return !(material.userData.ssaoDisabled ?? false)
  289. },
  290. set value(v) {
  291. if (v === !(material.userData.ssaoDisabled ?? false)) return
  292. material.userData.ssaoDisabled = !v
  293. material.setDirty()
  294. },
  295. onChange: this.setDirty,
  296. },
  297. {
  298. type: 'checkbox',
  299. label: 'Cast SSAO',
  300. get value() {
  301. return !(material.userData.ssaoCastDisabled ?? false)
  302. },
  303. set value(v) {
  304. if (v === !(material.userData.ssaoCastDisabled ?? false)) return
  305. material.userData.ssaoCastDisabled = !v
  306. material.setDirty()
  307. },
  308. onChange: this.setDirty,
  309. },
  310. ],
  311. }
  312. }
  313. }
  314. declare module '../../core/IMaterial' {
  315. interface IMaterialUserData {
  316. /**
  317. * Disable SSAOPlugin for this material.
  318. */
  319. ssaoDisabled?: boolean
  320. /**
  321. * Cast SSAO on other objects.
  322. * if casting is not working when this is false, ensure render to depth is true, like for transparent objects
  323. */
  324. ssaoCastDisabled?: boolean
  325. }
  326. }