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.5KB

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