diff --git a/Apps/Sandcastle/gallery/MSAA.html b/Apps/Sandcastle/gallery/MSAA.html new file mode 100644 index 000000000000..c62dfd959f19 --- /dev/null +++ b/Apps/Sandcastle/gallery/MSAA.html @@ -0,0 +1,183 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + diff --git a/Apps/Sandcastle/gallery/MSAA.jpg b/Apps/Sandcastle/gallery/MSAA.jpg new file mode 100644 index 000000000000..4d238f127b3a Binary files /dev/null and b/Apps/Sandcastle/gallery/MSAA.jpg differ diff --git a/CHANGES.md b/CHANGES.md index 2104ee93b957..4e173c427510 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ ##### Additions :tada: +- Added MSAA support for WebGL2. Enabled on viewer creation with the multisampling rate as the `msaaSamples` option and can be controlled through `Scene.msaaSamples`. - glTF contents now use `ModelExperimental` by default. [#10055](https://github.com/CesiumGS/cesium/pull/10055) - Added the ability to toggle back-face culling in `ModelExperimental`. [#10070](https://github.com/CesiumGS/cesium/pull/10070) - Added `depthPlaneEllipsoidOffset` to Viewer and Scene constructors to address rendering artefacts below ellipsoid zero elevation. [#9200](https://github.com/CesiumGS/cesium/pull/9200) diff --git a/Source/Renderer/Context.js b/Source/Renderer/Context.js index 430ed7d10ad3..a53eb4e2fa20 100644 --- a/Source/Renderer/Context.js +++ b/Source/Renderer/Context.js @@ -256,6 +256,8 @@ function Context(canvas, options) { gl.MAX_VERTEX_UNIFORM_VECTORS ); // min: 128 + ContextLimits._maximumSamples = gl.getParameter(gl.MAX_SAMPLES); + const aliasedLineWidthRange = gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE); // must include 1 ContextLimits._minimumAliasedLineWidth = aliasedLineWidthRange[0]; ContextLimits._maximumAliasedLineWidth = aliasedLineWidthRange[1]; @@ -598,6 +600,18 @@ Object.defineProperties(Context.prototype, { }, }, + /** + * true if the WebGL context supports multisample antialiasing. Requires + * WebGL2. + * @memberof Context.prototype + * @type {Boolean} + */ + msaa: { + get: function () { + return this._webgl2; + }, + }, + /** * true if the OES_standard_derivatives extension is supported. This * extension provides access to dFdx, dFdy, and fwidth diff --git a/Source/Renderer/ContextLimits.js b/Source/Renderer/ContextLimits.js index afbcde605b5a..287f44e494b2 100644 --- a/Source/Renderer/ContextLimits.js +++ b/Source/Renderer/ContextLimits.js @@ -21,6 +21,7 @@ const ContextLimits = { _maximumTextureFilterAnisotropy: 0, _maximumDrawBuffers: 0, _maximumColorAttachments: 0, + _maximumSamples: 0, _highpFloatSupported: false, _highpIntSupported: false, }; @@ -260,6 +261,17 @@ Object.defineProperties(ContextLimits, { }, }, + /** + * The maximum number of samples supported for multisampling. + * @memberof ContextLimits + * @type {Number} + */ + maximumSamples: { + get: function () { + return ContextLimits._maximumSamples; + }, + }, + /** * High precision float supported (highp) in fragment shaders. * @memberof ContextLimits diff --git a/Source/Renderer/Framebuffer.js b/Source/Renderer/Framebuffer.js index 7bd6f90ee62d..61a1ef71fcf6 100644 --- a/Source/Renderer/Framebuffer.js +++ b/Source/Renderer/Framebuffer.js @@ -33,7 +33,7 @@ function attachRenderbuffer(framebuffer, attachment, renderbuffer) { * Framebuffers are used for render-to-texture effects; they allow us to render to * textures in one pass, and read from it in a later pass. * - * @param {Object} options The initial framebuffer attachments as shown in the example below. context is required. The possible properties are colorTextures, colorRenderbuffers, depthTexture, depthRenderbuffer, stencilRenderbuffer, depthStencilTexture, and depthStencilRenderbuffer. + * @param {Object} options The initial framebuffer attachments as shown in the example below. context is required. The possible properties are colorTextures, colorRenderbuffers, depthTexture, depthRenderbuffer, stencilRenderbuffer, depthStencilTexture, depthStencilRenderbuffer, and destroyAttachments. * * @exception {DeveloperError} Cannot have both color texture and color renderbuffer attachments. * @exception {DeveloperError} Cannot have both a depth texture and depth renderbuffer attachment. @@ -362,6 +362,16 @@ Framebuffer.prototype._unBind = function () { gl.bindFramebuffer(gl.FRAMEBUFFER, null); }; +Framebuffer.prototype.bindDraw = function () { + const gl = this._gl; + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this._framebuffer); +}; + +Framebuffer.prototype.bindRead = function () { + const gl = this._gl; + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this._framebuffer); +}; + Framebuffer.prototype._getActiveColorAttachments = function () { return this._activeColorAttachments; }; diff --git a/Source/Renderer/FramebufferManager.js b/Source/Renderer/FramebufferManager.js index 72da83afc854..83d4a5d2b48f 100644 --- a/Source/Renderer/FramebufferManager.js +++ b/Source/Renderer/FramebufferManager.js @@ -1,4 +1,5 @@ import Framebuffer from "./Framebuffer.js"; +import MultisampleFramebuffer from "./MultisampleFramebuffer.js"; import PixelDatatype from "./PixelDatatype.js"; import Renderbuffer from "./Renderbuffer.js"; import RenderbufferFormat from "./RenderbufferFormat.js"; @@ -13,6 +14,7 @@ import PixelFormat from "../Core/PixelFormat.js"; * Creates a wrapper object around a framebuffer and its resources. * * @param {Object} options Object with the following properties: + * @param {Number} [options.numSamples=1] The multisampling rate of the render targets. Requires a WebGL2 context. * @param {Number} [options.colorAttachmentsLength=1] The number of color attachments this FramebufferManager will create. * @param {Boolean} [options.color=true] Whether the FramebufferManager will use color attachments. * @param {Boolean} [options.depth=false] Whether the FramebufferManager will use depth attachments. @@ -31,6 +33,7 @@ import PixelFormat from "../Core/PixelFormat.js"; */ function FramebufferManager(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); + this._numSamples = defaultValue(options.numSamples, 1); this._colorAttachmentsLength = defaultValue( options.colorAttachmentsLength, 1 @@ -72,10 +75,13 @@ function FramebufferManager(options) { this._height = undefined; this._framebuffer = undefined; + this._multisampleFramebuffer = undefined; this._colorTextures = undefined; if (this._color) { this._colorTextures = new Array(this._colorAttachmentsLength); + this._colorRenderbuffers = new Array(this._colorAttachmentsLength); } + this._colorRenderbuffer = undefined; this._depthStencilRenderbuffer = undefined; this._depthStencilTexture = undefined; this._depthRenderbuffer = undefined; @@ -87,12 +93,20 @@ function FramebufferManager(options) { Object.defineProperties(FramebufferManager.prototype, { framebuffer: { get: function () { + if (this._numSamples > 1) { + return this._multisampleFramebuffer.getRenderFramebuffer(); + } return this._framebuffer; }, }, + numSamples: { + get: function () { + return this._numSamples; + }, + }, status: { get: function () { - return this._framebuffer.status; + return this.framebuffer.status; }, }, }); @@ -100,19 +114,27 @@ Object.defineProperties(FramebufferManager.prototype, { FramebufferManager.prototype.isDirty = function ( width, height, + numSamples, pixelDatatype, pixelFormat ) { + numSamples = defaultValue(numSamples, 1); const dimensionChanged = this._width !== width || this._height !== height; + const samplesChanged = this._numSamples !== numSamples; const pixelChanged = (defined(pixelDatatype) && this._pixelDatatype !== pixelDatatype) || (defined(pixelFormat) && this._pixelFormat !== pixelFormat); + const framebufferDefined = + numSamples === 1 + ? defined(this._framebuffer) + : defined(this._multisampleFramebuffer); return ( this._attachmentsDirty || dimensionChanged || + samplesChanged || pixelChanged || - !defined(this._framebuffer) || + !framebufferDefined || (this._color && !defined(this._colorTextures[0])) ); }; @@ -121,6 +143,7 @@ FramebufferManager.prototype.update = function ( context, width, height, + numSamples, pixelDatatype, pixelFormat ) { @@ -129,6 +152,7 @@ FramebufferManager.prototype.update = function ( throw new DeveloperError("width and height must be defined."); } //>>includeEnd('debug'); + numSamples = context.msaa ? defaultValue(numSamples, 1) : 1; pixelDatatype = defaultValue( pixelDatatype, this._color @@ -140,10 +164,11 @@ FramebufferManager.prototype.update = function ( this._color ? defaultValue(this._pixelFormat, PixelFormat.RGBA) : undefined ); - if (this.isDirty(width, height, pixelDatatype, pixelFormat)) { + if (this.isDirty(width, height, numSamples, pixelDatatype, pixelFormat)) { this.destroy(); this._width = width; this._height = height; + this._numSamples = numSamples; this._pixelDatatype = pixelDatatype; this._pixelFormat = pixelFormat; this._attachmentsDirty = false; @@ -159,6 +184,16 @@ FramebufferManager.prototype.update = function ( pixelDatatype: pixelDatatype, sampler: Sampler.NEAREST, }); + if (this._numSamples > 1) { + const format = RenderbufferFormat.getColorFormat(pixelDatatype); + this._colorRenderbuffers[i] = new Renderbuffer({ + context: context, + width: width, + height: height, + format: format, + numSamples: this._numSamples, + }); + } } } @@ -173,6 +208,15 @@ FramebufferManager.prototype.update = function ( pixelDatatype: PixelDatatype.UNSIGNED_INT_24_8, sampler: Sampler.NEAREST, }); + if (this._numSamples > 1) { + this._depthStencilRenderbuffer = new Renderbuffer({ + context: context, + width: width, + height: height, + format: RenderbufferFormat.DEPTH24_STENCIL8, + numSamples: this._numSamples, + }); + } } else { this._depthStencilRenderbuffer = new Renderbuffer({ context: context, @@ -204,15 +248,28 @@ FramebufferManager.prototype.update = function ( } } - this._framebuffer = new Framebuffer({ - context: context, - colorTextures: this._colorTextures, - depthTexture: this._depthTexture, - depthRenderbuffer: this._depthRenderbuffer, - depthStencilTexture: this._depthStencilTexture, - depthStencilRenderbuffer: this._depthStencilRenderbuffer, - destroyAttachments: false, - }); + if (this._numSamples > 1) { + this._multisampleFramebuffer = new MultisampleFramebuffer({ + context: context, + width: this._width, + height: this._height, + colorTextures: this._colorTextures, + colorRenderbuffers: this._colorRenderbuffers, + depthStencilTexture: this._depthStencilTexture, + depthStencilRenderbuffer: this._depthStencilRenderbuffer, + destroyAttachments: false, + }); + } else { + this._framebuffer = new Framebuffer({ + context: context, + colorTextures: this._colorTextures, + depthTexture: this._depthTexture, + depthRenderbuffer: this._depthRenderbuffer, + depthStencilTexture: this._depthStencilTexture, + depthStencilRenderbuffer: this._depthStencilRenderbuffer, + destroyAttachments: false, + }); + } } }; @@ -246,6 +303,39 @@ FramebufferManager.prototype.setColorTexture = function (texture, index) { this._colorTextures[index] = texture; }; +FramebufferManager.prototype.getColorRenderbuffer = function (index) { + index = defaultValue(index, 0); + //>>includeStart('debug', pragmas.debug); + if (index >= this._colorAttachmentsLength) { + throw new DeveloperError( + "index must be smaller than total number of color attachments." + ); + } + //>>includeEnd('debug'); + return this._colorRenderbuffers[index]; +}; + +FramebufferManager.prototype.setColorRenderbuffer = function ( + renderbuffer, + index +) { + index = defaultValue(index, 0); + //>>includeStart('debug', pragmas.debug); + if (this._createColorAttachments) { + throw new DeveloperError( + "createColorAttachments must be false if setColorRenderbuffer is called." + ); + } + if (index >= this._colorAttachmentsLength) { + throw new DeveloperError( + "index must be smaller than total number of color attachments." + ); + } + //>>includeEnd('debug'); + this._attachmentsDirty = renderbuffer !== this._colorRenderbuffers[index]; + this._colorRenderbuffers[index] = renderbuffer; +}; + FramebufferManager.prototype.getDepthRenderbuffer = function () { return this._depthRenderbuffer; }; @@ -312,47 +402,94 @@ FramebufferManager.prototype.setDepthStencilTexture = function (texture) { this._depthStencilTexture = texture; }; +FramebufferManager.prototype.prepareTextures = function (context, blitStencil) { + if (this._numSamples > 1) { + this._multisampleFramebuffer.blitFramebuffers(context, blitStencil); + } +}; + FramebufferManager.prototype.clear = function ( context, clearCommand, passState ) { const framebuffer = clearCommand.framebuffer; - - clearCommand.framebuffer = this._framebuffer; + clearCommand.framebuffer = this.framebuffer; clearCommand.execute(context, passState); - clearCommand.framebuffer = framebuffer; }; FramebufferManager.prototype.destroyFramebuffer = function () { this._framebuffer = this._framebuffer && this._framebuffer.destroy(); + this._multisampleFramebuffer = + this._multisampleFramebuffer && this._multisampleFramebuffer.destroy(); }; FramebufferManager.prototype.destroy = function () { - if (this._color && this._createColorAttachments) { + if (this._color) { + let i; const length = this._colorTextures.length; - for (let i = 0; i < length; ++i) { + for (i = 0; i < length; ++i) { const texture = this._colorTextures[i]; - if (defined(texture) && !texture.isDestroyed()) { - this._colorTextures[i].destroy(); + if (this._createColorAttachments) { + if (defined(texture) && !texture.isDestroyed()) { + this._colorTextures[i].destroy(); + this._colorTextures[i] = undefined; + } + } + if (defined(texture) && texture.isDestroyed()) { this._colorTextures[i] = undefined; } + const renderbuffer = this._colorRenderbuffers[i]; + if (this._createColorAttachments) { + if (defined(renderbuffer) && !renderbuffer.isDestroyed()) { + this._colorRenderbuffers[i].destroy(); + this._colorRenderbuffers[i] = undefined; + } + } + if (defined(renderbuffer) && renderbuffer.isDestroyed()) { + this._colorRenderbuffers[i] = undefined; + } } } - if (this._depthStencil && this._createDepthAttachments) { - this._depthStencilTexture = - this._depthStencilTexture && this._depthStencilTexture.destroy(); - this._depthStencilRenderbuffer = - this._depthStencilRenderbuffer && - this._depthStencilRenderbuffer.destroy(); + if (this._depthStencil) { + if (this._createDepthAttachments) { + this._depthStencilTexture = + this._depthStencilTexture && this._depthStencilTexture.destroy(); + this._depthStencilRenderbuffer = + this._depthStencilRenderbuffer && + this._depthStencilRenderbuffer.destroy(); + } + if ( + defined(this._depthStencilTexture) && + this._depthStencilTexture.isDestroyed() + ) { + this._depthStencilTexture = undefined; + } + if ( + defined(this._depthStencilRenderbuffer) && + this._depthStencilRenderbuffer.isDestroyed() + ) { + this._depthStencilRenderbuffer = undefined; + } } - if (this._depth && this._createDepthAttachments) { - this._depthTexture = this._depthTexture && this._depthTexture.destroy(); - this._depthRenderbuffer = - this._depthRenderbuffer && this._depthRenderbuffer.destroy(); + if (this._depth) { + if (this._createDepthAttachments) { + this._depthTexture = this._depthTexture && this._depthTexture.destroy(); + this._depthRenderbuffer = + this._depthRenderbuffer && this._depthRenderbuffer.destroy(); + } + if (defined(this._depthTexture) && this._depthTexture.isDestroyed()) { + this._depthTexture = undefined; + } + if ( + defined(this._depthRenderbuffer) && + this._depthRenderbuffer.isDestroyed() + ) { + this._depthRenderbuffer = undefined; + } } this.destroyFramebuffer(); diff --git a/Source/Renderer/MultisampleFramebuffer.js b/Source/Renderer/MultisampleFramebuffer.js new file mode 100644 index 000000000000..9327cc49cc5e --- /dev/null +++ b/Source/Renderer/MultisampleFramebuffer.js @@ -0,0 +1,115 @@ +import Check from "../Core/Check.js"; +import defaultValue from "../Core/defaultValue.js"; +import defined from "../Core/defined.js"; +import destroyObject from "../Core/destroyObject.js"; +import DeveloperError from "../Core/DeveloperError.js"; +import Framebuffer from "./Framebuffer.js"; + +/** + * Creates a multisampling wrapper around two framebuffers with optional initial + * color and depth-stencil attachments. The first framebuffer has multisampled + * renderbuffer attachments and is bound to READ_FRAMEBUFFER during the blit. The + * second is bound to DRAW_FRAMEBUFFER during the blit, and has texture attachments + * to store the copied pixels. + * + * @param {Object} options The initial framebuffer attachments. context, width, and height are required. The possible properties are colorTextures, colorRenderbuffers, depthStencilTexture, depthStencilRenderbuffer, and destroyAttachments. + * + * @exception {DeveloperError} Both color renderbuffer and texture attachments must be provided. + * @exception {DeveloperError} Both depth-stencil renderbuffer and texture attachments must be provided. + * + * @private + * @constructor + */ +function MultisampleFramebuffer(options) { + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + + const context = options.context; + const width = options.width; + const height = options.height; + //>>includeStart('debug', pragmas.debug); + Check.defined("options.context", context); + Check.defined("options.width", width); + Check.defined("options.height", height); + //>>includeEnd('debug'); + this._width = width; + this._height = height; + + const colorRenderbuffers = options.colorRenderbuffers; + const colorTextures = options.colorTextures; + if (defined(colorRenderbuffers) !== defined(colorTextures)) { + throw new DeveloperError( + "Both color renderbuffer and texture attachments must be provided." + ); + } + + const depthStencilRenderbuffer = options.depthStencilRenderbuffer; + const depthStencilTexture = options.depthStencilTexture; + if (defined(depthStencilRenderbuffer) !== defined(depthStencilTexture)) { + throw new DeveloperError( + "Both depth-stencil renderbuffer and texture attachments must be provided." + ); + } + + this._renderFramebuffer = new Framebuffer({ + context: context, + colorRenderbuffers: colorRenderbuffers, + depthStencilRenderbuffer: depthStencilRenderbuffer, + destroyAttachments: options.destroyAttachments, + }); + this._colorFramebuffer = new Framebuffer({ + context: context, + colorTextures: colorTextures, + depthStencilTexture: depthStencilTexture, + destroyAttachments: options.destroyAttachments, + }); +} + +MultisampleFramebuffer.prototype.getRenderFramebuffer = function () { + return this._renderFramebuffer; +}; + +MultisampleFramebuffer.prototype.getColorFramebuffer = function () { + return this._colorFramebuffer; +}; + +MultisampleFramebuffer.prototype.blitFramebuffers = function ( + context, + blitStencil +) { + this._renderFramebuffer.bindRead(); + this._colorFramebuffer.bindDraw(); + const gl = context._gl; + let mask = 0; + if (this._colorFramebuffer._colorTextures.length > 0) { + mask |= gl.COLOR_BUFFER_BIT; + } + if (defined(this._colorFramebuffer.depthStencilTexture)) { + mask |= gl.DEPTH_BUFFER_BIT | (blitStencil ? gl.STENCIL_BUFFER_BIT : 0); + } + gl.blitFramebuffer( + 0, + 0, + this._width, + this._height, + 0, + 0, + this._width, + this._height, + mask, + gl.NEAREST + ); + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null); +}; + +MultisampleFramebuffer.prototype.isDestroyed = function () { + return false; +}; + +MultisampleFramebuffer.prototype.destroy = function () { + this._renderFramebuffer.destroy(); + this._colorFramebuffer.destroy(); + return destroyObject(this); +}; + +export default MultisampleFramebuffer; diff --git a/Source/Renderer/Renderbuffer.js b/Source/Renderer/Renderbuffer.js index 08102eebdbf0..ab875c2878db 100644 --- a/Source/Renderer/Renderbuffer.js +++ b/Source/Renderer/Renderbuffer.js @@ -25,6 +25,7 @@ function Renderbuffer(options) { const height = defined(options.height) ? options.height : gl.drawingBufferHeight; + const numSamples = defaultValue(options.numSamples, 1); //>>includeStart('debug', pragmas.debug); if (!RenderbufferFormat.validate(format)) { @@ -55,7 +56,17 @@ function Renderbuffer(options) { this._renderbuffer = this._gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, this._renderbuffer); - gl.renderbufferStorage(gl.RENDERBUFFER, format, width, height); + if (numSamples > 1) { + gl.renderbufferStorageMultisample( + gl.RENDERBUFFER, + numSamples, + format, + width, + height + ); + } else { + gl.renderbufferStorage(gl.RENDERBUFFER, format, width, height); + } gl.bindRenderbuffer(gl.RENDERBUFFER, null); } diff --git a/Source/Renderer/RenderbufferFormat.js b/Source/Renderer/RenderbufferFormat.js index 6e3f4e4442e8..d3874f6249d7 100644 --- a/Source/Renderer/RenderbufferFormat.js +++ b/Source/Renderer/RenderbufferFormat.js @@ -5,21 +5,38 @@ import WebGLConstants from "../Core/WebGLConstants.js"; */ const RenderbufferFormat = { RGBA4: WebGLConstants.RGBA4, + RGBA8: WebGLConstants.RGBA8, + RGBA16F: WebGLConstants.RGBA16F, + RGBA32F: WebGLConstants.RGBA32F, RGB5_A1: WebGLConstants.RGB5_A1, RGB565: WebGLConstants.RGB565, DEPTH_COMPONENT16: WebGLConstants.DEPTH_COMPONENT16, STENCIL_INDEX8: WebGLConstants.STENCIL_INDEX8, DEPTH_STENCIL: WebGLConstants.DEPTH_STENCIL, + DEPTH24_STENCIL8: WebGLConstants.DEPTH24_STENCIL8, validate: function (renderbufferFormat) { return ( renderbufferFormat === RenderbufferFormat.RGBA4 || + renderbufferFormat === RenderbufferFormat.RGBA8 || + renderbufferFormat === RenderbufferFormat.RGBA16F || + renderbufferFormat === RenderbufferFormat.RGBA32F || renderbufferFormat === RenderbufferFormat.RGB5_A1 || renderbufferFormat === RenderbufferFormat.RGB565 || renderbufferFormat === RenderbufferFormat.DEPTH_COMPONENT16 || renderbufferFormat === RenderbufferFormat.STENCIL_INDEX8 || - renderbufferFormat === RenderbufferFormat.DEPTH_STENCIL + renderbufferFormat === RenderbufferFormat.DEPTH_STENCIL || + renderbufferFormat === RenderbufferFormat.DEPTH24_STENCIL8 ); }, + + getColorFormat: function (datatype) { + if (datatype === WebGLConstants.FLOAT) { + return RenderbufferFormat.RGBA32F; + } else if (datatype === WebGLConstants.HALF_FLOAT_OES) { + return RenderbufferFormat.RGBA16F; + } + return RenderbufferFormat.RGBA8; + }, }; export default Object.freeze(RenderbufferFormat); diff --git a/Source/Scene/AutoExposure.js b/Source/Scene/AutoExposure.js index c42d134190ed..3a3f71e39ba8 100644 --- a/Source/Scene/AutoExposure.js +++ b/Source/Scene/AutoExposure.js @@ -140,7 +140,7 @@ function createFramebuffers(autoexposure, context) { width = Math.max(Math.ceil(width / 3.0), 1.0); height = Math.max(Math.ceil(height / 3.0), 1.0); framebuffers[i] = new FramebufferManager(); - framebuffers[i].update(context, width, height, pixelDatatype); + framebuffers[i].update(context, width, height, 1, pixelDatatype); } const lastTexture = framebuffers[length - 1].getColorTexture(0); @@ -148,6 +148,7 @@ function createFramebuffers(autoexposure, context) { context, lastTexture.width, lastTexture.height, + 1, pixelDatatype ); autoexposure._framebuffers = framebuffers; diff --git a/Source/Scene/GlobeDepth.js b/Source/Scene/GlobeDepth.js index 44d8ca322e74..562ec8ad0ccc 100644 --- a/Source/Scene/GlobeDepth.js +++ b/Source/Scene/GlobeDepth.js @@ -17,9 +17,15 @@ import StencilOperation from "./StencilOperation.js"; * @private */ function GlobeDepth() { + this._picking = false; + this._numSamples = 1; this._tempCopyDepthTexture = undefined; - this._colorFramebuffer = new FramebufferManager({ + this._pickColorFramebuffer = new FramebufferManager({ + depthStencil: true, + supportsDepthTexture: true, + }); + this._outputFramebuffer = new FramebufferManager({ depthStencil: true, supportsDepthTexture: true, }); @@ -50,15 +56,36 @@ function GlobeDepth() { } Object.defineProperties(GlobeDepth.prototype, { + colorFramebufferManager: { + get: function () { + return this._picking + ? this._pickColorFramebuffer + : this._outputFramebuffer; + }, + }, framebuffer: { get: function () { - return this._colorFramebuffer.framebuffer; + return this.colorFramebufferManager.framebuffer; + }, + }, + depthStencilTexture: { + get: function () { + return this.colorFramebufferManager.getDepthStencilTexture(); + }, + }, + picking: { + get: function () { + return this._picking; + }, + set: function (value) { + this._picking = value; }, }, }); function destroyFramebuffers(globeDepth) { - globeDepth._colorFramebuffer.destroy(); + globeDepth._pickColorFramebuffer.destroy(); + globeDepth._outputFramebuffer.destroy(); globeDepth._copyDepthFramebuffer.destroy(); globeDepth._tempCopyDepthFramebuffer.destroy(); globeDepth._updateDepthFramebuffer.destroy(); @@ -134,7 +161,7 @@ function updateCopyCommands(globeDepth, context, width, height, passState) { { uniformMap: { u_depthTexture: function () { - return globeDepth._colorFramebuffer.getDepthStencilTexture(); + return globeDepth.colorFramebufferManager.getDepthStencilTexture(); }, }, owner: globeDepth, @@ -152,7 +179,7 @@ function updateCopyCommands(globeDepth, context, width, height, passState) { { uniformMap: { colorTexture: function () { - return globeDepth._colorFramebuffer.getColorTexture(); + return globeDepth.colorFramebufferManager.getColorTexture(); }, }, owner: globeDepth, @@ -213,6 +240,7 @@ GlobeDepth.prototype.update = function ( context, passState, viewport, + numSamples, hdr, clearGlobeDepth ) { @@ -224,7 +252,18 @@ GlobeDepth.prototype.update = function ( ? PixelDatatype.HALF_FLOAT : PixelDatatype.FLOAT : PixelDatatype.UNSIGNED_BYTE; - this._colorFramebuffer.update(context, width, height, pixelDatatype); + this._numSamples = numSamples; + if (this.picking) { + this._pickColorFramebuffer.update(context, width, height); + } else { + this._outputFramebuffer.update( + context, + width, + height, + numSamples, + pixelDatatype + ); + } this._copyDepthFramebuffer.update(context, width, height); updateCopyCommands(this, context, width, height, passState); context.uniformState.globeDepthTexture = undefined; @@ -233,8 +272,15 @@ GlobeDepth.prototype.update = function ( this._clearGlobeDepth = clearGlobeDepth; }; +GlobeDepth.prototype.prepareColorTextures = function (context, blitStencil) { + if (!this.picking && this._numSamples > 1) { + this._outputFramebuffer.prepareTextures(context, blitStencil); + } +}; + GlobeDepth.prototype.executeCopyDepth = function (context, passState) { if (defined(this._copyDepthCommand)) { + this.prepareColorTextures(context); this._copyDepthCommand.execute(context, passState); context.uniformState.globeDepthTexture = this._copyDepthFramebuffer.getColorTexture(); } @@ -243,12 +289,15 @@ GlobeDepth.prototype.executeCopyDepth = function (context, passState) { GlobeDepth.prototype.executeUpdateDepth = function ( context, passState, - clearGlobeDepth + clearGlobeDepth, + depthTexture ) { - const depthTextureToCopy = passState.framebuffer.depthStencilTexture; + const depthTextureToCopy = defined(depthTexture) + ? depthTexture + : passState.framebuffer.depthStencilTexture; if ( clearGlobeDepth || - depthTextureToCopy !== this._colorFramebuffer.getDepthStencilTexture() + depthTextureToCopy !== this.colorFramebufferManager.getDepthStencilTexture() ) { // First copy the depth to a temporary globe depth texture, then update the // main globe depth texture where the stencil bit for 3D Tiles is set. @@ -268,11 +317,8 @@ GlobeDepth.prototype.executeUpdateDepth = function ( this._tempCopyDepthFramebuffer.update(context, width, height); const colorTexture = this._copyDepthFramebuffer.getColorTexture(); - const depthStencilTexture = passState.framebuffer.depthStencilTexture; this._updateDepthFramebuffer.setColorTexture(colorTexture, 0); - this._updateDepthFramebuffer.setDepthStencilTexture( - depthStencilTexture - ); + this._updateDepthFramebuffer.setDepthStencilTexture(depthTextureToCopy); this._updateDepthFramebuffer.update(context, width, height); updateCopyCommands(this, context, width, height, passState); @@ -300,7 +346,7 @@ GlobeDepth.prototype.clear = function (context, passState, clearColor) { const clear = this._clearGlobeColorCommand; if (defined(clear)) { Color.clone(clearColor, clear.color); - this._colorFramebuffer.clear(context, clear, passState); + this.colorFramebufferManager.clear(context, clear, passState); } }; diff --git a/Source/Scene/GlobeTranslucencyFramebuffer.js b/Source/Scene/GlobeTranslucencyFramebuffer.js index abd953cd702b..7aea7f86d402 100644 --- a/Source/Scene/GlobeTranslucencyFramebuffer.js +++ b/Source/Scene/GlobeTranslucencyFramebuffer.js @@ -76,7 +76,13 @@ function updateResources(globeTranslucency, context, width, height, hdr) { ? PixelDatatype.HALF_FLOAT : PixelDatatype.FLOAT : PixelDatatype.UNSIGNED_BYTE; - globeTranslucency._framebuffer.update(context, width, height, pixelDatatype); + globeTranslucency._framebuffer.update( + context, + width, + height, + 1, + pixelDatatype + ); globeTranslucency._packedDepthFramebuffer.update(context, width, height); } diff --git a/Source/Scene/InvertClassification.js b/Source/Scene/InvertClassification.js index d54897dda064..3fead5009c91 100644 --- a/Source/Scene/InvertClassification.js +++ b/Source/Scene/InvertClassification.js @@ -5,6 +5,8 @@ import PixelFormat from "../Core/PixelFormat.js"; import ClearCommand from "../Renderer/ClearCommand.js"; import FramebufferManager from "../Renderer/FramebufferManager.js"; import PixelDatatype from "../Renderer/PixelDatatype.js"; +import Renderbuffer from "../Renderer/Renderbuffer.js"; +import RenderbufferFormat from "../Renderer/RenderbufferFormat.js"; import RenderState from "../Renderer/RenderState.js"; import ShaderSource from "../Renderer/ShaderSource.js"; import Texture from "../Renderer/Texture.js"; @@ -18,14 +20,18 @@ import StencilOperation from "./StencilOperation.js"; * @private */ function InvertClassification() { + this._numSamples = 1; this.previousFramebuffer = undefined; this._previousFramebuffer = undefined; this._depthStencilTexture = undefined; + this._depthStencilRenderbuffer = undefined; this._fbo = new FramebufferManager({ + depthStencil: true, createDepthAttachments: false, }); this._fboClassified = new FramebufferManager({ + depthStencil: true, createDepthAttachments: false, }); @@ -167,20 +173,29 @@ const opaqueFS = "#endif\n" + "}\n"; -InvertClassification.prototype.update = function (context) { +InvertClassification.prototype.update = function ( + context, + numSamples, + globeFramebuffer +) { const texture = this._fbo.getColorTexture(); const previousFramebufferChanged = this.previousFramebuffer !== this._previousFramebuffer; this._previousFramebuffer = this.previousFramebuffer; + const samplesChanged = this._numSamples !== numSamples; const width = context.drawingBufferWidth; const height = context.drawingBufferHeight; const textureChanged = !defined(texture) || texture.width !== width || texture.height !== height; - if (textureChanged || previousFramebufferChanged) { + if (textureChanged || previousFramebufferChanged || samplesChanged) { + this._numSamples = numSamples; this._depthStencilTexture = this._depthStencilTexture && this._depthStencilTexture.destroy(); + this._depthStencilRenderbuffer = + this._depthStencilRenderbuffer && + this._depthStencilRenderbuffer.destroy(); if (!defined(this._previousFramebuffer)) { this._depthStencilTexture = new Texture({ @@ -190,13 +205,23 @@ InvertClassification.prototype.update = function (context) { pixelFormat: PixelFormat.DEPTH_STENCIL, pixelDatatype: PixelDatatype.UNSIGNED_INT_24_8, }); + if (numSamples > 1) { + this._depthStencilRenderbuffer = new Renderbuffer({ + context: context, + width: width, + height: height, + format: RenderbufferFormat.DEPTH24_STENCIL8, + numSamples: numSamples, + }); + } } } if ( !defined(this._fbo.framebuffer) || textureChanged || - previousFramebufferChanged + previousFramebufferChanged || + samplesChanged ) { this._fbo.destroy(); this._fboClassified.destroy(); @@ -204,18 +229,18 @@ InvertClassification.prototype.update = function (context) { let depthStencilTexture; let depthStencilRenderbuffer; if (defined(this._previousFramebuffer)) { - depthStencilTexture = this._previousFramebuffer.depthStencilTexture; - depthStencilRenderbuffer = this._previousFramebuffer - .depthStencilRenderbuffer; + depthStencilTexture = globeFramebuffer.getDepthStencilTexture(); + depthStencilRenderbuffer = globeFramebuffer.getDepthStencilRenderbuffer(); } else { depthStencilTexture = this._depthStencilTexture; + depthStencilRenderbuffer = this._depthStencilRenderbuffer; } this._fbo.setDepthStencilTexture(depthStencilTexture); if (defined(depthStencilRenderbuffer)) { this._fbo.setDepthStencilRenderbuffer(depthStencilRenderbuffer); } - this._fbo.update(context, width, height); + this._fbo.update(context, width, height, numSamples); if (!defined(this._previousFramebuffer)) { this._fboClassified.setDepthStencilTexture(depthStencilTexture); @@ -229,7 +254,11 @@ InvertClassification.prototype.update = function (context) { this._rsDefault = RenderState.fromCache(rsDefault); } - if (!defined(this._unclassifiedCommand) || previousFramebufferChanged) { + if ( + !defined(this._unclassifiedCommand) || + previousFramebufferChanged || + samplesChanged + ) { if (defined(this._unclassifiedCommand)) { this._unclassifiedCommand.shaderProgram = this._unclassifiedCommand.shaderProgram && @@ -286,6 +315,15 @@ InvertClassification.prototype.update = function (context) { } }; +InvertClassification.prototype.prepareTextures = function ( + context, + blitStencil +) { + if (this._fbo._numSamples > 1) { + this._fbo.prepareTextures(context, blitStencil); + } +}; + InvertClassification.prototype.clear = function (context, passState) { if (defined(this._previousFramebuffer)) { this._fbo.clear(context, this._clearColorCommand, passState); @@ -302,6 +340,7 @@ InvertClassification.prototype.executeClassified = function ( if (!defined(this._previousFramebuffer)) { const framebuffer = passState.framebuffer; + this.prepareTextures(context, true); passState.framebuffer = this._fboClassified.framebuffer; this._translucentCommand.execute(context, passState); @@ -326,6 +365,8 @@ InvertClassification.prototype.destroy = function () { this._fboClassified.destroy(); this._depthStencilTexture = this._depthStencilTexture && this._depthStencilTexture.destroy(); + this._depthStencilRenderbuffer = + this._depthStencilRenderbuffer && this._depthStencilRenderbuffer.destroy(); if (defined(this._unclassifiedCommand)) { this._unclassifiedCommand.shaderProgram = diff --git a/Source/Scene/OIT.js b/Source/Scene/OIT.js index 3cadb7a05c6f..819b0af44f44 100644 --- a/Source/Scene/OIT.js +++ b/Source/Scene/OIT.js @@ -20,6 +20,7 @@ import BlendFunction from "./BlendFunction.js"; * @private */ function OIT(context) { + this._numSamples = 1; // We support multipass for the Chrome D3D9 backend and ES 2.0 on mobile. this._translucentMultipassSupport = false; this._translucentMRTSupport = false; @@ -39,10 +40,12 @@ function OIT(context) { colorAttachmentsLength: this._translucentMRTSupport ? 2 : 1, createColorAttachments: false, createDepthAttachments: false, + depth: true, }); this._alphaFBO = new FramebufferManager({ createColorAttachments: false, createDepthAttachments: false, + depth: true, }); this._adjustTranslucentFBO = new FramebufferManager({ @@ -201,14 +204,20 @@ function updateFramebuffers(oit, context) { return supported; } -OIT.prototype.update = function (context, passState, framebuffer, useHDR) { +OIT.prototype.update = function ( + context, + passState, + framebuffer, + useHDR, + numSamples +) { if (!this.isSupported()) { return; } this._opaqueFBO = framebuffer; this._opaqueTexture = framebuffer.getColorTexture(0); - this._depthStencilTexture = framebuffer.depthStencilTexture; + this._depthStencilTexture = framebuffer.getDepthStencilTexture(); const width = this._opaqueTexture.width; const height = this._opaqueTexture.height; @@ -219,11 +228,18 @@ OIT.prototype.update = function (context, passState, framebuffer, useHDR) { accumulationTexture.width !== width || accumulationTexture.height !== height || useHDR !== this._useHDR; - if (textureChanged) { + const samplesChanged = this._numSamples !== numSamples; + + if (textureChanged || samplesChanged) { + this._numSamples = numSamples; updateTextures(this, context, width, height); } - if (!defined(this._translucentFBO.framebuffer) || textureChanged) { + if ( + !defined(this._translucentFBO.framebuffer) || + textureChanged || + samplesChanged + ) { if (!updateFramebuffers(this, context)) { // framebuffer creation failed return; @@ -656,7 +672,7 @@ function executeTranslucentCommandsSortedMultipass( passState.framebuffer = oit._adjustAlphaFBO.framebuffer; oit._adjustAlphaCommand.execute(context, passState); - const debugFramebuffer = oit._opaqueFBO; + const debugFramebuffer = oit._opaqueFBO.framebuffer; passState.framebuffer = oit._translucentFBO.framebuffer; for (j = 0; j < length; ++j) { @@ -747,7 +763,7 @@ function executeTranslucentCommandsSortedMRT( passState.framebuffer = oit._adjustTranslucentFBO.framebuffer; oit._adjustTranslucentCommand.execute(context, passState); - const debugFramebuffer = oit._opaqueFBO; + const debugFramebuffer = oit._opaqueFBO.framebuffer; passState.framebuffer = oit._translucentFBO.framebuffer; let command; @@ -824,7 +840,7 @@ OIT.prototype.execute = function (context, passState) { OIT.prototype.clear = function (context, passState, clearColor) { const framebuffer = passState.framebuffer; - passState.framebuffer = this._opaqueFBO; + passState.framebuffer = this._opaqueFBO.framebuffer; Color.clone(clearColor, this._opaqueClearCommand.color); this._opaqueClearCommand.execute(context, passState); diff --git a/Source/Scene/Scene.js b/Source/Scene/Scene.js index 1dbca11d453c..8a257ed62ecb 100644 --- a/Source/Scene/Scene.js +++ b/Source/Scene/Scene.js @@ -135,6 +135,7 @@ const requestRenderAfterFrame = function (scene) { * @param {Boolean} [options.requestRenderMode=false] If true, rendering a frame will only occur when needed as determined by changes within the scene. Enabling improves performance of the application, but requires using {@link Scene#requestRender} to render a new frame explicitly in this mode. This will be necessary in many cases after making changes to the scene in other parts of the API. See {@link https://cesium.com/blog/2018/01/24/cesium-scene-rendering-performance/|Improving Performance with Explicit Rendering}. * @param {Number} [options.maximumRenderTimeChange=0.0] If requestRenderMode is true, this value defines the maximum change in simulation time allowed before a render is requested. See {@link https://cesium.com/blog/2018/01/24/cesium-scene-rendering-performance/|Improving Performance with Explicit Rendering}. * @param {Number} [depthPlaneEllipsoidOffset=0.0] Adjust the DepthPlane to address rendering artefacts below ellipsoid zero elevation. + * @param {Number} [options.msaaSamples=1] If provided, this value controls the rate of multisample antialiasing. Typical multisampling rates are 2, 4, and sometimes 8 samples per pixel. Higher sampling rates of MSAA may impact performance in exchange for improved visual quality. This value only applies to WebGL2 contexts that support multisample render targets. * * @see CesiumWidget * @see {@link http://www.khronos.org/registry/webgl/specs/latest/#5.2|WebGLContextAttributes} @@ -261,6 +262,8 @@ function Scene(options) { this._minimumDisableDepthTestDistance = 0.0; this._debugInspector = new DebugInspector(); + this._msaaSamples = defaultValue(options.msaaSamples, 1); + /** * Exceptions occurring in render are always caught in order to raise the * renderError event. If this property is true, the error is rethrown @@ -1594,6 +1597,35 @@ Object.defineProperties(Scene.prototype, { }, }, + /** + * The sample rate of multisample antialiasing (values greater than 1 enable MSAA). + * @memberof Scene.prototype + * @type {Number} + * @readonly + * @default 1 + */ + msaaSamples: { + get: function () { + return this._msaaSamples; + }, + set: function (value) { + value = Math.min(value, ContextLimits.maximumSamples); + this._msaaSamples = value; + }, + }, + + /** + * Returns true if the Scene's context supports MSAA. + * @memberof Scene.prototype + * @type {Boolean} + * @readonly + */ + msaaSupported: { + get: function () { + return this._context.msaa; + }, + }, + /** * Ratio between a pixel and a density-independent pixel. Provides a standard unit of * measure for real pixel measurements appropriate to a particular device. @@ -2321,6 +2353,7 @@ function executeCommands(scene, passState) { commands, invertClassification ) { + view.globeDepth.prepareColorTextures(context); view.oit.executeCommands( scene, executeFunction, @@ -2451,7 +2484,15 @@ function executeCommands(scene, passState) { if (length > 0) { if (defined(globeDepth) && environmentState.useGlobeDepthFramebuffer) { - globeDepth.executeUpdateDepth(context, passState, clearGlobeDepth); + // When clearGlobeDepth is true, executeUpdateDepth needs + // a globe depth texture with resolved stencil bits. + globeDepth.prepareColorTextures(context, clearGlobeDepth); + globeDepth.executeUpdateDepth( + context, + passState, + clearGlobeDepth, + globeDepth.depthStencilTexture + ); } // Draw classifications. Modifies 3D Tiles color. @@ -2512,7 +2553,13 @@ function executeCommands(scene, passState) { } if (defined(globeDepth) && environmentState.useGlobeDepthFramebuffer) { - globeDepth.executeUpdateDepth(context, passState, clearGlobeDepth); + scene._invertClassification.prepareTextures(context); + globeDepth.executeUpdateDepth( + context, + passState, + clearGlobeDepth, + scene._invertClassification._fbo.getDepthStencilTexture() + ); } // Set stencil @@ -2601,7 +2648,7 @@ function executeCommands(scene, passState) { executeCommand, passState, commands, - globeDepth.framebuffer + globeDepth.depthStencilTexture ); view.translucentTileClassification.executeClassificationCommands( scene, @@ -2618,9 +2665,7 @@ function executeCommands(scene, passState) { renderTranslucentDepthForPick) ) { // PERFORMANCE_IDEA: Use MRT to avoid the extra copy. - const depthStencilTexture = renderTranslucentDepthForPick - ? passState.framebuffer.depthStencilTexture - : globeDepth.framebuffer.depthStencilTexture; + const depthStencilTexture = globeDepth.depthStencilTexture; const pickDepth = scene._picking.getPickDepth(scene, index); pickDepth.update(context, depthStencilTexture); pickDepth.executeCopyDepth(context, passState); @@ -3333,6 +3378,9 @@ function updateAndClearFramebuffers(scene, passState, clearColor) { const passes = scene._frameState.passes; const picking = passes.pick; + if (defined(view.globeDepth)) { + view.globeDepth.picking = picking; + } const useWebVR = environmentState.useWebVR; // Preserve the reference to the original framebuffer. @@ -3367,6 +3415,7 @@ function updateAndClearFramebuffers(scene, passState, clearColor) { context, passState, view.viewport, + scene.msaaSamples, scene._hdr, environmentState.clearGlobeDepth ); @@ -3378,7 +3427,13 @@ function updateAndClearFramebuffers(scene, passState, clearColor) { const useOIT = (environmentState.useOIT = !picking && defined(oit) && oit.isSupported()); if (useOIT) { - oit.update(context, passState, view.globeDepth.framebuffer, scene._hdr); + oit.update( + context, + passState, + view.globeDepth.colorFramebufferManager, + scene._hdr, + scene.msaaSamples + ); oit.clear(context, passState, clearColor); environmentState.useOIT = oit.isSupported(); } @@ -3393,7 +3448,12 @@ function updateAndClearFramebuffers(scene, passState, clearColor) { postProcess.bloom.enabled)); environmentState.usePostProcessSelected = false; if (usePostProcess) { - view.sceneFramebuffer.update(context, view.viewport, scene._hdr); + view.sceneFramebuffer.update( + context, + view.viewport, + scene._hdr, + scene.msaaSamples + ); view.sceneFramebuffer.clear(context, passState, clearColor); postProcess.update(context, frameState.useLogDepth, scene._hdr); @@ -3429,7 +3489,11 @@ function updateAndClearFramebuffers(scene, passState, clearColor) { if (defined(depthFramebuffer) || context.depthTexture) { scene._invertClassification.previousFramebuffer = depthFramebuffer; - scene._invertClassification.update(context); + scene._invertClassification.update( + context, + scene.msaaSamples, + view.globeDepth.colorFramebufferManager + ); scene._invertClassification.clear(context, passState); if (scene.frameState.invertClassificationColor.alpha < 1.0 && useOIT) { @@ -3464,6 +3528,9 @@ Scene.prototype.resolveFramebuffers = function (passState) { const environmentState = this._environmentState; const view = this._view; const globeDepth = view.globeDepth; + if (defined(globeDepth)) { + globeDepth.prepareColorTextures(context); + } const useOIT = environmentState.useOIT; const useGlobeDepthFramebuffer = environmentState.useGlobeDepthFramebuffer; @@ -3471,14 +3538,14 @@ Scene.prototype.resolveFramebuffers = function (passState) { const defaultFramebuffer = environmentState.originalFramebuffer; const globeFramebuffer = useGlobeDepthFramebuffer - ? globeDepth.framebuffer + ? globeDepth.colorFramebufferManager : undefined; - const sceneFramebuffer = view.sceneFramebuffer.framebuffer; + const sceneFramebuffer = view.sceneFramebuffer._colorFramebuffer; const idFramebuffer = view.sceneFramebuffer.idFramebuffer; if (useOIT) { passState.framebuffer = usePostProcess - ? sceneFramebuffer + ? sceneFramebuffer.framebuffer : defaultFramebuffer; view.oit.execute(context, passState); } @@ -3492,6 +3559,7 @@ Scene.prototype.resolveFramebuffers = function (passState) { } if (usePostProcess) { + view.sceneFramebuffer.prepareColorTextures(context); let inputFramebuffer = sceneFramebuffer; if (useGlobeDepthFramebuffer && !useOIT) { inputFramebuffer = globeFramebuffer; @@ -3500,8 +3568,10 @@ Scene.prototype.resolveFramebuffers = function (passState) { const postProcess = this.postProcessStages; const colorTexture = inputFramebuffer.getColorTexture(0); const idTexture = idFramebuffer.getColorTexture(0); - const depthTexture = defaultValue(globeFramebuffer, sceneFramebuffer) - .depthStencilTexture; + const depthTexture = defaultValue( + globeFramebuffer, + sceneFramebuffer + ).getDepthStencilTexture(); postProcess.execute(context, colorTexture, depthTexture, idTexture); postProcess.copy(context, defaultFramebuffer); } diff --git a/Source/Scene/SceneFramebuffer.js b/Source/Scene/SceneFramebuffer.js index c9f6e1156906..274c623a5e8a 100644 --- a/Source/Scene/SceneFramebuffer.js +++ b/Source/Scene/SceneFramebuffer.js @@ -8,6 +8,7 @@ import PixelDatatype from "../Renderer/PixelDatatype.js"; * @private */ function SceneFramebuffer() { + this._numSamples = 1; this._colorFramebuffer = new FramebufferManager({ depthStencil: true, supportsDepthTexture: true, @@ -42,9 +43,19 @@ Object.defineProperties(SceneFramebuffer.prototype, { return this._idFramebuffer.framebuffer; }, }, + depthStencilTexture: { + get: function () { + return this._colorFramebuffer.getDepthStencilTexture(); + }, + }, }); -SceneFramebuffer.prototype.update = function (context, viewport, hdr) { +SceneFramebuffer.prototype.update = function ( + context, + viewport, + hdr, + numSamples +) { const width = viewport.width; const height = viewport.height; const pixelDatatype = hdr @@ -52,7 +63,14 @@ SceneFramebuffer.prototype.update = function (context, viewport, hdr) { ? PixelDatatype.HALF_FLOAT : PixelDatatype.FLOAT : PixelDatatype.UNSIGNED_BYTE; - this._colorFramebuffer.update(context, width, height, pixelDatatype); + this._numSamples = numSamples; + this._colorFramebuffer.update( + context, + width, + height, + numSamples, + pixelDatatype + ); this._idFramebuffer.update(context, width, height); }; @@ -71,6 +89,12 @@ SceneFramebuffer.prototype.getIdFramebuffer = function () { return this._idFramebuffer.framebuffer; }; +SceneFramebuffer.prototype.prepareColorTextures = function (context) { + if (this._numSamples > 1) { + this._colorFramebuffer.prepareTextures(context); + } +}; + SceneFramebuffer.prototype.isDestroyed = function () { return false; }; diff --git a/Source/Scene/TranslucentTileClassification.js b/Source/Scene/TranslucentTileClassification.js index b21637f226db..4b36673d889c 100644 --- a/Source/Scene/TranslucentTileClassification.js +++ b/Source/Scene/TranslucentTileClassification.js @@ -133,14 +133,13 @@ function updateResources( transpClass, context, passState, - globeDepthFramebuffer + globeDepthStencilTexture ) { if (!transpClass.isSupported()) { return; } - transpClass._opaqueDepthStencilTexture = - globeDepthFramebuffer.depthStencilTexture; + transpClass._opaqueDepthStencilTexture = globeDepthStencilTexture; const width = transpClass._opaqueDepthStencilTexture.width; const height = transpClass._opaqueDepthStencilTexture.height; @@ -345,7 +344,7 @@ TranslucentTileClassification.prototype.executeTranslucentCommands = function ( executeCommand, passState, commands, - globeDepthFramebuffer + globeDepthStencilTexture ) { // Check for translucent commands that should be classified const length = commands.length; @@ -370,7 +369,7 @@ TranslucentTileClassification.prototype.executeTranslucentCommands = function ( return; } - updateResources(this, context, passState, globeDepthFramebuffer); + updateResources(this, context, passState, globeDepthStencilTexture); // Get translucent depth passState.framebuffer = this._drawClassificationFBO.framebuffer; diff --git a/Source/Widgets/CesiumWidget/CesiumWidget.js b/Source/Widgets/CesiumWidget/CesiumWidget.js index 58926d7e8b88..b470d38c3540 100644 --- a/Source/Widgets/CesiumWidget/CesiumWidget.js +++ b/Source/Widgets/CesiumWidget/CesiumWidget.js @@ -146,6 +146,7 @@ function configureCameraFrustum(widget) { * @param {MapMode2D} [options.mapMode2D=MapMode2D.INFINITE_SCROLL] Determines if the 2D map is rotatable or can be scrolled infinitely in the horizontal direction. * @param {Boolean} [options.requestRenderMode=false] If true, rendering a frame will only occur when needed as determined by changes within the scene. Enabling improves performance of the application, but requires using {@link Scene#requestRender} to render a new frame explicitly in this mode. This will be necessary in many cases after making changes to the scene in other parts of the API. See {@link https://cesium.com/blog/2018/01/24/cesium-scene-rendering-performance/|Improving Performance with Explicit Rendering}. * @param {Number} [options.maximumRenderTimeChange=0.0] If requestRenderMode is true, this value defines the maximum change in simulation time allowed before a render is requested. See {@link https://cesium.com/blog/2018/01/24/cesium-scene-rendering-performance/|Improving Performance with Explicit Rendering}. + * @param {Number} [options.msaaSamples=1] If provided, this value controls the rate of multisample antialiasing. Typical multisampling rates are 2, 4, and sometimes 8 samples per pixel. Higher sampling rates of MSAA may impact performance in exchange for improved visual quality. This value only applies to WebGL2 contexts that support multisample render targets. * * @exception {DeveloperError} Element with id "container" does not exist in the document. * @@ -273,6 +274,7 @@ function CesiumWidget(container, options) { requestRenderMode: options.requestRenderMode, maximumRenderTimeChange: options.maximumRenderTimeChange, depthPlaneEllipsoidOffset: options.depthPlaneEllipsoidOffset, + msaaSamples: options.msaaSamples, }); this._scene = scene; diff --git a/Source/Widgets/Viewer/Viewer.js b/Source/Widgets/Viewer/Viewer.js index 88d029cba752..4f2c16ba1acf 100644 --- a/Source/Widgets/Viewer/Viewer.js +++ b/Source/Widgets/Viewer/Viewer.js @@ -338,6 +338,7 @@ function enableVRUI(viewer, enabled) { * @property {Boolean} [requestRenderMode=false] If true, rendering a frame will only occur when needed as determined by changes within the scene. Enabling reduces the CPU/GPU usage of your application and uses less battery on mobile, but requires using {@link Scene#requestRender} to render a new frame explicitly in this mode. This will be necessary in many cases after making changes to the scene in other parts of the API. See {@link https://cesium.com/blog/2018/01/24/cesium-scene-rendering-performance/|Improving Performance with Explicit Rendering}. * @property {Number} [maximumRenderTimeChange=0.0] If requestRenderMode is true, this value defines the maximum change in simulation time allowed before a render is requested. See {@link https://cesium.com/blog/2018/01/24/cesium-scene-rendering-performance/|Improving Performance with Explicit Rendering}. * @property {Number} [depthPlaneEllipsoidOffset=0.0] Adjust the DepthPlane to address rendering artefacts below ellipsoid zero elevation. + * @property {Number} [msaaSamples=1] If provided, this value controls the rate of multisample antialiasing. Typical multisampling rates are 2, 4, and sometimes 8 samples per pixel. Higher sampling rates of MSAA may impact performance in exchange for improved visual quality. This value only applies to WebGL2 contexts that support multisample render targets. */ /** @@ -503,6 +504,7 @@ Either specify options.terrainProvider instead or set options.baseLayerPicker to requestRenderMode: options.requestRenderMode, maximumRenderTimeChange: options.maximumRenderTimeChange, depthPlaneEllipsoidOffset: options.depthPlaneEllipsoidOffset, + msaaSamples: options.msaaSamples, }); let dataSourceCollection = options.dataSources; diff --git a/Specs/Renderer/FramebufferManagerSpec.js b/Specs/Renderer/FramebufferManagerSpec.js index f6017c0d7dfe..b8c0967d358d 100644 --- a/Specs/Renderer/FramebufferManagerSpec.js +++ b/Specs/Renderer/FramebufferManagerSpec.js @@ -17,7 +17,7 @@ describe( let fbm; beforeAll(function () { - context = createContext(); + context = createContext({ requestWebgl2: true }); }); afterAll(function () { @@ -295,16 +295,35 @@ describe( depth: true, supportsDepthTexture: true, }); - // Disable extension + // Disable extensions const depthTexture = context._depthTexture; context._depthTexture = false; + const webgl2 = context._webgl2; + context._webgl2 = false; fbm.update(context, 1, 1); expect(fbm.getDepthTexture()).toBeUndefined(); expect(fbm.getDepthRenderbuffer()).toBeDefined(); - // Re-enable extension + // Re-enable extensions context._depthTexture = depthTexture; + context._webgl2 = webgl2; + }); + + it("doesn't create multisample resources if msaa is unsupported", function () { + fbm = new FramebufferManager({ + depth: true, + supportsDepthTexture: true, + }); + // Disable extensions + const webgl2 = context._webgl2; + context._webgl2 = false; + + fbm.update(context, 1, 1, 4); + expect(fbm._multisampleFramebuffer).toBeUndefined(); + + // Re-enable extensions + context._webgl2 = webgl2; }); it("destroys attachments and framebuffer", function () { @@ -383,13 +402,25 @@ describe( expect(FramebufferManager.prototype.destroy.calls.count()).toEqual(2); }); + it("destroys resources after numSamples changes", function () { + if (!context.webgl2) { + return; + } + + fbm = new FramebufferManager(); + spyOn(FramebufferManager.prototype, "destroy").and.callThrough(); + fbm.update(context, 1, 1); + fbm.update(context, 1, 1, 2); + expect(FramebufferManager.prototype.destroy.calls.count()).toEqual(2); + }); + it("destroys resources after pixel datatype changes", function () { fbm = new FramebufferManager({ pixelDatatype: PixelDatatype.UNSIGNED_INT, }); spyOn(FramebufferManager.prototype, "destroy").and.callThrough(); fbm.update(context, 1, 1); - fbm.update(context, 1, 1, PixelDatatype.UNSIGNED_BYTE); + fbm.update(context, 1, 1, 1, PixelDatatype.UNSIGNED_BYTE); expect(FramebufferManager.prototype.destroy.calls.count()).toEqual(2); }); @@ -399,7 +430,7 @@ describe( }); spyOn(FramebufferManager.prototype, "destroy").and.callThrough(); fbm.update(context, 1, 1); - fbm.update(context, 1, 1, undefined, PixelFormat.RGBA); + fbm.update(context, 1, 1, 1, undefined, PixelFormat.RGBA); expect(FramebufferManager.prototype.destroy.calls.count()).toEqual(2); }); diff --git a/Specs/Renderer/MultisampleFramebufferSpec.js b/Specs/Renderer/MultisampleFramebufferSpec.js new file mode 100644 index 000000000000..7f97bb26f66e --- /dev/null +++ b/Specs/Renderer/MultisampleFramebufferSpec.js @@ -0,0 +1,382 @@ +import { ClearCommand } from "../../Source/Cesium.js"; +import { Color } from "../../Source/Cesium.js"; +import { PrimitiveType } from "../../Source/Cesium.js"; +import { Buffer } from "../../Source/Cesium.js"; +import { BufferUsage } from "../../Source/Cesium.js"; +import { DrawCommand } from "../../Source/Cesium.js"; +import { MultisampleFramebuffer } from "../../Source/Cesium.js"; +import { PixelDatatype } from "../../Source/Cesium.js"; +import { PixelFormat } from "../../Source/Cesium.js"; +import { Texture } from "../../Source/Cesium.js"; +import { Renderbuffer } from "../../Source/Cesium.js"; +import { RenderbufferFormat } from "../../Source/Cesium.js"; +import { RenderState } from "../../Source/Cesium.js"; +import { ShaderProgram } from "../../Source/Cesium.js"; +import { VertexArray } from "../../Source/Cesium.js"; +import createContext from "../createContext.js"; + +describe( + "Renderer/MultisampleFramebuffer", + function () { + let context; + let sp; + let va; + let framebuffer; + + beforeAll(function () { + context = createContext({ requestWebgl2: true }); + }); + + afterAll(function () { + context.destroyForSpecs(); + }); + + afterEach(function () { + sp = sp && sp.destroy(); + va = va && va.destroy(); + framebuffer = framebuffer && framebuffer.destroy(); + }); + + it("throws when missing a color attachment", function () { + expect(function () { + framebuffer = new MultisampleFramebuffer({ + context: context, + width: 1, + height: 1, + colorTextures: [], + }); + }).toThrowDeveloperError(); + expect(function () { + framebuffer = new MultisampleFramebuffer({ + context: context, + width: 1, + height: 1, + colorRenderbuffers: [], + }); + }).toThrowDeveloperError(); + }); + + it("throws when missing a depth-stencil attachment", function () { + expect(function () { + framebuffer = new MultisampleFramebuffer({ + context: context, + width: 1, + height: 1, + depthStencilTexture: [], + }); + }).toThrowDeveloperError(); + expect(function () { + framebuffer = new MultisampleFramebuffer({ + context: context, + width: 1, + height: 1, + depthStencilRenderbuffer: [], + }); + }).toThrowDeveloperError(); + }); + + it("creates read and draw framebuffers", function () { + if (!context.depthTexture) { + return; + } + + framebuffer = new MultisampleFramebuffer({ + context: context, + width: 1, + height: 1, + colorTextures: [ + new Texture({ + context: context, + width: 1, + height: 1, + }), + ], + colorRenderbuffers: [ + new Renderbuffer({ + context: context, + format: RenderbufferFormat.RGBA8, + }), + ], + depthStencilTexture: new Texture({ + context: context, + width: 1, + height: 1, + pixelFormat: PixelFormat.DEPTH_STENCIL, + pixelDatatype: PixelDatatype.UNSIGNED_INT_24_8, + }), + depthStencilRenderbuffer: new Renderbuffer({ + context: context, + format: RenderbufferFormat.DEPTH_STENCIL, + }), + }); + const readFB = framebuffer.getRenderFramebuffer(); + const drawFB = framebuffer.getColorFramebuffer(); + expect(readFB).toBeDefined(); + expect(readFB.getColorRenderbuffer(0)).toBeDefined(); + expect(readFB.depthStencilRenderbuffer).toBeDefined(); + expect(drawFB).toBeDefined(); + expect(drawFB.getColorTexture(0)).toBeDefined(); + expect(drawFB.depthStencilTexture).toBeDefined(); + }); + + function renderColor(framebuffer, color) { + const vs = + "attribute vec4 position; void main() { gl_PointSize = 1.0; gl_Position = position; }"; + const fs = "uniform vec4 color; void main() { gl_FragColor = color; }"; + sp = ShaderProgram.fromCache({ + context: context, + vertexShaderSource: vs, + fragmentShaderSource: fs, + attributeLocations: { + position: 0, + }, + }); + + va = new VertexArray({ + context: context, + attributes: [ + { + index: 0, + vertexBuffer: Buffer.createVertexBuffer({ + context: context, + typedArray: new Float32Array([0, 0, 0, 1]), + usage: BufferUsage.STATIC_DRAW, + }), + componentsPerAttribute: 4, + }, + ], + }); + + const uniformMap = { + color: function () { + return color; + }, + }; + + const command = new DrawCommand({ + primitiveType: PrimitiveType.POINTS, + shaderProgram: sp, + vertexArray: va, + uniformMap: uniformMap, + framebuffer: framebuffer, + }); + command.execute(context); + } + + it("blits color attachments", function () { + if (!context.webgl2) { + return; + } + + framebuffer = new MultisampleFramebuffer({ + context: context, + width: 1, + height: 1, + colorTextures: [ + new Texture({ + context: context, + width: 1, + height: 1, + }), + ], + colorRenderbuffers: [ + new Renderbuffer({ + context: context, + format: RenderbufferFormat.RGBA8, + numSamples: 2, + }), + ], + }); + + const renderFB = framebuffer.getRenderFramebuffer(); + renderColor(renderFB, new Color(0.0, 1.0, 0.0, 1.0)); + framebuffer.blitFramebuffers(context); + const colorFB = framebuffer.getColorFramebuffer(); + expect({ + context: context, + framebuffer: colorFB, + }).toReadPixels([0, 255, 0, 255]); + }); + + function renderAndBlitDepthAttachment(framebuffer) { + const renderFB = framebuffer.getRenderFramebuffer(); + const colorFB = framebuffer.getColorFramebuffer(); + ClearCommand.ALL.execute(context); + + const framebufferClear = new ClearCommand({ + depth: 1.0, + framebuffer: renderFB, + }); + + framebufferClear.execute(context); + + // 1 of 3. Render green point into color attachment. + const vs = + "attribute vec4 position; void main() { gl_PointSize = 1.0; gl_Position = position; }"; + const fs = "void main() { gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0); }"; + sp = ShaderProgram.fromCache({ + context: context, + vertexShaderSource: vs, + fragmentShaderSource: fs, + attributeLocations: { + position: 0, + }, + }); + + va = new VertexArray({ + context: context, + attributes: [ + { + index: 0, + vertexBuffer: Buffer.createVertexBuffer({ + context: context, + typedArray: new Float32Array([0, 0, 0, 1]), + usage: BufferUsage.STATIC_DRAW, + }), + componentsPerAttribute: 4, + }, + ], + }); + + let command = new DrawCommand({ + primitiveType: PrimitiveType.POINTS, + shaderProgram: sp, + vertexArray: va, + framebuffer: renderFB, + renderState: RenderState.fromCache({ + depthTest: { + enabled: true, + }, + }), + }); + command.execute(context); + + // 2 of 3. Verify default color buffer is still black. + expect(context).toReadPixels([0, 0, 0, 255]); + + framebuffer.blitFramebuffers(context); + + // 3 of 3. Render green to default color buffer by reading from blitted color attachment + const vs2 = + "attribute vec4 position; void main() { gl_PointSize = 1.0; gl_Position = position; }"; + const fs2 = + "uniform sampler2D u_texture; void main() { gl_FragColor = texture2D(u_texture, vec2(0.0)).rrrr; }"; + let sp2 = ShaderProgram.fromCache({ + context: context, + vertexShaderSource: vs2, + fragmentShaderSource: fs2, + attributeLocations: { + position: 0, + }, + }); + const uniformMap = { + u_texture: function () { + return colorFB.depthStencilTexture; + }, + }; + + command = new DrawCommand({ + primitiveType: PrimitiveType.POINTS, + shaderProgram: sp2, + vertexArray: va, + uniformMap: uniformMap, + }); + command.execute(context); + + sp2 = sp2.destroy(); + + return context.readPixels(); + } + + it("blits depth-stencil attachments", function () { + if (!context.webgl2 || !context.depthTexture) { + return; + } + + framebuffer = new MultisampleFramebuffer({ + context: context, + width: 1, + height: 1, + colorTextures: [ + new Texture({ + context: context, + width: 1, + height: 1, + }), + ], + colorRenderbuffers: [ + new Renderbuffer({ + context: context, + format: RenderbufferFormat.RGBA8, + numSamples: 2, + }), + ], + depthStencilTexture: new Texture({ + context: context, + width: 1, + height: 1, + pixelFormat: PixelFormat.DEPTH_STENCIL, + pixelDatatype: PixelDatatype.UNSIGNED_INT_24_8, + }), + depthStencilRenderbuffer: new Renderbuffer({ + context: context, + width: 1, + height: 1, + format: RenderbufferFormat.DEPTH24_STENCIL8, + numSamples: 2, + }), + }); + + expect(renderAndBlitDepthAttachment(framebuffer)).toEqualEpsilon( + [128, 128, 128, 255], + 1 + ); + }); + + it("destroys", function () { + const f = new MultisampleFramebuffer({ + context: context, + width: 1, + height: 1, + }); + expect(f.isDestroyed()).toEqual(false); + f.destroy(); + expect(f.isDestroyed()).toEqual(true); + }); + + it("fails to destroy", function () { + const f = new MultisampleFramebuffer({ + context: context, + width: 1, + height: 1, + }); + f.destroy(); + + expect(function () { + f.destroy(); + }).toThrowDeveloperError(); + }); + + it("throws when there is no context", function () { + expect(function () { + return new MultisampleFramebuffer(); + }).toThrowDeveloperError(); + }); + + it("throws when there is no width or height", function () { + expect(function () { + return new MultisampleFramebuffer({ + context: context, + height: 1, + }); + }).toThrowDeveloperError(); + expect(function () { + return new MultisampleFramebuffer({ + context: context, + width: 1, + }); + }).toThrowDeveloperError(); + }); + }, + "WebGL" +); diff --git a/Specs/Scene/TranslucentTileClassificationSpec.js b/Specs/Scene/TranslucentTileClassificationSpec.js index dd12833b1669..03ba7ea19701 100644 --- a/Specs/Scene/TranslucentTileClassificationSpec.js +++ b/Specs/Scene/TranslucentTileClassificationSpec.js @@ -230,7 +230,7 @@ describe( executeCommand, passState, translucentPrimitive.commands, - globeDepthFramebuffer + undefined ); expectResources(translucentTileClassification, false); @@ -255,7 +255,7 @@ describe( executeCommand, passState, [], - globeDepthFramebuffer + globeDepthFramebuffer.depthStencilTexture ); expectResources(translucentTileClassification, false); @@ -265,7 +265,7 @@ describe( executeCommand, passState, translucentPrimitive.commands, - globeDepthFramebuffer + globeDepthFramebuffer.depthStencilTexture ); expectResources(translucentTileClassification, true); @@ -299,7 +299,7 @@ describe( executeCommand, passState, translucentPrimitive.commands, - globeDepthFramebuffer + globeDepthFramebuffer.depthStencilTexture ); expect(translucentTileClassification.hasTranslucentDepth).toBe(true); @@ -324,7 +324,7 @@ describe( executeCommand, passState, translucentPrimitive.commands, - globeDepthFramebuffer + globeDepthFramebuffer.depthStencilTexture ); const drawClassificationFBO = @@ -366,7 +366,7 @@ describe( executeCommand, passState, translucentPrimitive.commands, - globeDepthFramebuffer + globeDepthFramebuffer.depthStencilTexture ); const accumulationFBO = @@ -396,7 +396,7 @@ describe( executeCommand, passState, translucentPrimitive.commands, - globeDepthFramebuffer + globeDepthFramebuffer.depthStencilTexture ); translucentTileClassification.executeClassificationCommands( scene, @@ -439,7 +439,7 @@ describe( executeCommand, passState, [], - globeDepthFramebuffer + globeDepthFramebuffer.depthStencilTexture ); const preClassifyPixels = readPixels(drawClassificationFBO); @@ -493,7 +493,7 @@ describe( executeCommand, passState, translucentPrimitive.commands, - globeDepthFramebuffer + globeDepthFramebuffer.depthStencilTexture ); const frustumCommands = { @@ -574,7 +574,7 @@ describe( executeCommand, passState, translucentPrimitive.commands, - globeDepthFramebuffer + globeDepthFramebuffer.depthStencilTexture ); const frustumCommands = { @@ -600,7 +600,7 @@ describe( executeCommand, passState, translucentPrimitive.commands, - globeDepthFramebuffer + globeDepthFramebuffer.depthStencilTexture ); translucentTileClassification.executeClassificationCommands( @@ -659,7 +659,7 @@ describe( executeCommand, passState, [], - globeDepthFramebuffer + globeDepthFramebuffer.depthStencilTexture ); const frustumCommands = { @@ -705,7 +705,7 @@ describe( executeCommand, passState, translucentPrimitive.commands, - globeDepthFramebuffer + globeDepthFramebuffer.depthStencilTexture ); const drawClassificationFBO = diff --git a/Specs/getWebGLStub.js b/Specs/getWebGLStub.js index b11179893a37..9457b4f41ba2 100644 --- a/Specs/getWebGLStub.js +++ b/Specs/getWebGLStub.js @@ -222,6 +222,7 @@ function getParameterStub(options) { parameterStubValues[WebGLConstants.MAX_TEXTURE_MAX_ANISOTROPY_EXT] = 16; // Assuming extension parameterStubValues[WebGLConstants.MAX_DRAW_BUFFERS] = 8; // Assuming extension parameterStubValues[WebGLConstants.MAX_COLOR_ATTACHMENTS] = 8; // Assuming extension + parameterStubValues[WebGLConstants.MAX_SAMPLES] = 8; // Assuming WebGL2 return function (pname) { const value = parameterStubValues[pname];