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.

FrameFadePlugin.ts 8.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import {LinearFilter, WebGLRenderTarget} from 'three'
  2. import {IPassID, IPipelinePass} from '../../postprocessing'
  3. import {ThreeViewer} from '../../viewer'
  4. import {PipelinePassPlugin} from '../base/PipelinePassPlugin'
  5. import {uiFolderContainer, uiToggle} from 'uiconfig.js'
  6. import {ITexture, IWebGLRenderer} from '../../core'
  7. import {AddBlendTexturePass} from '../../postprocessing/AddBlendTexturePass'
  8. import {now, serialize, timeout, ValOrFunc} from 'ts-browser-helpers'
  9. import {ProgressivePlugin} from './ProgressivePlugin'
  10. import {IRenderTarget} from '../../rendering'
  11. export type FrameFadePluginEventTypes = ''
  12. /**
  13. * FrameFade Plugin
  14. *
  15. * Adds a post-render pass to smoothly fade to a new rendered frame over time.
  16. * This is useful for example when changing the camera position, material, object properties, etc to avoid a sudden jump.
  17. * @category Plugins
  18. */
  19. @uiFolderContainer('FrameFade Plugin')
  20. export class FrameFadePlugin
  21. extends PipelinePassPlugin<FrameFadeBlendPass, 'frameFade', FrameFadePluginEventTypes> {
  22. readonly passId = 'frameFade'
  23. public static readonly PluginType = 'FrameFadePlugin'
  24. dependencies = [ProgressivePlugin]
  25. @serialize() @uiToggle() fadeOnActiveCameraChange = true
  26. @serialize() @uiToggle() fadeOnMaterialUpdate = true
  27. @serialize() @uiToggle() fadeOnSceneUpdate = true
  28. protected _pointerEnabled = true
  29. protected _target?: IRenderTarget
  30. constructor(
  31. enabled = true,
  32. ) {
  33. super()
  34. this.enabled = enabled
  35. this.startTransition = this.startTransition.bind(this)
  36. this.stopTransition = this.stopTransition.bind(this)
  37. this._fadeCam = this._fadeCam.bind(this)
  38. this._fadeMat = this._fadeMat.bind(this)
  39. this.isDisabled = ((sup)=>()=>!this._pointerEnabled || sup())(this.isDisabled)
  40. }
  41. saveFrameTimeThreshold = 500 // ms
  42. /**
  43. * Start a frame fade transition.
  44. * Note that the current frame data will only be used if the last running transition is ended or near the end. To do it anyway, call {@link stopTransition} first
  45. * @param duration
  46. */
  47. public async startTransition(duration: number) { // duration in ms
  48. if (!this._viewer || !this._pass || this.isDisabled()) return
  49. if (!this._target)
  50. this._target = this._viewer.renderManager.getTempTarget({
  51. sizeMultiplier: 1.,
  52. minFilter: LinearFilter,
  53. magFilter: LinearFilter,
  54. colorSpace: (this._viewer.renderManager.composerTarget.texture as ITexture).colorSpace,
  55. })
  56. if (this._pass.fadeTimeState < this.saveFrameTimeThreshold) // only save if very near the end
  57. this._pass.toSaveFrame = true
  58. this._pass.fadeTimeState = Math.max(duration, this._pass.fadeTimeState)
  59. this._pass.fadeTime = this._pass.fadeTimeState
  60. // this._pass.enabled = true
  61. this.setDirty()
  62. await timeout(duration)
  63. }
  64. /**
  65. * Stop a frame fade transition if running. Note that it will be stopped next frame.
  66. */
  67. public stopTransition() {
  68. if (!this._pass) return
  69. this._pass.fadeTimeState = 0. // will be stopped in update on next frame
  70. }
  71. onAdded(viewer: ThreeViewer) {
  72. super.onAdded(viewer)
  73. viewer.scene.addEventListener('mainCameraUpdate', this.stopTransition)
  74. viewer.scene.addEventListener('mainCameraChange', this._fadeCam)
  75. viewer.scene.addEventListener('materialUpdate', this._fadeMat)
  76. viewer.scene.addEventListener('sceneUpdate', this._fadeScene)
  77. viewer.scene.addEventListener('objectUpdate', this._fadeObjectUpdate)
  78. window.addEventListener('pointermove', this._onPointerMove)
  79. }
  80. onRemove(viewer: ThreeViewer) {
  81. viewer.scene.removeEventListener('mainCameraUpdate', this.stopTransition)
  82. viewer.scene.removeEventListener('mainCameraChange', this._fadeCam)
  83. viewer.scene.removeEventListener('materialUpdate', this._fadeMat)
  84. viewer.scene.removeEventListener('sceneUpdate', this._fadeScene)
  85. viewer.scene.removeEventListener('objectUpdate', this._fadeObjectUpdate)
  86. window.removeEventListener('pointermove', this._onPointerMove)
  87. super.onRemove(viewer)
  88. }
  89. private _fadeCam = async(ev: any)=>
  90. ev.frameFade !== false && this.fadeOnActiveCameraChange && this.startTransition(ev.fadeDuration || 1000)
  91. private _fadeMat = async(ev: any)=>
  92. ev.frameFade !== false && this.fadeOnMaterialUpdate && this.startTransition(ev.fadeDuration || 200)
  93. private _fadeScene = async(ev: any)=>
  94. ev.frameFade !== false && this.fadeOnSceneUpdate && this.startTransition(ev.fadeDuration || 500)
  95. private _fadeObjectUpdate = async(ev: any)=>
  96. ev.frameFade && this.startTransition(ev.fadeDuration || 500)
  97. private _onPointerMove = (ev: PointerEvent)=> {
  98. const canvas = this._viewer?.canvas
  99. if (!canvas) {
  100. this._pointerEnabled = false
  101. return
  102. }
  103. // no button is pressed
  104. if (!ev.buttons || ev.target !== canvas) {
  105. this._pointerEnabled = true
  106. return
  107. }
  108. // check if pointer is over canvas
  109. const rect = canvas.getBoundingClientRect()
  110. const x = (ev.clientX - rect.left) / rect.width
  111. const y = (ev.clientY - rect.top) / rect.height
  112. this._pointerEnabled = x < 0 || x > 1 || y < 0 || y > 1
  113. }
  114. setDirty() {
  115. if (this.isDisabled()) return
  116. this._viewer?.setDirty()
  117. }
  118. get dirty() {
  119. return !this.isDisabled() && !!this._pass && this._pass.fadeTimeState > 0
  120. }
  121. set dirty(_: boolean) {
  122. console.error('FrameFadePlugin.dirty is readonly')
  123. }
  124. protected _createPass() {
  125. return new FrameFadeBlendPass(this.passId, this)
  126. }
  127. get canFrameFade() {
  128. return this._target && this._pointerEnabled &&
  129. this.dirty && this._pass &&
  130. this._pass.fadeTimeState > 0.001 &&
  131. this._viewer && this._viewer.scene.renderCamera === this._viewer.scene.mainCamera
  132. }
  133. get lastFrame() {
  134. return this._viewer?.getPlugin(ProgressivePlugin)?.texture
  135. }
  136. get target() {
  137. return this._target
  138. }
  139. protected _beforeRender(): boolean {
  140. if (!super._beforeRender() || !this._pass) return false
  141. if (this.isDisabled()) this.stopTransition()
  142. if (this._pass.fadeTimeState < 0.001) {
  143. this._pass.toSaveFrame = false
  144. if (this._target && this._viewer) {
  145. this._viewer.renderManager.releaseTempTarget(this._target)
  146. this._target = undefined
  147. }
  148. }
  149. return true
  150. }
  151. }
  152. class FrameFadeBlendPass extends AddBlendTexturePass implements IPipelinePass {
  153. before = ['progressive', 'taa']
  154. after = ['render']
  155. required = ['render', 'progressive']
  156. dirty: ValOrFunc<boolean> = () => false
  157. fadeTime = 0 // ms
  158. fadeTimeState = 0
  159. toSaveFrame = false
  160. private _lastTime = 0
  161. constructor(public readonly passId: IPassID, public plugin: FrameFadePlugin) {
  162. super()
  163. }
  164. render(renderer: IWebGLRenderer, writeBuffer: WebGLRenderTarget, readBuffer: WebGLRenderTarget, deltaTime: number, maskActive: boolean) {
  165. this.needsSwap = false
  166. const target = this.plugin.target
  167. if (!this.plugin.canFrameFade || !target) return
  168. const lastFrame = this.plugin.lastFrame
  169. if (this.toSaveFrame && lastFrame) {
  170. renderer.renderManager.blit(target, {source: lastFrame, respectColorSpace: false})
  171. this._lastTime = 0
  172. this.toSaveFrame = false
  173. }
  174. this.uniforms.tDiffuse2.value = target.texture
  175. const weight = this.fadeTimeState / this.fadeTime
  176. this.uniforms.weight2.value.setScalar(weight)
  177. this.uniforms.weight2.value.w = 1
  178. this.uniforms.weight.value.setScalar(1. - weight)
  179. this.uniforms.weight.value.w = 1
  180. super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive)
  181. this.needsSwap = true
  182. const time = now()
  183. if (this._lastTime < 10) this._lastTime = time - 10 // ms
  184. const dt = time - this._lastTime
  185. this._lastTime = time
  186. this.fadeTimeState -= dt
  187. }
  188. }