multi-sample.ts 14 KB

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