multi-sample.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. /**
  2. * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. */
  6. import { QuadSchema, QuadValues } from 'mol-gl/compute/util';
  7. import { TextureSpec, UniformSpec, Values } from 'mol-gl/renderable/schema';
  8. import { Texture } from 'mol-gl/webgl/texture';
  9. import { WebGLContext } from 'mol-gl/webgl/context';
  10. import { ValueCell } from 'mol-util';
  11. import { Vec2 } from 'mol-math/linear-algebra';
  12. import { ShaderCode } from 'mol-gl/shader-code';
  13. import { createComputeRenderItem } from 'mol-gl/webgl/render-item';
  14. import { createComputeRenderable, ComputeRenderable } from 'mol-gl/renderable';
  15. import { ParamDefinition as PD } from 'mol-util/param-definition';
  16. import { RenderTarget, createRenderTarget } from 'mol-gl/webgl/render-target';
  17. import { Camera } from 'mol-canvas3d/camera';
  18. import { PostprocessingPass } from './postprocessing';
  19. import { DrawPass } from './draw';
  20. const ComposeSchema = {
  21. ...QuadSchema,
  22. tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
  23. uTexSize: UniformSpec('v2'),
  24. uWeight: UniformSpec('f'),
  25. }
  26. type ComposeRenderable = ComputeRenderable<Values<typeof ComposeSchema>>
  27. function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture): ComposeRenderable {
  28. const values: Values<typeof ComposeSchema> = {
  29. ...QuadValues,
  30. tColor: ValueCell.create(colorTexture),
  31. uTexSize: ValueCell.create(Vec2.create(colorTexture.width, colorTexture.height)),
  32. uWeight: ValueCell.create(1.0),
  33. }
  34. const schema = { ...ComposeSchema }
  35. const shaderCode = ShaderCode(
  36. require('mol-gl/shader/quad.vert').default,
  37. require('mol-gl/shader/compose.frag').default
  38. )
  39. const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values)
  40. return createComputeRenderable(renderItem, values)
  41. }
  42. export const MultiSampleParams = {
  43. mode: PD.Select('off', [['off', 'Off'], ['on', 'On'], ['temporal', 'Temporal']]),
  44. sampleLevel: PD.Numeric(2, { min: 0, max: 5, step: 1 }),
  45. }
  46. export type MultiSampleProps = PD.Values<typeof MultiSampleParams>
  47. export class MultiSamplePass {
  48. props: MultiSampleProps
  49. private composeTarget: RenderTarget
  50. private holdTarget: RenderTarget
  51. private compose: ComposeRenderable
  52. private sampleIndex = -1
  53. private currentTime = 0
  54. private lastRenderTime = 0
  55. constructor(private webgl: WebGLContext, private camera: Camera, private drawPass: DrawPass, private postprocessing: PostprocessingPass, props: Partial<MultiSampleProps>) {
  56. const { gl } = webgl
  57. this.composeTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight)
  58. this.holdTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight)
  59. this.compose = getComposeRenderable(webgl, drawPass.colorTarget.texture)
  60. this.props = { ...PD.getDefaultValues(MultiSampleParams), ...props }
  61. }
  62. get enabled() {
  63. if (this.props.mode === 'temporal') {
  64. if (this.currentTime - this.lastRenderTime > 200) {
  65. return this.sampleIndex !== -1
  66. } else {
  67. this.sampleIndex = 0
  68. return false
  69. }
  70. } else if (this.props.mode === 'on') {
  71. return true
  72. } else {
  73. return false
  74. }
  75. }
  76. update(changed: boolean, currentTime: number) {
  77. if (changed) this.lastRenderTime = currentTime
  78. this.currentTime = currentTime
  79. }
  80. setSize(width: number, height: number) {
  81. this.composeTarget.setSize(width, height)
  82. this.holdTarget.setSize(width, height)
  83. ValueCell.update(this.compose.values.uTexSize, Vec2.set(this.compose.values.uTexSize.ref.value, width, height))
  84. }
  85. setProps(props: Partial<MultiSampleProps>) {
  86. if (props.mode !== undefined) this.props.mode = props.mode
  87. if (props.sampleLevel !== undefined) this.props.sampleLevel = props.sampleLevel
  88. }
  89. render() {
  90. if (this.props.mode === 'temporal') {
  91. this.renderTemporalMultiSample()
  92. } else {
  93. this.renderMultiSample()
  94. }
  95. }
  96. private renderMultiSample() {
  97. const { camera, compose, composeTarget, drawPass, postprocessing, webgl } = this
  98. const { gl, state } = webgl
  99. // based on the Multisample Anti-Aliasing Render Pass
  100. // contributed to three.js by bhouston / http://clara.io/
  101. //
  102. // This manual approach to MSAA re-renders the scene once for
  103. // each sample with camera jitter and accumulates the results.
  104. const offsetList = JitterVectors[ Math.max(0, Math.min(this.props.sampleLevel, 5)) ]
  105. const baseSampleWeight = 1.0 / offsetList.length
  106. const roundingRange = 1 / 32
  107. camera.viewOffset.enabled = true
  108. ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawPass.colorTarget.texture)
  109. compose.update()
  110. const { width, height } = drawPass.colorTarget
  111. // render the scene multiple times, each slightly jitter offset
  112. // from the last and accumulate the results.
  113. for (let i = 0; i < offsetList.length; ++i) {
  114. const offset = offsetList[i]
  115. Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height)
  116. camera.updateMatrices()
  117. // the theory is that equal weights for each sample lead to an accumulation of rounding
  118. // errors. The following equation varies the sampleWeight per sample so that it is uniformly
  119. // distributed across a range of values whose rounding errors cancel each other out.
  120. const uniformCenteredDistribution = -0.5 + (i + 0.5) / offsetList.length
  121. const sampleWeight = baseSampleWeight + roundingRange * uniformCenteredDistribution
  122. ValueCell.update(compose.values.uWeight, sampleWeight)
  123. // render scene and optionally postprocess
  124. drawPass.render(false)
  125. if (postprocessing.enabled) postprocessing.render(false)
  126. // compose rendered scene with compose target
  127. composeTarget.bind()
  128. gl.viewport(0, 0, width, height)
  129. state.enable(gl.BLEND)
  130. state.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD)
  131. state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE)
  132. state.disable(gl.DEPTH_TEST)
  133. state.disable(gl.SCISSOR_TEST)
  134. state.depthMask(false)
  135. if (i === 0) {
  136. state.clearColor(0, 0, 0, 0)
  137. gl.clear(gl.COLOR_BUFFER_BIT)
  138. }
  139. compose.render()
  140. }
  141. ValueCell.update(compose.values.uWeight, 1.0)
  142. ValueCell.update(compose.values.tColor, composeTarget.texture)
  143. compose.update()
  144. webgl.unbindFramebuffer()
  145. gl.viewport(0, 0, width, height)
  146. state.disable(gl.BLEND)
  147. compose.render()
  148. camera.viewOffset.enabled = false
  149. camera.updateMatrices()
  150. }
  151. private renderTemporalMultiSample() {
  152. const { camera, compose, composeTarget, holdTarget, postprocessing, drawPass, webgl } = this
  153. const { gl, state } = webgl
  154. // based on the Multisample Anti-Aliasing Render Pass
  155. // contributed to three.js by bhouston / http://clara.io/
  156. //
  157. // This manual approach to MSAA re-renders the scene once for
  158. // each sample with camera jitter and accumulates the results.
  159. const offsetList = JitterVectors[ Math.max(0, Math.min(this.props.sampleLevel, 5)) ]
  160. if (this.sampleIndex === -1) return
  161. if (this.sampleIndex >= offsetList.length) {
  162. this.sampleIndex = -1
  163. return
  164. }
  165. const i = this.sampleIndex
  166. if (i === 0) {
  167. drawPass.render(false)
  168. if (postprocessing.enabled) postprocessing.render(false)
  169. ValueCell.update(compose.values.uWeight, 1.0)
  170. ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawPass.colorTarget.texture)
  171. compose.update()
  172. holdTarget.bind()
  173. state.disable(gl.BLEND)
  174. compose.render()
  175. }
  176. const sampleWeight = 1.0 / offsetList.length
  177. camera.viewOffset.enabled = true
  178. ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawPass.colorTarget.texture)
  179. ValueCell.update(compose.values.uWeight, sampleWeight)
  180. compose.update()
  181. const { width, height } = drawPass.colorTarget
  182. // render the scene multiple times, each slightly jitter offset
  183. // from the last and accumulate the results.
  184. const numSamplesPerFrame = Math.pow(2, this.props.sampleLevel)
  185. for (let i = 0; i < numSamplesPerFrame; ++i) {
  186. const offset = offsetList[this.sampleIndex]
  187. Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height)
  188. camera.updateMatrices()
  189. // render scene and optionally postprocess
  190. drawPass.render(false)
  191. if (postprocessing.enabled) postprocessing.render(false)
  192. // compose rendered scene with compose target
  193. composeTarget.bind()
  194. state.enable(gl.BLEND)
  195. state.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD)
  196. state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE)
  197. state.disable(gl.DEPTH_TEST)
  198. state.disable(gl.SCISSOR_TEST)
  199. state.depthMask(false)
  200. if (this.sampleIndex === 0) {
  201. state.clearColor(0, 0, 0, 0)
  202. gl.clear(gl.COLOR_BUFFER_BIT)
  203. }
  204. compose.render()
  205. this.sampleIndex += 1
  206. if (this.sampleIndex >= offsetList.length ) break
  207. }
  208. const accumulationWeight = this.sampleIndex * sampleWeight
  209. if (accumulationWeight > 0) {
  210. ValueCell.update(compose.values.uWeight, 1.0)
  211. ValueCell.update(compose.values.tColor, composeTarget.texture)
  212. compose.update()
  213. webgl.unbindFramebuffer()
  214. gl.viewport(0, 0, width, height)
  215. state.disable(gl.BLEND)
  216. compose.render()
  217. }
  218. if (accumulationWeight < 1.0) {
  219. ValueCell.update(compose.values.uWeight, 1.0 - accumulationWeight)
  220. ValueCell.update(compose.values.tColor, holdTarget.texture)
  221. compose.update()
  222. webgl.unbindFramebuffer()
  223. gl.viewport(0, 0, width, height)
  224. if (accumulationWeight === 0) state.disable(gl.BLEND)
  225. else state.enable(gl.BLEND)
  226. compose.render()
  227. }
  228. camera.viewOffset.enabled = false
  229. camera.updateMatrices()
  230. if (this.sampleIndex >= offsetList.length) this.sampleIndex = -1
  231. }
  232. }
  233. const JitterVectors = [
  234. [
  235. [ 0, 0 ]
  236. ],
  237. [
  238. [ 4, 4 ], [ -4, -4 ]
  239. ],
  240. [
  241. [ -2, -6 ], [ 6, -2 ], [ -6, 2 ], [ 2, 6 ]
  242. ],
  243. [
  244. [ 1, -3 ], [ -1, 3 ], [ 5, 1 ], [ -3, -5 ],
  245. [ -5, 5 ], [ -7, -1 ], [ 3, 7 ], [ 7, -7 ]
  246. ],
  247. [
  248. [ 1, 1 ], [ -1, -3 ], [ -3, 2 ], [ 4, -1 ],
  249. [ -5, -2 ], [ 2, 5 ], [ 5, 3 ], [ 3, -5 ],
  250. [ -2, 6 ], [ 0, -7 ], [ -4, -6 ], [ -6, 4 ],
  251. [ -8, 0 ], [ 7, -4 ], [ 6, 7 ], [ -7, -8 ]
  252. ],
  253. [
  254. [ -4, -7 ], [ -7, -5 ], [ -3, -5 ], [ -5, -4 ],
  255. [ -1, -4 ], [ -2, -2 ], [ -6, -1 ], [ -4, 0 ],
  256. [ -7, 1 ], [ -1, 2 ], [ -6, 3 ], [ -3, 3 ],
  257. [ -7, 6 ], [ -3, 6 ], [ -5, 7 ], [ -1, 7 ],
  258. [ 5, -7 ], [ 1, -6 ], [ 6, -5 ], [ 4, -4 ],
  259. [ 2, -3 ], [ 7, -2 ], [ 1, -1 ], [ 4, -1 ],
  260. [ 2, 1 ], [ 6, 2 ], [ 0, 4 ], [ 4, 4 ],
  261. [ 2, 5 ], [ 7, 5 ], [ 5, 6 ], [ 3, 7 ]
  262. ]
  263. ]
  264. JitterVectors.forEach(offsetList => {
  265. offsetList.forEach(offset => {
  266. // 0.0625 = 1 / 16
  267. offset[0] *= 0.0625
  268. offset[1] *= 0.0625
  269. })
  270. })