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.

RenderTargetPreviewPlugin.ts 6.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import {AViewerPluginSync, ThreeViewer} from '../../viewer'
  2. import {IRenderTarget} from '../../rendering'
  3. import {createDiv, createStyles, getOrCall, onChange, ValOrArr, ValOrFunc} from 'ts-browser-helpers'
  4. import {SRGBColorSpace, Vector4, WebGLRenderTarget} from 'three'
  5. import styles from './RenderTargetPreviewPlugin.css?inline'
  6. import {CustomContextMenu} from '../../utils'
  7. import {uiFolderContainer, uiToggle} from 'uiconfig.js'
  8. import {ITexture} from '../../core'
  9. export interface RenderTargetBlock {
  10. target: ValOrFunc<IRenderTarget|{texture?: ValOrArr<ITexture>}|undefined>
  11. name: string
  12. visible: boolean
  13. transparent: boolean
  14. originalColorSpace: boolean
  15. div: HTMLDivElement
  16. }
  17. @uiFolderContainer('Render Target Preview Plugin')
  18. export class RenderTargetPreviewPlugin<TEvent extends string> extends AViewerPluginSync<TEvent> {
  19. static readonly PluginType = 'RenderTargetPreviewPlugin'
  20. @uiToggle('Enabled')
  21. @onChange(RenderTargetPreviewPlugin.prototype.refreshUi) enabled = true
  22. toJSON: any = null
  23. mainDiv: HTMLDivElement = createDiv({id: 'RenderTargetPreviewPluginContainer', addToBody: false})
  24. stylesheet?: HTMLStyleElement
  25. constructor(enabled = true) {
  26. super()
  27. this.enabled = enabled
  28. }
  29. targetBlocks: RenderTargetBlock[] = []
  30. onAdded(viewer: ThreeViewer): void {
  31. super.onAdded(viewer)
  32. viewer.addEventListener('postRender', this._postRender)
  33. this.stylesheet = createStyles(styles, viewer.container)
  34. this.refreshUi()
  35. }
  36. onRemove(viewer: ThreeViewer): void {
  37. viewer.removeEventListener('postRender', this._postRender)
  38. this.stylesheet?.remove()
  39. this.stylesheet = undefined
  40. this.refreshUi()
  41. super.onRemove(viewer)
  42. }
  43. private _postRender = () => {
  44. if (!this._viewer) return
  45. for (const targetBlock of this.targetBlocks) {
  46. if (!targetBlock.visible) continue
  47. const rt = getOrCall(targetBlock.target)
  48. if (!rt) {
  49. // todo draw white or pink
  50. continue
  51. }
  52. const rect = targetBlock.div.getBoundingClientRect()
  53. let tex = rt.texture
  54. const canvasRect = this._viewer.canvas.getBoundingClientRect()
  55. rect.x = rect.x - canvasRect.x
  56. rect.y = canvasRect.height + canvasRect.y - rect.y - rect.height
  57. if (Array.isArray(tex)) {
  58. // todo support multi target
  59. this._viewer.console.warn('Multi target preview not supported yet, rendering just the first one')
  60. tex = tex[0]
  61. }
  62. const outputColorSpace = this._viewer.renderManager.webglRenderer.outputColorSpace
  63. if (!targetBlock.originalColorSpace) this._viewer.renderManager.webglRenderer.outputColorSpace = SRGBColorSpace
  64. this._viewer.renderManager.blit(null, {
  65. source: tex,
  66. clear: !targetBlock.transparent,
  67. respectColorSpace: !targetBlock.originalColorSpace,
  68. viewport: new Vector4(rect.x, rect.y, rect.width, rect.height),
  69. })
  70. this._viewer.renderManager.webglRenderer.outputColorSpace = outputColorSpace
  71. }
  72. }
  73. addTarget(target: RenderTargetBlock['target'], name: string, transparent = false, originalColorSpace = false, visible = true): this {
  74. if (!target) return this
  75. const div = document.createElement('div')
  76. const targetDef = {target, name, transparent, div, originalColorSpace, visible}
  77. div.classList.add('RenderTargetPreviewPluginTarget')
  78. if (!targetDef.visible) div.classList.add('RenderTargetPreviewPluginCollapsed')
  79. const header = document.createElement('div')
  80. header.classList.add('RenderTargetPreviewPluginTargetHeader')
  81. header.innerText = name
  82. header.onclick = () => {
  83. targetDef.visible = !targetDef.visible
  84. if (!targetDef.visible) div.classList.add('RenderTargetPreviewPluginCollapsed')
  85. else div.classList.remove('RenderTargetPreviewPluginCollapsed')
  86. this._viewer?.setDirty()
  87. }
  88. header.oncontextmenu = (e) => {
  89. e.preventDefault()
  90. e.stopPropagation()
  91. CustomContextMenu.Create({
  92. 'Download': () => this.downloadTarget(target),
  93. 'Remove': () => this.removeTarget(target),
  94. }, e.clientX, e.clientY)
  95. }
  96. div.appendChild(header)
  97. this.mainDiv.appendChild(div)
  98. this.targetBlocks.push(targetDef)
  99. this.refreshUi()
  100. return this
  101. }
  102. removeTarget(target: RenderTargetBlock['target']): this {
  103. const index = this.targetBlocks.findIndex(t => t.target === target)
  104. if (index >= 0) {
  105. const t = this.targetBlocks[index]
  106. this.targetBlocks.splice(index, 1)
  107. t.div.remove()
  108. }
  109. this.refreshUi()
  110. return this
  111. }
  112. downloadTarget(target1: RenderTargetBlock['target']): this {
  113. if (!this._viewer) return this
  114. const target = getOrCall(target1)
  115. if (!target) return this
  116. const tex = target.texture
  117. if (Array.isArray(tex)) {
  118. // todo support multi target
  119. this._viewer.dialog.alert('Multi target not supported yet')
  120. this._viewer.console.warn('support multi target export')
  121. return this
  122. }
  123. const canvas = this._viewer?.canvas
  124. if (!canvas) return this
  125. const blob = this._viewer.renderManager.exportRenderTarget(target as WebGLRenderTarget)
  126. const url = URL.createObjectURL(blob)
  127. const link = document.createElement('a')
  128. document.body.appendChild(link)
  129. link.style.display = 'none'
  130. link.href = url
  131. link.download = 'renderTarget.' + (blob.ext || 'png')
  132. link.click()
  133. document.body.removeChild(link)
  134. URL.revokeObjectURL(url)
  135. return this
  136. }
  137. refreshUi(): void {
  138. if (!this.mainDiv) return
  139. if (!this._viewer) {
  140. if (this.mainDiv.parentElement) this.mainDiv.remove()
  141. this.mainDiv.style.display = 'none'
  142. this.mainDiv.style.zIndex = '1000'
  143. return
  144. }
  145. if (!this.mainDiv.parentElement) this._viewer.container?.appendChild(this.mainDiv)
  146. this.mainDiv.style.display = !this.isDisabled() ? 'flex' : 'none'
  147. this.mainDiv.style.zIndex = parseInt(this._viewer.canvas.style.zIndex || '0') + 1 + ''
  148. this._viewer?.setDirty()
  149. }
  150. setDirty() { // for enable/disable functions
  151. this.refreshUi()
  152. }
  153. dispose() {
  154. for (const target of this.targetBlocks) {
  155. this.removeTarget(target.target)
  156. }
  157. super.dispose()
  158. }
  159. }