threepipe
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

RenderManager.ts 27KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. import {
  2. BaseEvent,
  3. Color,
  4. ColorSpace,
  5. FloatType,
  6. HalfFloatType,
  7. IUniform,
  8. NoBlending,
  9. NoColorSpace,
  10. NormalBlending,
  11. NoToneMapping,
  12. PCFShadowMap,
  13. Texture,
  14. Vector2,
  15. Vector4,
  16. WebGLMultipleRenderTargets,
  17. WebGLRenderer,
  18. WebGLRenderTarget,
  19. WebGLRenderTargetOptions,
  20. } from 'three'
  21. import {EffectComposer2, IPassID, IPipelinePass, sortPasses} from '../postprocessing'
  22. import {IRenderTarget} from './RenderTarget'
  23. import {RenderTargetManager} from './RenderTargetManager'
  24. import {IShaderPropertiesUpdater} from '../materials'
  25. import {
  26. IRenderManager,
  27. IRenderManagerEvent,
  28. IRenderManagerEventTypes,
  29. type IRenderManagerOptions,
  30. IRenderManagerUpdateEvent,
  31. IScene,
  32. IWebGLRenderer,
  33. upgradeWebGLRenderer,
  34. } from '../core'
  35. import {
  36. base64ToArrayBuffer,
  37. canvasFlipY,
  38. Class,
  39. getOrCall,
  40. onChange2,
  41. serializable,
  42. serialize,
  43. ValOrArr,
  44. } from 'ts-browser-helpers'
  45. import {uiButton, uiConfig, uiFolderContainer, uiMonitor, uiSlider, uiToggle} from 'uiconfig.js'
  46. import {generateUUID, textureDataToImageData} from '../three'
  47. import {BlobExt, EXRExporter2} from '../assetmanager'
  48. import {RendererBlitOptions} from '../core/IRenderer'
  49. @serializable('RenderManager')
  50. @uiFolderContainer('Render Manager')
  51. export class RenderManager<TEvent extends BaseEvent = IRenderManagerEvent, TEventTypes extends string = IRenderManagerEventTypes> extends RenderTargetManager<IRenderManagerEvent|TEvent, IRenderManagerEventTypes|TEventTypes> implements IShaderPropertiesUpdater, IRenderManager {
  52. private readonly _isWebGL2: boolean
  53. private readonly _composer: EffectComposer2
  54. private readonly _context: WebGLRenderingContext
  55. @uiMonitor('Render Size')
  56. private readonly _renderSize = new Vector2(512, 512) // this is updated automatically.
  57. protected readonly _renderer: IWebGLRenderer<this>
  58. private _renderScale = 1.
  59. @uiSlider('Render Scale', [0.1, 8], 0.05) // keep here in code so its at the top in the UI
  60. get renderScale(): number {
  61. return this._renderScale
  62. }
  63. set renderScale(value: number) {
  64. if (value !== this._renderScale) {
  65. this._renderScale = value
  66. this.setSize(undefined, undefined, true)
  67. }
  68. }
  69. @uiConfig(undefined, {label: 'Passes'})
  70. private _passes: IPipelinePass[] = []
  71. private _pipeline: IPassID[] = []
  72. private _passesNeedsUpdate = true
  73. private _frameCount = 0
  74. private _lastTime = 0
  75. private _totalFrameCount = 0
  76. public static readonly POWER_PREFERENCE: 'high-performance' | 'low-power' | 'default' = 'high-performance'
  77. get renderer() {return this._renderer}
  78. /**
  79. * Use total frame count, if this is set to true, then frameCount won't be reset when the viewer is set to dirty.
  80. * Which will generate different random numbers for each frame during postprocessing steps. With TAA set properly, this will give a smoother result.
  81. */
  82. @uiToggle() @serialize() stableNoise = false
  83. public frameWaitTime = 0 // time to wait before next frame // used by canvas recorder //todo/
  84. protected _dirty = true
  85. /**
  86. * Set autoBuildPipeline = false to be able to set the pipeline manually.
  87. */
  88. @onChange2(RenderManager.prototype.rebuildPipeline)
  89. public autoBuildPipeline = true
  90. @uiButton('Rebuild Pipeline')
  91. rebuildPipeline(setDirty = true): void {
  92. this._passesNeedsUpdate = true
  93. if (setDirty) this._updated({change: 'rebuild'})
  94. }
  95. /**
  96. * Regenerates the render pipeline by resolving dependencies and sorting the passes.
  97. * This is called automatically when the passes are changed.
  98. */
  99. private _refreshPipeline(): IPassID[] {
  100. if (!this.autoBuildPipeline) return this._pipeline
  101. const ps = this._passes
  102. return this._pipeline = sortPasses(ps)
  103. }
  104. private _animationLoop(time: number, frame?:XRFrame) {
  105. const deltaTime = time - this._lastTime
  106. this._lastTime = time
  107. this.frameWaitTime -= deltaTime
  108. if (this.frameWaitTime > 0) return
  109. this.frameWaitTime = 0
  110. this.dispatchEvent({type: 'animationLoop', deltaTime, time, renderer: this._renderer, xrFrame: frame})
  111. }
  112. constructor({canvas, alpha = true, renderScale = 1, targetOptions}:IRenderManagerOptions) {
  113. super()
  114. this._animationLoop = this._animationLoop.bind(this)
  115. // this._xrPreAnimationLoop = this._xrPreAnimationLoop.bind(this)
  116. this._renderSize = new Vector2(canvas.clientWidth, canvas.clientHeight)
  117. this._renderScale = renderScale
  118. this._renderer = this._initWebGLRenderer(canvas, alpha)
  119. this._context = this._renderer.getContext()
  120. this._isWebGL2 = this._renderer.capabilities.isWebGL2
  121. this.resetShadows()
  122. const composerTarget = this.createTarget<WebGLRenderTarget>(targetOptions, false)
  123. composerTarget.texture.name = 'EffectComposer.rt1'
  124. this._composer = new EffectComposer2(this._renderer, composerTarget)
  125. // if (animationLoop) this.addEventListener('animationLoop', animationLoop) // todo: from viewer
  126. }
  127. protected _initWebGLRenderer(canvas: HTMLCanvasElement, alpha: boolean): IWebGLRenderer<this> {
  128. const renderer = new WebGLRenderer({
  129. canvas,
  130. antialias: false,
  131. alpha,
  132. premultipliedAlpha: false, // todo: see this, maybe use this with rgbm mode.
  133. preserveDrawingBuffer: true,
  134. powerPreference: RenderManager.POWER_PREFERENCE,
  135. })
  136. // renderer.info.autoReset = false // Not supported by ExtendedRenderPass
  137. renderer.useLegacyLights = false
  138. renderer.setAnimationLoop(this._animationLoop)
  139. renderer.onContextLost = (event: WebGLContextEvent) => {
  140. this.dispatchEvent({type: 'contextLost', event})
  141. }
  142. renderer.onContextRestore = () => {
  143. // console.log('restored')
  144. this.dispatchEvent({type: 'contextRestored'})
  145. }
  146. renderer.setSize(this._renderSize.width, this._renderSize.height, false)
  147. renderer.setPixelRatio(this._renderScale)
  148. renderer.toneMapping = NoToneMapping
  149. renderer.toneMappingExposure = 1
  150. renderer.outputColorSpace = NoColorSpace // or SRGBColorSpace
  151. renderer.shadowMap.enabled = true
  152. renderer.shadowMap.type = PCFShadowMap // use? THREE.PCFShadowMap. dont use VSM if need ground: https://github.com/mrdoob/three.js/issues/17473
  153. // renderer.shadowMap.type = BasicShadowMap // use? THREE.PCFShadowMap. dont use VSM if need ground: https://github.com/mrdoob/three.js/issues/17473
  154. renderer.shadowMap.autoUpdate = false
  155. return upgradeWebGLRenderer.call(renderer, this)
  156. }
  157. setSize(width?: number, height?: number, force = false) {
  158. if (!force &&
  159. (width ? Math.abs(width - this._renderSize.width) : 0) +
  160. (height ? Math.abs(height - this._renderSize.height) : 0) < 0.1
  161. ) return
  162. if (width) this._renderSize.width = width
  163. if (height) this._renderSize.height = height
  164. if (!(this.webglRenderer.xr.enabled && this.webglRenderer.xr.isPresenting)) {
  165. this._renderer.setSize(this._renderSize.width, this._renderSize.height, false)
  166. this._renderer.setPixelRatio(this._renderScale)
  167. }
  168. this._composer.setPixelRatio(this._renderScale, false)
  169. this._composer.setSize(this._renderSize.width, this._renderSize.height)
  170. this.resizeTrackedTargets()
  171. // console.log('setSize', {...this._renderSize}, this._trackedTargets.length)
  172. this.dispatchEvent({type: 'resize'})
  173. this._updated({change: 'size', data: this._renderSize.toArray()})
  174. this.reset()
  175. }
  176. // render(scene: RenderScene): void {
  177. // const camera = scene.activeCamera
  178. // const activeScene = scene.activeScene
  179. // if(!camera) return
  180. // this._renderer.render(scene.threeScene, camera)
  181. // // todo gizmos
  182. // }
  183. /**
  184. * Default value for renderToScreen in {@link render}
  185. */
  186. defaultRenderToScreen = true
  187. render(scene: IScene, renderToScreen?: boolean): void {
  188. if (this._passesNeedsUpdate) {
  189. this._refreshPipeline()
  190. this.refreshPasses()
  191. }
  192. for (const pass of this._passes) {
  193. if (pass.enabled && pass.beforeRender) pass.beforeRender(scene, scene.renderCamera, this)
  194. }
  195. this._composer.renderToScreen = renderToScreen ?? this.defaultRenderToScreen
  196. this._composer.render()
  197. this._composer.renderToScreen = true
  198. if (renderToScreen) {
  199. this._frameCount += 1
  200. this._totalFrameCount += 1
  201. }
  202. this._dirty = false
  203. }
  204. onPostFrame = () => {
  205. for (const pass of this._passes) {
  206. if (pass.enabled && pass.onPostFrame) pass.onPostFrame?.(this)
  207. }
  208. }
  209. get needsRender(): boolean {
  210. this._dirty = this._dirty || this._passes.findIndex(value => getOrCall(value.dirty)) >= 0 // todo: check for enabled passes only.
  211. return this._dirty
  212. }
  213. setDirty(reset = false): void {
  214. this._dirty = true
  215. if (reset) this.reset()
  216. // do NOT call _updated from here.
  217. }
  218. reset(): void {
  219. this._frameCount = 0
  220. this._dirty = true
  221. // do NOT call _updated from here.
  222. }
  223. resetShadows(): void {
  224. this._renderer.shadowMap.needsUpdate = true
  225. }
  226. refreshPasses(): void {
  227. if (!this._passesNeedsUpdate) return
  228. this._passesNeedsUpdate = false
  229. const p = []
  230. for (const passId of this._pipeline) {
  231. const a = this._passes.find(value => value.passId === passId)
  232. if (!a) {
  233. console.warn('Unable to find pass: ', passId)
  234. continue
  235. }
  236. p.push(a)
  237. }
  238. [...this._composer.passes].forEach(p1=>this._composer.removePass(p1))
  239. p.forEach(p1=>this._composer.addPass(p1))
  240. this._updated({change: 'passRefresh'})
  241. }
  242. dispose(): void {
  243. super.dispose()
  244. this._renderer.dispose()
  245. }
  246. updateShaderProperties(material: {defines: Record<string, string|number|undefined>, uniforms: {[name: string]: IUniform}}): this {
  247. if (material.uniforms.currentFrameCount) material.uniforms.currentFrameCount.value = this.frameCount
  248. if (!this.stableNoise) {
  249. if (material.uniforms.frameCount) material.uniforms.frameCount.value = this._totalFrameCount
  250. else console.warn('RenderManager: no uniform: frameCount')
  251. } else {
  252. if (material.uniforms.frameCount) material.uniforms.frameCount.value = this.frameCount
  253. else console.warn('RenderManager: no uniform: frameCount')
  254. }
  255. return this
  256. }
  257. // region Passes
  258. registerPass(pass: IPipelinePass, replaceId = true): void {
  259. if (replaceId) {
  260. for (const pass1 of [...this._passes]) {
  261. if (pass.passId === pass1.passId) this.unregisterPass(pass1)
  262. }
  263. }
  264. this._passes.push(pass)
  265. pass.onRegister?.(this)
  266. this.rebuildPipeline(false)
  267. this._updated({change: 'registerPass', pass})
  268. }
  269. unregisterPass(pass: IPipelinePass): void {
  270. const i = this._passes.indexOf(pass)
  271. if (i >= 0) {
  272. pass.onUnregister?.(this)
  273. this._passes.splice(i, 1)
  274. this.rebuildPipeline(false)
  275. this._updated({change: 'unregisterPass', pass})
  276. }
  277. }
  278. // endregion
  279. // region Getters and Setters
  280. get frameCount(): number {
  281. return this._frameCount
  282. }
  283. get totalFrameCount(): number {
  284. return this._totalFrameCount
  285. }
  286. resetTotalFrameCount(): void {
  287. this._totalFrameCount = 0
  288. }
  289. set pipeline(value: IPassID[]) {
  290. this._pipeline = value
  291. if (this.autoBuildPipeline) {
  292. console.warn('RenderManager: pipeline changed, but autoBuildPipeline is true. This will not have any effect.')
  293. }
  294. this.rebuildPipeline()
  295. }
  296. get pipeline(): IPassID[] {
  297. return this._pipeline
  298. }
  299. get composer(): EffectComposer2 {
  300. return this._composer
  301. }
  302. get passes(): IPipelinePass[] {
  303. return this._passes
  304. }
  305. get isWebGL2(): boolean {
  306. return this._isWebGL2
  307. }
  308. get composerTarget(): IRenderTarget {
  309. return this._composer.renderTarget1
  310. }
  311. get composerTarget2(): IRenderTarget {
  312. return this._composer.renderTarget2
  313. }
  314. /**
  315. * The size set in the three.js renderer.
  316. * Final size is renderSize * renderScale
  317. */
  318. get renderSize(): Vector2 {
  319. return this._renderSize
  320. }
  321. get context(): WebGLRenderingContext {
  322. return this._context
  323. }
  324. /**
  325. * Same as {@link renderer}
  326. */
  327. get webglRenderer(): WebGLRenderer {
  328. return this._renderer
  329. }
  330. @serialize()
  331. get useLegacyLights(): boolean {
  332. return this._renderer.useLegacyLights
  333. }
  334. set useLegacyLights(v: boolean) {
  335. this._renderer.useLegacyLights = v
  336. this._updated({change: 'useLegacyLights', data: v})
  337. this.resetShadows()
  338. }
  339. get clock() {
  340. return this._composer.clock
  341. }
  342. // endregion
  343. // region Utils
  344. /**
  345. * blit - blits a texture to the screen or another render target.
  346. * @param destination - destination target, or screen if undefined or null
  347. * @param source - source Texture
  348. * @param viewport - viewport and scissor
  349. * @param material - override material
  350. * @param clear - clear before blit
  351. * @param respectColorSpace - does color space conversion when reading and writing to the target
  352. * @param blending - Note - Set to NormalBlending if transparent is set to false
  353. * @param transparent
  354. */
  355. blit(destination: IRenderTarget|undefined|null, {source, viewport, material, clear = true, respectColorSpace = false, blending = NoBlending, transparent = true}: RendererBlitOptions = {}): void {
  356. const copyPass = !respectColorSpace ? this._composer.copyPass : this._composer.copyPass2
  357. const {renderToScreen, material: oldMaterial, uniforms: oldUniforms, clear: oldClear} = copyPass
  358. if (material) {
  359. copyPass.material = material
  360. }
  361. const oldTransparent = copyPass.material.transparent
  362. const oldViewport = !destination ? this._renderer.getViewport(new Vector4()) : destination.viewport.clone()
  363. const oldScissor = !destination ? this._renderer.getScissor(new Vector4()) : destination.scissor.clone()
  364. const oldScissorTest = !destination ? this._renderer.getScissorTest() : destination.scissorTest
  365. const oldAutoClear = this._renderer.autoClear
  366. const oldTarget = this._renderer.getRenderTarget()
  367. const oldBlending = copyPass.material.blending
  368. if (viewport) {
  369. if (!destination) {
  370. this._renderer.setViewport(viewport)
  371. this._renderer.setScissor(viewport)
  372. this._renderer.setScissorTest(true)
  373. } else {
  374. destination.viewport.copy(viewport)
  375. destination.scissor.copy(viewport)
  376. destination.scissorTest = true
  377. }
  378. }
  379. this._renderer.autoClear = false
  380. copyPass.material.blending = !transparent ? NormalBlending : blending
  381. copyPass.uniforms = copyPass.material.uniforms
  382. copyPass.renderToScreen = false
  383. copyPass.clear = clear
  384. copyPass.material.transparent = transparent
  385. copyPass.material.needsUpdate = true
  386. this._renderer.renderWithModes({
  387. sceneRender: true,
  388. opaqueRender: true,
  389. shadowMapRender: false,
  390. backgroundRender: false,
  391. transparentRender: true,
  392. transmissionRender: false,
  393. }, ()=>{
  394. copyPass.render(this._renderer, <WebGLRenderTarget>destination || null, {texture: source} as any, 0, false)
  395. })
  396. copyPass.renderToScreen = renderToScreen
  397. copyPass.clear = oldClear
  398. copyPass.material = oldMaterial
  399. copyPass.uniforms = oldUniforms
  400. copyPass.material.blending = oldBlending
  401. copyPass.material.transparent = oldTransparent
  402. this._renderer.autoClear = oldAutoClear
  403. if (viewport) {
  404. if (!destination) {
  405. this._renderer.setViewport(oldViewport)
  406. this._renderer.setScissor(oldScissor)
  407. this._renderer.setScissorTest(oldScissorTest)
  408. } else {
  409. destination.viewport.copy(oldViewport)
  410. destination.scissor.copy(oldScissor)
  411. destination.scissorTest = oldScissorTest
  412. }
  413. }
  414. this._renderer.setRenderTarget(oldTarget) // todo: active cubeface etc
  415. }
  416. clearColor({r, g, b, a, target, depth = true, stencil = true, viewport}:
  417. {r?: number, g?: number, b?: number, a?: number, target?: IRenderTarget, depth?: boolean, stencil?: boolean, viewport?: Vector4}): void {
  418. const color = this._renderer.getClearColor(new Color())
  419. const alpha = this._renderer.getClearAlpha()
  420. this._renderer.setClearColor(new Color(r ?? color.r, g ?? color.g, b ?? color.b))
  421. this._renderer.setClearAlpha(a ?? alpha)
  422. const lastTarget = this._renderer.getRenderTarget()
  423. const activeCubeFace = this._renderer.getActiveCubeFace()
  424. const activeMipLevel = this._renderer.getActiveMipmapLevel()
  425. const oldViewport = !target ? this._renderer.getViewport(new Vector4()) : target.viewport.clone()
  426. const oldScissor = !target ? this._renderer.getScissor(new Vector4()) : target.scissor.clone()
  427. const oldScissorTest = !target ? this._renderer.getScissorTest() : target.scissorTest
  428. if (viewport) {
  429. if (!target) {
  430. this._renderer.setViewport(viewport)
  431. this._renderer.setScissor(viewport)
  432. this._renderer.setScissorTest(true)
  433. } else {
  434. target.viewport.copy(viewport)
  435. target.scissor.copy(viewport)
  436. target.scissorTest = true
  437. }
  438. }
  439. this._renderer.setRenderTarget((target as WebGLRenderTarget) ?? null)
  440. this._renderer.clear(true, depth, stencil)
  441. if (viewport) {
  442. if (!target) {
  443. this._renderer.setViewport(oldViewport)
  444. this._renderer.setScissor(oldScissor)
  445. this._renderer.setScissorTest(oldScissorTest)
  446. } else {
  447. target.viewport.copy(oldViewport)
  448. target.scissor.copy(oldScissor)
  449. target.scissorTest = oldScissorTest
  450. }
  451. }
  452. this._renderer.setRenderTarget(lastTarget, activeCubeFace, activeMipLevel)
  453. this._renderer.setClearColor(color)
  454. this._renderer.setClearAlpha(alpha)
  455. }
  456. /**
  457. * Converts a render target to a png/jpeg data url string.
  458. * Note: this will clamp the values to [0, 1] and converts to srgb for float and half-float render targets.
  459. * @param target
  460. * @param mimeType
  461. * @param quality
  462. * @param textureIndex - index of the texture to use in the render target (only in case of multiple render target)
  463. */
  464. renderTargetToDataUrl(target: WebGLMultipleRenderTargets|WebGLRenderTarget, mimeType = 'image/png', quality = 90, textureIndex = 0): string {
  465. const canvas = document.createElement('canvas')
  466. canvas.width = target.width
  467. canvas.height = target.height
  468. const ctx = canvas.getContext('2d')
  469. if (!ctx) throw new Error('Unable to get 2d context')
  470. const texture = Array.isArray(target.texture) ? target.texture[textureIndex] : target.texture
  471. const imageData = ctx.createImageData(target.width, target.height, {colorSpace: ['display-p3', 'srgb'].includes(texture.colorSpace) ? <PredefinedColorSpace>texture.colorSpace : undefined})
  472. if (texture.type === HalfFloatType || texture.type === FloatType) {
  473. const buffer = this.renderTargetToBuffer(target, textureIndex)
  474. textureDataToImageData({data: buffer, width: target.width, height: target.height}, texture.colorSpace, imageData) // this handles converting to srgb
  475. } else {
  476. // todo: handle rgbm to srgb conversion?
  477. this._renderer.readRenderTargetPixels(target, 0, 0, target.width, target.height, imageData.data, undefined, textureIndex)
  478. }
  479. ctx.putImageData(imageData, 0, 0)
  480. const string = (texture.flipY ? canvas : canvasFlipY(canvas)).toDataURL(mimeType, quality) // intentionally inverted ternary
  481. canvas.remove()
  482. return string
  483. }
  484. /**
  485. * Rend pixels from a render target into a new Uint8Array|Uint16Array|Float32Array buffer
  486. * @param target - render target to read from
  487. * @param textureIndex - index of the texture to use in the render target (only in case of multiple render target)
  488. */
  489. renderTargetToBuffer(target: WebGLMultipleRenderTargets|WebGLRenderTarget, textureIndex = 0): Uint8Array|Uint16Array|Float32Array {
  490. const texture = Array.isArray(target.texture) ? target.texture[textureIndex] : target.texture
  491. const buffer =
  492. texture.type === HalfFloatType ?
  493. new Uint16Array(target.width * target.height * 4) :
  494. texture.type === FloatType ?
  495. new Float32Array(target.width * target.height * 4) :
  496. new Uint8Array(target.width * target.height * 4)
  497. this._renderer.readRenderTargetPixels(target, 0, 0, target.width, target.height, buffer, undefined, textureIndex)
  498. return buffer
  499. }
  500. /**
  501. * Exports a render target to a blob. The type is automatically picked from exr to png based on the render target.
  502. * @param target - render target to export
  503. * @param mimeType - mime type to use.
  504. * If auto (default), then it will be picked based on the render target type.
  505. * @param textureIndex - index of the texture to use in the render target (only in case of multiple render target)
  506. */
  507. exportRenderTarget(target: WebGLMultipleRenderTargets|WebGLRenderTarget, mimeType = 'auto', textureIndex = 0): BlobExt {
  508. const hdrFormats = ['image/x-exr']
  509. const texture = Array.isArray(target.texture) ? target.texture[textureIndex] : target.texture
  510. let hdr = texture.type === HalfFloatType || texture.type === FloatType
  511. if (mimeType === 'auto') {
  512. mimeType = hdr ? 'image/x-exr' : 'image/png'
  513. }
  514. if (!hdrFormats.includes(mimeType)) hdr = false
  515. let buffer: ArrayBufferLike
  516. if (!hdr) {
  517. const url = this.renderTargetToDataUrl(target, mimeType === 'auto' ? undefined : mimeType, 90, textureIndex)
  518. buffer = base64ToArrayBuffer(url.split(',')[1])
  519. mimeType = url.split(';')[0].split(':')[1]
  520. } else {
  521. if (mimeType !== 'image/x-exr') {
  522. console.warn('RenderManager: mimeType ', mimeType, ' is not supported for HDR. Using EXR instead')
  523. mimeType = 'image/x-exr'
  524. }
  525. const exporter = new EXRExporter2()
  526. buffer = exporter.parse(this._renderer, target, {textureIndex})
  527. }
  528. const b = new Blob([buffer], {type: mimeType}) as BlobExt
  529. b.ext = mimeType === 'image/x-exr' ? 'exr' : mimeType.split('/')[1]
  530. return b
  531. }
  532. // endregion
  533. // region Events Dispatch
  534. private _updated(data?: Partial<IRenderManagerUpdateEvent>) {
  535. this.dispatchEvent({...data, type: 'update'})
  536. }
  537. // endregion
  538. protected _createTargetClass(clazz: Class<WebGLRenderTarget>, size: number[], options: WebGLRenderTargetOptions): IRenderTarget {
  539. const processNewTarget = this._processNewTarget
  540. return new class RenderTarget extends clazz implements IRenderTarget {
  541. isTemporary?: boolean
  542. sizeMultiplier?: number
  543. uuid: string
  544. readonly assetType = 'renderTarget'
  545. name = 'RenderTarget'
  546. // @ts-expect-error because WebGLRenderTarget does not have texture as array
  547. texture: ValOrArr<Texture&{_target: IRenderTarget}>
  548. constructor(public readonly renderManager: IRenderManager, ...ps: any[]) {
  549. super(...ps)
  550. this.uuid = generateUUID()
  551. const ops = ps[ps.length - 1] as WebGLRenderTargetOptions
  552. const colorSpace = ops?.colorSpace
  553. this._initTexture(colorSpace)
  554. }
  555. private _initTexture(colorSpace?: ColorSpace) {
  556. if (Array.isArray(this.texture)) {
  557. this.texture.forEach(t => {
  558. if (colorSpace !== undefined) t.colorSpace = colorSpace
  559. t._target = this
  560. t.toJSON = () => {
  561. console.warn('Multiple render target texture.toJSON not supported yet.')
  562. return {}
  563. }
  564. })
  565. } else {
  566. this.texture._target = this
  567. // if (colorSpace !== undefined) this.texture.colorSpace = colorSpace
  568. this.texture.toJSON = () => ({ // todo use readRenderTargetPixels as data url or data buffer.
  569. isRenderTargetTexture: true,
  570. }) // so that it doesn't get serialized
  571. }
  572. }
  573. setSize(w: number, h: number, depth?: number) {
  574. super.setSize(Math.floor(w), Math.floor(h), depth)
  575. // console.log('setSize', w, h, depth)
  576. return this
  577. }
  578. clone(trackTarget = true): any {
  579. if (this.isTemporary) throw 'Cloning temporary render targets not supported'
  580. if (Array.isArray(this.texture)) throw 'Cloning multiple render targets not supported'
  581. // Note: todo: webgl render target.clone messes up the texture, by not copying isRenderTargetTexture prop and maybe some other stuff. So its better to just create a new one
  582. // const cloned = super.clone() as IRenderTarget
  583. const cloned = new (this.constructor as Class<typeof this>)(this.renderManager)
  584. cloned.copy(this)
  585. cloned._initTexture((Array.isArray(this.texture) ? this.texture[0] : this.texture)?.colorSpace)
  586. const tex = cloned.texture
  587. if (Array.isArray(tex)) tex.forEach(t => t.isRenderTargetTexture = true)
  588. else tex.isRenderTargetTexture = true
  589. return processNewTarget(cloned, this.sizeMultiplier || 1, trackTarget)
  590. }
  591. copy(source: IRenderTarget|WebGLRenderTarget|WebGLMultipleRenderTargets): this {
  592. super.copy(source as any)
  593. return this
  594. }
  595. }(this, ...size, options)
  596. }
  597. /**
  598. * @deprecated use renderScale instead
  599. */
  600. get displayCanvasScaling() {
  601. console.error('displayCanvasScaling is deprecated, use renderScale instead')
  602. return this.renderScale
  603. }
  604. /**
  605. * @deprecated use renderScale instead
  606. */
  607. set displayCanvasScaling(value) {
  608. console.error('displayCanvasScaling is deprecated, use renderScale instead')
  609. this.renderScale = value
  610. }
  611. }