multi-sample.ts 13 KB

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