multi-sample.ts 14 KB

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