multi-sample.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. /**
  2. * Copyright (c) 2019-2022 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 { MarkingProps } from './marking';
  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('temporal', [['off', 'Off'], ['on', 'On'], ['temporal', 'Temporal']]),
  48. sampleLevel: PD.Numeric(2, { min: 0, max: 5, step: 1 }, { description: 'Take level^2 samples.' }),
  49. };
  50. export type MultiSampleProps = PD.Values<typeof MultiSampleParams>
  51. type Props = {
  52. multiSample: MultiSampleProps
  53. postprocessing: PostprocessingProps
  54. marking: MarkingProps
  55. transparentBackground: boolean;
  56. }
  57. type RenderContext = {
  58. renderer: Renderer;
  59. camera: Camera | StereoCamera;
  60. scene: Scene;
  61. helper: Helper;
  62. }
  63. export class MultiSamplePass {
  64. static isEnabled(props: MultiSampleProps) {
  65. return props.mode !== 'off';
  66. }
  67. colorTarget: RenderTarget;
  68. private composeTarget: RenderTarget;
  69. private holdTarget: RenderTarget;
  70. private compose: ComposeRenderable;
  71. constructor(private webgl: WebGLContext, private drawPass: DrawPass) {
  72. const { colorBufferFloat, textureFloat, colorBufferHalfFloat, textureHalfFloat } = webgl.extensions;
  73. const width = drawPass.colorTarget.getWidth();
  74. const height = drawPass.colorTarget.getHeight();
  75. this.colorTarget = webgl.createRenderTarget(width, height, false);
  76. const type = colorBufferHalfFloat && textureHalfFloat ? 'fp16' :
  77. colorBufferFloat && textureFloat ? 'float32' : 'uint8';
  78. this.composeTarget = webgl.createRenderTarget(width, height, false, type);
  79. this.holdTarget = webgl.createRenderTarget(width, height, false);
  80. this.compose = getComposeRenderable(webgl, drawPass.colorTarget.texture);
  81. }
  82. syncSize() {
  83. const width = this.drawPass.colorTarget.getWidth();
  84. const height = this.drawPass.colorTarget.getHeight();
  85. const [w, h] = this.compose.values.uTexSize.ref.value;
  86. if (width !== w || height !== h) {
  87. this.colorTarget.setSize(width, height);
  88. this.composeTarget.setSize(width, height);
  89. this.holdTarget.setSize(width, height);
  90. ValueCell.update(this.compose.values.uTexSize, Vec2.set(this.compose.values.uTexSize.ref.value, width, height));
  91. }
  92. }
  93. render(sampleIndex: number, ctx: RenderContext, props: Props, toDrawingBuffer: boolean, forceOn: boolean) {
  94. if (props.multiSample.mode === 'temporal' && !forceOn) {
  95. return this.renderTemporalMultiSample(sampleIndex, ctx, props, toDrawingBuffer);
  96. } else {
  97. this.renderMultiSample(ctx, toDrawingBuffer, props);
  98. return -2;
  99. }
  100. }
  101. private bindOutputTarget(toDrawingBuffer: boolean) {
  102. if (toDrawingBuffer) {
  103. this.webgl.unbindFramebuffer();
  104. } else {
  105. this.colorTarget.bind();
  106. }
  107. }
  108. private renderMultiSample(ctx: RenderContext, toDrawingBuffer: boolean, props: Props) {
  109. const { camera } = ctx;
  110. const { compose, composeTarget, drawPass, webgl } = this;
  111. const { gl, state } = webgl;
  112. // based on the Multisample Anti-Aliasing Render Pass
  113. // contributed to three.js by bhouston / http://clara.io/
  114. //
  115. // This manual approach to MSAA re-renders the scene once for
  116. // each sample with camera jitter and accumulates the results.
  117. const offsetList = JitterVectors[Math.max(0, Math.min(props.multiSample.sampleLevel, 5))];
  118. const { x, y, width, height } = camera.viewport;
  119. const baseSampleWeight = 1.0 / offsetList.length;
  120. const roundingRange = 1 / 32;
  121. camera.viewOffset.enabled = true;
  122. ValueCell.update(compose.values.tColor, drawPass.getColorTarget(props.postprocessing).texture);
  123. compose.update();
  124. // render the scene multiple times, each slightly jitter offset
  125. // from the last and accumulate the results.
  126. for (let i = 0; i < offsetList.length; ++i) {
  127. const offset = offsetList[i];
  128. Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height);
  129. camera.update();
  130. // the theory is that equal weights for each sample lead to an accumulation of rounding
  131. // errors. The following equation varies the sampleWeight per sample so that it is uniformly
  132. // distributed across a range of values whose rounding errors cancel each other out.
  133. const uniformCenteredDistribution = -0.5 + (i + 0.5) / offsetList.length;
  134. const sampleWeight = baseSampleWeight + roundingRange * uniformCenteredDistribution;
  135. ValueCell.update(compose.values.uWeight, sampleWeight);
  136. // render scene
  137. if (i === 0) {
  138. drawPass.postprocessing.setOcclusionOffset(0, 0);
  139. } else {
  140. drawPass.postprocessing.setOcclusionOffset(
  141. offset[0] / width,
  142. offset[1] / height
  143. );
  144. }
  145. drawPass.render(ctx, props, false);
  146. // compose rendered scene with compose target
  147. composeTarget.bind();
  148. state.enable(gl.BLEND);
  149. state.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
  150. state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
  151. state.disable(gl.DEPTH_TEST);
  152. state.depthMask(false);
  153. gl.viewport(x, y, width, height);
  154. gl.scissor(x, y, width, height);
  155. if (i === 0) {
  156. state.clearColor(0, 0, 0, 0);
  157. gl.clear(gl.COLOR_BUFFER_BIT);
  158. }
  159. compose.render();
  160. }
  161. drawPass.postprocessing.setOcclusionOffset(0, 0);
  162. ValueCell.update(compose.values.uWeight, 1.0);
  163. ValueCell.update(compose.values.tColor, composeTarget.texture);
  164. compose.update();
  165. this.bindOutputTarget(toDrawingBuffer);
  166. gl.viewport(x, y, width, height);
  167. gl.scissor(x, y, width, height);
  168. state.disable(gl.BLEND);
  169. compose.render();
  170. camera.viewOffset.enabled = false;
  171. camera.update();
  172. }
  173. private renderTemporalMultiSample(sampleIndex: number, ctx: RenderContext, props: Props, toDrawingBuffer: boolean) {
  174. const { camera } = ctx;
  175. const { compose, composeTarget, holdTarget, drawPass, webgl } = this;
  176. const { gl, state } = webgl;
  177. // based on the Multisample Anti-Aliasing Render Pass
  178. // contributed to three.js by bhouston / http://clara.io/
  179. //
  180. // This manual approach to MSAA re-renders the scene once for
  181. // each sample with camera jitter and accumulates the results.
  182. const offsetList = JitterVectors[Math.max(0, Math.min(props.multiSample.sampleLevel, 5))];
  183. if (sampleIndex === -2 || sampleIndex >= offsetList.length) return -2;
  184. const { x, y, width, height } = camera.viewport;
  185. const sampleWeight = 1.0 / offsetList.length;
  186. if (sampleIndex === -1) {
  187. drawPass.render(ctx, props, false);
  188. ValueCell.update(compose.values.uWeight, 1.0);
  189. ValueCell.update(compose.values.tColor, drawPass.getColorTarget(props.postprocessing).texture);
  190. compose.update();
  191. holdTarget.bind();
  192. state.disable(gl.BLEND);
  193. state.disable(gl.DEPTH_TEST);
  194. state.depthMask(false);
  195. gl.viewport(x, y, width, height);
  196. gl.scissor(x, y, width, height);
  197. compose.render();
  198. sampleIndex += 1;
  199. } else {
  200. camera.viewOffset.enabled = true;
  201. ValueCell.update(compose.values.tColor, drawPass.getColorTarget(props.postprocessing).texture);
  202. ValueCell.update(compose.values.uWeight, sampleWeight);
  203. compose.update();
  204. // render the scene multiple times, each slightly jitter offset
  205. // from the last and accumulate the results.
  206. const numSamplesPerFrame = Math.pow(2, Math.max(0, props.multiSample.sampleLevel - 2));
  207. for (let i = 0; i < numSamplesPerFrame; ++i) {
  208. const offset = offsetList[sampleIndex];
  209. Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height);
  210. camera.update();
  211. // render scene
  212. if (sampleIndex === 0) {
  213. drawPass.postprocessing.setOcclusionOffset(0, 0);
  214. } else {
  215. drawPass.postprocessing.setOcclusionOffset(
  216. offset[0] / width,
  217. offset[1] / height
  218. );
  219. }
  220. drawPass.render(ctx, props, false);
  221. // compose rendered scene with compose target
  222. composeTarget.bind();
  223. state.enable(gl.BLEND);
  224. state.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
  225. state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
  226. state.disable(gl.DEPTH_TEST);
  227. state.depthMask(false);
  228. gl.viewport(x, y, width, height);
  229. gl.scissor(x, y, width, height);
  230. if (sampleIndex === 0) {
  231. state.clearColor(0, 0, 0, 0);
  232. gl.clear(gl.COLOR_BUFFER_BIT);
  233. }
  234. compose.render();
  235. sampleIndex += 1;
  236. if (sampleIndex >= offsetList.length) break;
  237. }
  238. }
  239. drawPass.postprocessing.setOcclusionOffset(0, 0);
  240. this.bindOutputTarget(toDrawingBuffer);
  241. gl.viewport(x, y, width, height);
  242. gl.scissor(x, y, width, height);
  243. const accumulationWeight = sampleIndex * sampleWeight;
  244. if (accumulationWeight > 0) {
  245. ValueCell.update(compose.values.uWeight, 1.0);
  246. ValueCell.update(compose.values.tColor, composeTarget.texture);
  247. compose.update();
  248. state.disable(gl.BLEND);
  249. compose.render();
  250. }
  251. if (accumulationWeight < 1.0) {
  252. ValueCell.update(compose.values.uWeight, 1.0 - accumulationWeight);
  253. ValueCell.update(compose.values.tColor, holdTarget.texture);
  254. compose.update();
  255. if (accumulationWeight === 0) state.disable(gl.BLEND);
  256. else state.enable(gl.BLEND);
  257. compose.render();
  258. }
  259. camera.viewOffset.enabled = false;
  260. camera.update();
  261. return sampleIndex >= offsetList.length ? -2 : sampleIndex;
  262. }
  263. }
  264. const JitterVectors = [
  265. [
  266. [0, 0]
  267. ],
  268. [
  269. [0, 0], [-4, -4]
  270. ],
  271. [
  272. [0, 0], [6, -2], [-6, 2], [2, 6]
  273. ],
  274. [
  275. [0, 0], [-1, 3], [5, 1], [-3, -5],
  276. [-5, 5], [-7, -1], [3, 7], [7, -7]
  277. ],
  278. [
  279. [0, 0], [-1, -3], [-3, 2], [4, -1],
  280. [-5, -2], [2, 5], [5, 3], [3, -5],
  281. [-2, 6], [0, -7], [-4, -6], [-6, 4],
  282. [-8, 0], [7, -4], [6, 7], [-7, -8]
  283. ],
  284. [
  285. [0, 0], [-7, -5], [-3, -5], [-5, -4],
  286. [-1, -4], [-2, -2], [-6, -1], [-4, 0],
  287. [-7, 1], [-1, 2], [-6, 3], [-3, 3],
  288. [-7, 6], [-3, 6], [-5, 7], [-1, 7],
  289. [5, -7], [1, -6], [6, -5], [4, -4],
  290. [2, -3], [7, -2], [1, -1], [4, -1],
  291. [2, 1], [6, 2], [0, 4], [4, 4],
  292. [2, 5], [7, 5], [5, 6], [3, 7]
  293. ]
  294. ];
  295. JitterVectors.forEach(offsetList => {
  296. offsetList.forEach(offset => {
  297. // 0.0625 = 1 / 16
  298. offset[0] *= 0.0625;
  299. offset[1] *= 0.0625;
  300. });
  301. });
  302. export class MultiSampleHelper {
  303. private sampleIndex = -2;
  304. update(changed: boolean, props: MultiSampleProps) {
  305. if (changed) this.sampleIndex = -1;
  306. return props.mode === 'temporal' ? this.sampleIndex !== -2 : false;
  307. }
  308. /** Return `true` while more samples are needed */
  309. render(ctx: RenderContext, props: Props, toDrawingBuffer: boolean, forceOn?: boolean) {
  310. this.sampleIndex = this.multiSamplePass.render(this.sampleIndex, ctx, props, toDrawingBuffer, !!forceOn);
  311. return this.sampleIndex < 0;
  312. }
  313. constructor(private multiSamplePass: MultiSamplePass) {
  314. }
  315. }