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

ThreeGpuPathTracerPlugin.ts 17KB

11 месяцев назад
11 месяцев назад
11 месяцев назад
11 месяцев назад
11 месяцев назад
11 месяцев назад
11 месяцев назад
11 месяцев назад
11 месяцев назад
11 месяцев назад
11 месяцев назад
11 месяцев назад
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. import {
  2. AViewerPluginSync,
  3. Euler,
  4. NoColorSpace,
  5. onChange,
  6. ProgressivePlugin,
  7. Scene,
  8. serialize,
  9. ThreeViewer,
  10. uiButton,
  11. uiConfig,
  12. uiFolderContainer,
  13. uiSlider,
  14. uiToggle,
  15. } from 'threepipe'
  16. import {WebGLPathTracer} from 'three-gpu-pathtracer'
  17. import {WebGLPathTracer2} from './WebGLPathTracer2'
  18. // @ts-expect-error polyfill for new threejs
  19. Scene.prototype.backgroundRotation = new Euler(0, 0, 0, 'XYZ')
  20. // @ts-expect-error polyfill
  21. Scene.prototype.environmentRotation = new Euler(0, 0, 0, 'XYZ')
  22. /**
  23. * ThreeGpuPathTracerPlugin
  24. *
  25. * Path Tracing plugin for Threepipe using [three-gpu-pathtracer](https://github.com/gkjohnson/three-gpu-pathtracer).
  26. *
  27. * This plugin allows for GPU-accelerated path tracing in Three.js scenes. (using webgl2)
  28. *
  29. * It provides options to configure the path tracing parameters such as bounces, samples per frame, and more.
  30. * It also integrates with the Three.js scene and camera, updating materials and lights as needed.
  31. * Serialization and deserialization are supported for plugin state management.
  32. * It listens to scene updates, camera changes, and material updates to refresh the path tracing setup.
  33. * It can be enabled or disabled, and it automatically handles rendering to the screen or to a texture.
  34. * It supports progressive rendering, allowing for a smooth transition of rendered frames.
  35. *
  36. */
  37. @uiFolderContainer('Path Tracing', {expanded: false})
  38. export class ThreeGpuPathTracerPlugin extends AViewerPluginSync {
  39. public static readonly PluginType = 'ThreeGpuPathTracerPlugin'
  40. @uiToggle()
  41. @serialize()
  42. @onChange(ThreeGpuPathTracerPlugin.prototype._enableChange)
  43. enabled = true
  44. // public ptMaterial: any
  45. // public ptGenerator: any
  46. // public sceneInfo: any
  47. // @uiSlider('Environment Multiplier', [0, 10], 0.01)
  48. // @serialize()
  49. // @onChange(ThreeGpuPathTracerPlugin.prototype.reset)
  50. // environmentMultiplier = 1
  51. //
  52. // @uiSlider('Max Iterations', [0, 10000], 1)
  53. // @serialize()
  54. // maxIterations = 100
  55. //
  56. // @uiSlider('Bounces', [1, 30], 1)
  57. // @serialize()
  58. // @onChange(ThreeGpuPathTracerPlugin.prototype.reset)
  59. // bounces = 10
  60. //
  61. // @uiSlider('filterGlossyFactor', [0, 1], 0.01)
  62. // @serialize()
  63. // @onChange(ThreeGpuPathTracerPlugin.prototype.reset)
  64. // filterGlossyFactor = 0.25
  65. // @uiSlider('environmentBlur', [0, 1], 0.01)
  66. // @onChange(ThreeGpuPathTracerPlugin.prototype.reset)
  67. // environmentBlur = 0.0
  68. @uiSlider('maxFrameCount', [16, 10000], 1)
  69. get maxFrameCount(): number {
  70. return this._viewer?.getPlugin(ProgressivePlugin)?.maxFrameCount ?? 32
  71. }
  72. set maxFrameCount(value: number) {
  73. const p = this._viewer?.getPlugin(ProgressivePlugin)
  74. if (!p) {
  75. console.warn('ThreeGpuPathTracerPlugin: ProgressivePlugin not found, cannot set maxFrameCount')
  76. return
  77. }
  78. p.maxFrameCount = value
  79. }
  80. @uiSlider('samplesPerFrame', [1, 5], 1)
  81. samplesPerFrame = 1
  82. @uiConfig(undefined, {expanded: true})
  83. @serialize()
  84. tracer: WebGLPathTracer | undefined
  85. // @uiSlider('tiles', [1, 5], 1)
  86. // tiles = 2
  87. // @uiMonitor()
  88. // private _lastEnvMap: any = null
  89. // private _lastBgMap: any = null
  90. dependencies = [ProgressivePlugin]
  91. constructor(enabled = true) {
  92. super()
  93. this.enabled = enabled
  94. this.refreshScene = this.refreshScene.bind(this)
  95. this.reset = this.reset.bind(this)
  96. this._enableChange = this._enableChange.bind(this)
  97. this._refreshScene = this._refreshScene.bind(this)
  98. }
  99. private async _enableChange() {
  100. if (!this.isDisabled()) this.refreshScene()
  101. this.setDirty()
  102. }
  103. setDirty() {
  104. if (!this._viewer) return
  105. this._viewer.renderManager.defaultRenderToScreen = this.isDisabled()
  106. this._viewer.setDirty()
  107. }
  108. // todo: onremove and ondispose (dispose sceneInfo in that and remove event listeners)
  109. onAdded(viewer: ThreeViewer) {
  110. super.onAdded(viewer)
  111. // removed
  112. // if (viewer.renderer.useLegacyLights) {
  113. // viewer.console.warn('Set viewer.renderer.useLegacyLights to false to get consistent lighting')
  114. // }
  115. // todo diamond
  116. // if (viewer.getPluginByType<any>('Diamond'))
  117. // viewer.getPluginByType<any>('Diamond').forceSceneEnvMap = true // we dont have separate env map support in path tracing
  118. this.tracer = new WebGLPathTracer2(viewer.renderManager.webglRenderer)
  119. this.tracer.setScene(viewer.scene, viewer.scene.mainCamera) // todo: handle active camera change
  120. // this.ptMaterial = new PhysicalPathTracingMaterial()
  121. // this.ptRenderer.alpha = !this._viewer?.getBackground() // for transparent background
  122. // this.ptRenderer.camera = viewer.scene.activeCamera.cameraObject
  123. // this.ptRenderer.material = this.ptMaterial
  124. // this.ptRenderer.setSize(viewer.renderer.renderSize.width, viewer.renderer.renderSize.height)
  125. // this.ptRenderer.tiles.set(this.tiles, this.tiles)
  126. viewer.scene.addEventListener('sceneUpdate', () => {
  127. this.refreshScene()
  128. })
  129. viewer.scene.addEventListener('mainCameraChange', () => {
  130. this.refreshScene()
  131. })
  132. viewer.scene.addEventListener('materialUpdate', () => {
  133. if (this.isDisabled() || !this.tracer || this._refreshing) return
  134. this.tracer.updateMaterials() // todo do post frame?
  135. this.reset()
  136. })
  137. viewer.scene.addEventListener('environmentChanged', () => {
  138. if (this.isDisabled() || !this.tracer || this._refreshing) return
  139. this.tracer.updateEnvironment() // todo do post frame?
  140. this.reset()
  141. })
  142. viewer.renderManager.addEventListener('resize', () => {
  143. if (this.isDisabled() || !this.tracer || this._refreshing) return
  144. // this.ptRenderer.setSize(viewer.renderer.renderSize.width, viewer.renderer.renderSize.height)
  145. // if (this._refreshing) return
  146. // console.log('resize')
  147. this.tracer.updateCamera()// todo do post frame?
  148. this.reset()
  149. })
  150. viewer.scene.mainCamera.addEventListener('update', () => {
  151. if (this.isDisabled() || !this.tracer || this._refreshing) return
  152. this.tracer.updateCamera()// todo do post frame?
  153. this.reset()
  154. })
  155. viewer.addEventListener('preFrame', () => {
  156. if (this.isDisabled() || this._refreshing || !this._viewer || !this.tracer) return
  157. // if (viewer.getPlugin(ProgressivePlugin)?.isConverged(true)) {
  158. // todo on no rasterize don't render rasterize pipeline
  159. const rasterize = viewer.renderManager.frameCount < 16 - 1 // min 16 required for path tracing to show up
  160. viewer.renderManager.defaultRenderToScreen = rasterize
  161. if (viewer.renderManager.frameCount > 0 && viewer.renderManager.frameCount < this.maxFrameCount) {
  162. // const outline = viewer.getPluginByType<OutlinePlugin>('Outline')
  163. // if (outline) outline.mouseInOutAnimationEnabled = false
  164. if (this._sceneNeedsRefresh) {
  165. this._refreshScene()
  166. return
  167. }
  168. //
  169. // if (!this.ptMaterial.bvh.bvhBounds.source.data.data) {
  170. // return
  171. // }
  172. this._viewer.renderEnabled = false
  173. // this.ptRenderer.material.environmentMap = (viewer.renderManager.webglRenderer as any).cubeuvmaps
  174. // const env = viewer.scene.getEnvironment()
  175. // let bg = viewer.scene.getBackground() as any
  176. // if (!bg || !bg.isTexture || bg === env) bg = null
  177. // if (this._lastEnvMap !== env) {
  178. // this._lastEnvMap = env
  179. // this.ptRenderer.material.envMapInfo.updateFrom(env)
  180. // }
  181. // if (this._lastBgMap !== bg) {
  182. // this._lastBgMap = bg
  183. // this.ptRenderer.material.backgroundMap = bg
  184. // }
  185. // // viewer.getPlugin(DiamondPlugin)?.envMap || viewer.scene.getEnvironment()
  186. // // this.ptRenderer.material.environmentMap = viewer.scene.getEnvironment()
  187. // this.ptRenderer.material.environmentRotation.makeRotationY(viewer.scene.getEnvironment()?.rotation || 0)
  188. // // console.log(this.ptRenderer.material.environmentMap)
  189. // // console.log(viewer.getPlugin(DiamondPlugin)?.envMap)
  190. //
  191. // this.ptRenderer.material.filterGlossyFactor = this.filterGlossyFactor
  192. // this.ptRenderer.material.environmentIntensity = (viewer.scene.envMapIntensity || 1) * this.environmentMultiplier
  193. // this.ptRenderer.material.bounces = this.bounces
  194. // this.ptRenderer.camera.updateMatrixWorld()
  195. //
  196. // this.ptRenderer.camera.clearViewOffset()
  197. //
  198. // this._updateDiamondMaterials(this._modelRoot())
  199. //
  200. const encoding = viewer.renderManager.webglRenderer.outputColorSpace
  201. viewer.renderManager.webglRenderer.outputColorSpace = NoColorSpace
  202. // const pcl = (viewer.renderManager.webglRenderer as any).useLegacyLights;
  203. // (viewer.renderManager.webglRenderer as any).useLegacyLights = true
  204. for (let i = 0, l = this.samplesPerFrame; i < l; i++)
  205. this.tracer.renderSample()
  206. // ;(viewer.renderManager.webglRenderer as any).useLegacyLights = pcl
  207. viewer.renderManager.webglRenderer.outputColorSpace = encoding
  208. // this.ptRenderer.camera.clearViewOffset() // todo?
  209. // viewer.renderManager.webglRenderer.autoClear = false
  210. // ;(fsQuad.material as any).map = this.ptRenderer.target.texture
  211. // fsQuad.render(viewer.renderManager.webglRenderer)
  212. // viewer.renderManager.webglRenderer.autoClear = true
  213. // todo
  214. // const combinedPost = this._viewer.getPlugin(CombinedPostPlugin)!
  215. if (!rasterize) {
  216. const combinedPass = viewer.renderManager.screenPass
  217. // if (this._iterationCount < 10 || this._iterationCount > 5 && this._iterationCount % 5 === 0) {
  218. combinedPass.render(viewer.renderManager.renderer, null, this.tracer.target, 0, false)
  219. viewer.renderManager.incRenderToScreen()
  220. }
  221. // }
  222. // viewer.renderer.blit(this.ptRenderer.target.texture, undefined)
  223. // console.log(this.ptRenderer.target.texture)
  224. this._viewer.renderEnabled = true
  225. }
  226. })
  227. }
  228. private _refreshing = false
  229. private _sceneNeedsRefresh = false
  230. @uiButton()
  231. refreshScene() {
  232. this._sceneNeedsRefresh = true
  233. }
  234. // private readonly _diaPhysicalMaterial = new MeshPhysicalMaterial({
  235. // color: 0xffffff,
  236. // metalness: 0,
  237. // roughness: 0,
  238. // fog: false,
  239. // transmission: 1.0,
  240. // opacity: 1,
  241. // transparent: true,
  242. // thickness: 0,
  243. // attenuationDistance: 100,
  244. // })
  245. // private readonly _diaMatMap: Record<string, MeshPhysicalMaterial> = {}
  246. // private _modelRoot = ()=>this._viewer?.scene.modelRoot.modelObject
  247. private async _refreshScene() {
  248. if (!this._viewer) return
  249. if (this.isDisabled() || !this.tracer) return
  250. if (!this._sceneNeedsRefresh) return
  251. if (this._refreshing) {
  252. console.warn('ThreeGpuPathTracerPlugin: refreshing already')
  253. return
  254. }
  255. // const root = this._modelRoot()
  256. //
  257. // if (!root || !root.children.length) return
  258. this._sceneNeedsRefresh = false
  259. this._viewer.renderEnabled = false
  260. // // if (this.sceneInfo) {
  261. // // this.sceneInfo.bvh.geometry.dispose()
  262. // // this.sceneInfo.bvh.geometry.disposeBoundsTree?.() // todo: check what this is and if required
  263. // // this.sceneInfo = undefined
  264. // // }
  265. // this.reset()
  266. //
  267. // let geoms = 0
  268. // root.traverse((node:any)=>{
  269. // if (node.geometry) geoms++
  270. // })
  271. // if (geoms < 1) return
  272. //
  273. this._refreshing = true
  274. const viewer = this._viewer
  275. const async = false // todo needs bvh worker?
  276. if (async) {
  277. await this.tracer.setSceneAsync(viewer.scene, viewer.scene.mainCamera, {
  278. onProgress: (progress)=>{
  279. console.log('Path Tracing Scene Generation Progress:', progress)
  280. },
  281. })
  282. } else {
  283. this.tracer.setScene(viewer.scene, viewer.scene.mainCamera, {
  284. onProgress: (progress) => {
  285. console.log('Path Tracing Scene Generation Progress:', progress)
  286. },
  287. })
  288. }
  289. //
  290. // const generator = new PathTracingSceneGenerator()
  291. // this._updateDiamondMaterials(root)
  292. //
  293. // const matMap: any = {}
  294. //
  295. // root.traverse((node: any) => {
  296. // if (node.material?.isDiamondMaterial) {
  297. // matMap[node.uuid] = node.material
  298. // node.material = this._diaMatMap[this._diamondMaterialKey(node.material)]
  299. // }
  300. // })
  301. //
  302. // const result = generator.generate([root, ...this._viewer!.scene.children.filter(c=>(c as any)?.isLight)] as any)
  303. // // const result = generator.generate(root as any)
  304. //
  305. // root.traverse((node: any) => {
  306. // if (matMap[node.uuid]) {
  307. // node.material = matMap[node.uuid]
  308. // }
  309. // })
  310. //
  311. // this.sceneInfo = result
  312. // const geometry = result.bvh.geometry
  313. // // update bvh and geometry attribute textures
  314. // this.ptMaterial.bvh.updateFrom(result.bvh)
  315. // this.ptMaterial.attributesArray.updateFrom(
  316. // geometry.attributes.normal,
  317. // geometry.attributes.tangent,
  318. // geometry.attributes.uv,
  319. // geometry.attributes.color,
  320. // )
  321. // // this.ptMaterial.normalAttribute.updateFrom(geometry.attributes.normal)
  322. // // this.ptMaterial.tangentAttribute.updateFrom(geometry.attributes.tangent)
  323. // // this.ptMaterial.uvAttribute.updateFrom(geometry.attributes.uv)
  324. // // this.ptMaterial.colorAttribute.updateFrom(geometry.attributes.color)
  325. //
  326. // // console.log(this.ptMaterial, result.materials)
  327. //
  328. // // update lights
  329. // this.ptMaterial.lights.updateFrom(result.lights)
  330. // // this.ptMaterial.lightCount = result.lights.length
  331. //
  332. // // update materials and texture arrays
  333. // this.ptMaterial.materialIndexAttribute.updateFrom(geometry.attributes.materialIndex)
  334. // this.ptMaterial.textures.setTextures(this._viewer.renderManager.webglRenderer, 2048, 2048, result.textures)
  335. //
  336. // // this is done before render also
  337. // this.ptMaterial.materials.updateFrom(result.materials, result.textures)
  338. //
  339. // result.matera
  340. //
  341. // // generator.dispose()
  342. //
  343. this._viewer.renderEnabled = true
  344. this._refreshing = false
  345. }
  346. // private _updateDiamondMaterials(root?: Object3D) {
  347. // root?.traverse((node: any) => {
  348. // if (node.material?.isDiamondMaterial) {
  349. // const matKey = this._diamondMaterialKey(node.material)
  350. // if (!this._diaMatMap[matKey]) {
  351. // this._diaMatMap[matKey] = this._diaPhysicalMaterial.clone()
  352. // }
  353. // this._diaMatMap[matKey].color.set(node.material.color)
  354. // this._diaMatMap[matKey].ior = node.material.refractiveIndex
  355. // }
  356. // })
  357. // }
  358. //
  359. // private _diamondMaterialKey = (mat: any)=>mat.color.getHexString() + mat.refractiveIndex
  360. reset() {
  361. if (this.isDisabled() || !this.tracer) return
  362. this.tracer.reset()
  363. this.setDirty()
  364. }
  365. dispose() {
  366. super.dispose()
  367. if (this.tracer) { // todo do on plugin remove
  368. this.tracer.dispose()
  369. // this.tracer = undefined
  370. }
  371. }
  372. }