diff --git a/extras/mini-stats/render2d.js b/extras/mini-stats/render2d.js index 0eaba2e98d0..701429bbfcb 100644 --- a/extras/mini-stats/render2d.js +++ b/extras/mini-stats/render2d.js @@ -14,7 +14,8 @@ import { shaderChunks, IndexBuffer, VertexBuffer, - VertexFormat + VertexFormat, + BlendState } from 'playcanvas'; // render 2d textured quads @@ -120,6 +121,9 @@ class Render2d { this.screenTextureSizeId = device.scope.resolve('screenAndTextureSize'); this.screenTextureSize = new Float32Array(4); + + this.blendState = new BlendState(true, BLENDEQUATION_ADD, BLENDMODE_SRC_ALPHA, BLENDMODE_ONE_MINUS_SRC_ALPHA, + BLENDEQUATION_ADD, BLENDMODE_ONE, BLENDMODE_ONE); } quad(texture, x, y, w, h, u, v, uw, uh, enabled) { @@ -174,12 +178,8 @@ class Render2d { device.setDepthTest(false); device.setDepthWrite(false); device.setCullMode(CULLFACE_NONE); - device.setBlending(true); - device.setBlendFunctionSeparate(BLENDMODE_SRC_ALPHA, - BLENDMODE_ONE_MINUS_SRC_ALPHA, - BLENDMODE_ONE, - BLENDMODE_ONE); - device.setBlendEquationSeparate(BLENDEQUATION_ADD, BLENDEQUATION_ADD); + device.setBlendState(this.blendState); + device.setVertexBuffer(buffer, 0); device.setIndexBuffer(this.indexBuffer); device.setShader(this.shader); diff --git a/src/deprecated/deprecated.js b/src/deprecated/deprecated.js index ba4a99d8d6b..d4c8ec65890 100644 --- a/src/deprecated/deprecated.js +++ b/src/deprecated/deprecated.js @@ -20,6 +20,7 @@ import { BLENDMODE_ZERO, BLENDMODE_ONE, BLENDMODE_SRC_COLOR, BLENDMODE_ONE_MINUS_SRC_COLOR, BLENDMODE_DST_COLOR, BLENDMODE_ONE_MINUS_DST_COLOR, BLENDMODE_SRC_ALPHA, BLENDMODE_SRC_ALPHA_SATURATE, BLENDMODE_ONE_MINUS_SRC_ALPHA, BLENDMODE_DST_ALPHA, BLENDMODE_ONE_MINUS_DST_ALPHA, + BLENDMODE_CONSTANT, BLENDMODE_ONE_MINUS_CONSTANT, BUFFER_STATIC, BUFFER_DYNAMIC, BUFFER_STREAM, CULLFACE_NONE, CULLFACE_BACK, CULLFACE_FRONT, CULLFACE_FRONTANDBACK, FILTER_NEAREST, FILTER_LINEAR, FILTER_NEAREST_MIPMAP_NEAREST, FILTER_NEAREST_MIPMAP_LINEAR, @@ -53,6 +54,7 @@ import { VertexFormat } from '../platform/graphics/vertex-format.js'; import { VertexIterator } from '../platform/graphics/vertex-iterator.js'; import { ShaderUtils } from '../platform/graphics/shader-utils.js'; import { GraphicsDeviceAccess } from '../platform/graphics/graphics-device-access.js'; +import { BlendState } from '../platform/graphics/blend-state.js'; import { PROJECTION_ORTHOGRAPHIC, PROJECTION_PERSPECTIVE, LAYERID_IMMEDIATE, LINEBATCH_OVERLAY, LAYERID_WORLD } from '../scene/constants.js'; import { calculateTangents, createBox, createCapsule, createCone, createCylinder, createMesh, createPlane, createSphere, createTorus } from '../scene/procedural.js'; @@ -346,6 +348,11 @@ export const PIXELFORMAT_R4_G4_B4_A4 = PIXELFORMAT_RGBA4; export const PIXELFORMAT_R8_G8_B8 = PIXELFORMAT_RGB8; export const PIXELFORMAT_R8_G8_B8_A8 = PIXELFORMAT_RGBA8; +export const BLENDMODE_CONSTANT_COLOR = BLENDMODE_CONSTANT; +export const BLENDMODE_ONE_MINUS_CONSTANT_COLOR = BLENDMODE_ONE_MINUS_CONSTANT; +export const BLENDMODE_CONSTANT_ALPHA = BLENDMODE_CONSTANT; +export const BLENDMODE_ONE_MINUS_CONSTANT_ALPHA = BLENDMODE_ONE_MINUS_CONSTANT; + export function UnsupportedBrowserError(message) { this.name = 'UnsupportedBrowserError'; this.message = (message || ''); @@ -572,6 +579,63 @@ GraphicsDevice.prototype.removeShaderFromCache = function (shader) { getProgramLibrary(this).removeFromCache(shader); }; +const _tempBlendState = new BlendState(); + +GraphicsDevice.prototype.setBlendFunction = function (blendSrc, blendDst) { + Debug.deprecated(`pc.GraphicsDevice#setBlendFunction is deprecated, use pc.GraphicsDevice.setBlendState instead.`); + const currentBlendState = this.blendState; + _tempBlendState.copy(currentBlendState); + _tempBlendState.setColorBlend(currentBlendState.colorOp, blendSrc, blendDst); + _tempBlendState.setAlphaBlend(currentBlendState.alphaOp, blendSrc, blendDst); + this.setBlendState(_tempBlendState); +}; + +GraphicsDevice.prototype.setBlendFunctionSeparate = function (blendSrc, blendDst, blendSrcAlpha, blendDstAlpha) { + Debug.deprecated(`pc.GraphicsDevice#setBlendFunctionSeparate is deprecated, use pc.GraphicsDevice.setBlendState instead.`); + const currentBlendState = this.blendState; + _tempBlendState.copy(currentBlendState); + _tempBlendState.setColorBlend(currentBlendState.colorOp, blendSrc, blendDst); + _tempBlendState.setAlphaBlend(currentBlendState.alphaOp, blendSrcAlpha, blendDstAlpha); + this.setBlendState(_tempBlendState); +}; + +GraphicsDevice.prototype.setBlendEquation = function (blendEquation) { + Debug.deprecated(`pc.GraphicsDevice#setBlendEquation is deprecated, use pc.GraphicsDevice.setBlendState instead.`); + const currentBlendState = this.blendState; + _tempBlendState.copy(currentBlendState); + _tempBlendState.setColorBlend(blendEquation, currentBlendState.colorSrcFactor, currentBlendState.colorDstFactor); + _tempBlendState.setAlphaBlend(blendEquation, currentBlendState.alphaSrcFactor, currentBlendState.alphaDstFactor); + this.setBlendState(_tempBlendState); +}; + +GraphicsDevice.prototype.setBlendEquationSeparate = function (blendEquation, blendAlphaEquation) { + Debug.deprecated(`pc.GraphicsDevice#setBlendEquationSeparate is deprecated, use pc.GraphicsDevice.setBlendState instead.`); + const currentBlendState = this.blendState; + _tempBlendState.copy(currentBlendState); + _tempBlendState.setColorBlend(blendEquation, currentBlendState.colorSrcFactor, currentBlendState.colorDstFactor); + _tempBlendState.setAlphaBlend(blendAlphaEquation, currentBlendState.alphaSrcFactor, currentBlendState.alphaDstFactor); + this.setBlendState(_tempBlendState); +}; + +GraphicsDevice.prototype.setColorWrite = function (redWrite, greenWrite, blueWrite, alphaWrite) { + Debug.deprecated(`pc.GraphicsDevice#setColorWrite is deprecated, use pc.GraphicsDevice.setBlendState instead.`); + const currentBlendState = this.blendState; + _tempBlendState.copy(currentBlendState); + _tempBlendState.setColorWrite(redWrite, greenWrite, blueWrite, alphaWrite); + this.setBlendState(_tempBlendState); +}; + +GraphicsDevice.prototype.getBlending = function () { + return this.blendState.blend; +}; + +GraphicsDevice.prototype.setBlending = function (blending) { + Debug.deprecated(`pc.GraphicsDevice#setBlending is deprecated, use pc.GraphicsDevice.setBlendState instead.`); + _tempBlendState.copy(this.blendState); + _tempBlendState.blend = blending; + this.setBlendState(_tempBlendState); +}; + // SCENE export const PhongMaterial = StandardMaterial; @@ -831,6 +895,47 @@ Material.prototype.setShader = function (shader) { this.shader = shader; }; +// Note: this is used by the Editor +Object.defineProperty(Material.prototype, 'blend', { + set: function (value) { + Debug.deprecated(`pc.Material#blend is deprecated, use pc.Material.blendState.`); + this.blendState.blend = value; + }, + get: function () { + return this.blendState.blend; + } +}); + +// Note: this is used by the Editor +Object.defineProperty(Material.prototype, 'blendSrc', { + set: function (value) { + Debug.deprecated(`pc.Material#blendSrc is deprecated, use pc.Material.blendState.`); + const currentBlendState = this.blendState; + _tempBlendState.copy(currentBlendState); + _tempBlendState.setColorBlend(currentBlendState.colorOp, value, currentBlendState.colorDstFactor); + _tempBlendState.setAlphaBlend(currentBlendState.alphaOp, value, currentBlendState.alphaDstFactor); + this.blendState = _tempBlendState; + }, + get: function () { + return this.blendState.colorSrcFactor; + } +}); + +// Note: this is used by the Editor +Object.defineProperty(Material.prototype, 'blendDst', { + set: function (value) { + Debug.deprecated(`pc.Material#blendDst is deprecated, use pc.Material.blendState.`); + const currentBlendState = this.blendState; + _tempBlendState.copy(currentBlendState); + _tempBlendState.setColorBlend(currentBlendState.colorOp, currentBlendState.colorSrcFactor, value); + _tempBlendState.setAlphaBlend(currentBlendState.alphaOp, currentBlendState.alphaSrcFactor, value); + this.blendState = _tempBlendState; + }, + get: function () { + return this.blendState.colorDstFactor; + } +}); + function _defineAlias(newName, oldName) { Object.defineProperty(StandardMaterial.prototype, oldName, { get: function () { diff --git a/src/framework/graphics/picker.js b/src/framework/graphics/picker.js index 0387a814123..06c95dd3f08 100644 --- a/src/framework/graphics/picker.js +++ b/src/framework/graphics/picker.js @@ -15,6 +15,7 @@ import { LayerComposition } from '../../scene/composition/layer-composition.js'; import { getApplication } from '../globals.js'; import { Entity } from '../entity.js'; import { Debug } from '../../core/debug.js'; +import { BlendState } from '../../platform/graphics/blend-state.js'; const tempSet = new Set(); @@ -208,7 +209,7 @@ class Picker { self.pickColor[1] = ((index >> 8) & 0xff) / 255; self.pickColor[2] = (index & 0xff) / 255; pickColorId.setValue(self.pickColor); - device.setBlending(false); + device.setBlendState(BlendState.DEFAULT); // keep the index -> meshInstance index mapping self.mapping[index] = meshInstance; diff --git a/src/framework/lightmapper/lightmapper.js b/src/framework/lightmapper/lightmapper.js index cd4a5bebb7a..e3337079eac 100644 --- a/src/framework/lightmapper/lightmapper.js +++ b/src/framework/lightmapper/lightmapper.js @@ -44,6 +44,7 @@ import { BakeLightAmbient } from './bake-light-ambient.js'; import { BakeMeshNode } from './bake-mesh-node.js'; import { LightmapCache } from '../../scene/graphics/lightmap-cache.js'; import { LightmapFilters } from './lightmap-filters.js'; +import { BlendState } from '../../platform/graphics/blend-state.js'; const MAX_LIGHTMAP_SIZE = 2048; @@ -874,6 +875,8 @@ class Lightmapper { this.lightmapFilters.prepareDenoise(this.scene.lightmapFilterRange, this.scene.lightmapFilterSmoothness); } + device.setBlendState(BlendState.DEFAULT); + for (let node = 0; node < bakeNodes.length; node++) { const bakeNode = bakeNodes[node]; diff --git a/src/index.js b/src/index.js index 113756ae514..73d59bc7ab2 100644 --- a/src/index.js +++ b/src/index.js @@ -57,6 +57,7 @@ export * from './platform/audio/constants.js'; // PLATFORM / GRAPHICS export * from './platform/graphics/constants.js'; export { createGraphicsDevice } from './platform/graphics/graphics-device-create.js'; +export { BlendState } from './platform/graphics/blend-state.js'; export { GraphicsDevice } from './platform/graphics/graphics-device.js'; export { IndexBuffer } from './platform/graphics/index-buffer.js'; export { RenderTarget } from './platform/graphics/render-target.js'; diff --git a/src/platform/graphics/blend-state.js b/src/platform/graphics/blend-state.js new file mode 100644 index 00000000000..b141561b137 --- /dev/null +++ b/src/platform/graphics/blend-state.js @@ -0,0 +1,232 @@ +import { BitPacking } from "../../core/math/bit-packing.js"; +import { BLENDEQUATION_ADD, BLENDMODE_ONE, BLENDMODE_ZERO } from '../../platform/graphics/constants.js'; + +// masks (to only keep relevant bits) +const opMask = 0b111; +const factorMask = 0b1111; + +// shifts values to where individual parts are stored +const colorOpShift = 0; // 00 - 02 (3bits) +const colorSrcFactorShift = 3; // 03 - 06 (4bits) +const colorDstFactorShift = 7; // 07 - 10 (4bits) +const alphaOpShift = 11; // 11 - 13 (3bits) +const alphaSrcFactorShift = 14; // 14 - 17 (4bits) +const alphaDstFactorShift = 18; // 18 - 21 (4bits) +const redWriteShift = 22; // 22 (1 bit) +const greenWriteShift = 23; // 23 (1 bit) +const blueWriteShift = 24; // 24 (1 bit) +const alphaWriteShift = 25; // 25 (1 bit) +const blendShift = 26; // 26 (1 bit) + +// combined values access +const allWriteMasks = 0b1111; +const allWriteShift = redWriteShift; +/** + * BlendState is a descriptor that defines how output of fragment shader is written and blended + * into render target. A blend state can be set on a material using {@link Material#blendState}, + * or in some cases on the graphics device using {@link GraphicsDevice#setBlendState}. + * + * For the best performance, do not modify blend state after it has been created, but create + * multiple blend states and assign them to the material or graphics device as needed. + */ +class BlendState { + /** + * Number bits of which represent the blend state for render target 0. + * + * @private + */ + target0 = 0; + + /** + * Create a new BlendState instance. + * + * All factor parameters can take the following values: + * + * - {@link BLENDMODE_ZERO} + * - {@link BLENDMODE_ONE} + * - {@link BLENDMODE_SRC_COLOR} + * - {@link BLENDMODE_ONE_MINUS_SRC_COLOR} + * - {@link BLENDMODE_DST_COLOR} + * - {@link BLENDMODE_ONE_MINUS_DST_COLOR} + * - {@link BLENDMODE_SRC_ALPHA} + * - {@link BLENDMODE_SRC_ALPHA_SATURATE} + * - {@link BLENDMODE_ONE_MINUS_SRC_ALPHA} + * - {@link BLENDMODE_DST_ALPHA} + * - {@link BLENDMODE_ONE_MINUS_DST_ALPHA} + * - {@link BLENDMODE_CONSTANT} + * - {@link BLENDMODE_ONE_MINUS_CONSTANT} + * + * All op parameters can take the following values: + * + * - {@link BLENDEQUATION_ADD} + * - {@link BLENDEQUATION_SUBTRACT} + * - {@link BLENDEQUATION_REVERSE_SUBTRACT} + * - {@link BLENDEQUATION_MIN} + * - {@link BLENDEQUATION_MAX} + * + * Note that MIN and MAX operations on WebGL platform require either EXT_blend_minmax or WebGL2 + * to work (check device.extBlendMinmax). + * + * @param {boolean} blend - Enables or disables blending. Defaults to false. + * @param {number} colorOp - Configures color blending operation. Defaults to + * {@link BLENDEQUATION_ADD}. + * @param {number} colorSrcFactor - Configures source color blending factor. Defaults to + * {@link BLENDMODE_ONE}. + * @param {number} colorDstFactor - Configures destination color blending factor. Defaults to + * {@link BLENDMODE_ZERO}. + * @param {number} alphaOp - Configures alpha blending operation. Defaults to + * {@link BLENDEQUATION_ADD}. + * @param {number} alphaSrcFactor - Configures source alpha blending factor. Defaults to + * {@link BLENDMODE_ONE}. + * @param {number} alphaDstFactor - Configures destination alpha blending factor. Defaults to + * {@link BLENDMODE_ZERO}. + * @param {boolean} redWrite - True to enable writing of the red channel and false otherwise. + * Defaults to true. + * @param {boolean} greenWrite - True to enable writing of the green channel and false + * otherwise. Defaults to true. + * @param {boolean} blueWrite - True to enable writing of the blue channel and false otherwise. + * Defaults to true. + * @param {boolean} alphaWrite - True to enable writing of the alpha channel and false + * otherwise. Defaults to true. + */ + constructor(blend = false, colorOp = BLENDEQUATION_ADD, colorSrcFactor = BLENDMODE_ONE, colorDstFactor = BLENDMODE_ZERO, + alphaOp, alphaSrcFactor, alphaDstFactor, + redWrite = true, greenWrite = true, blueWrite = true, alphaWrite = true) { + this.setColorBlend(colorOp, colorSrcFactor, colorDstFactor); + this.setAlphaBlend(alphaOp ?? colorOp, alphaSrcFactor ?? colorSrcFactor, alphaDstFactor ?? colorDstFactor); + this.setColorWrite(redWrite, greenWrite, blueWrite, alphaWrite); + this.blend = blend; + } + + /** + * Enables or disables blending. + * + * @type {boolean} + */ + set blend(value) { + this.target0 = BitPacking.set(this.target0, value ? 1 : 0, blendShift); + } + + get blend() { + return BitPacking.all(this.target0, blendShift); + } + + setColorBlend(op, srcFactor, dstFactor) { + this.target0 = BitPacking.set(this.target0, op, colorOpShift, opMask); + this.target0 = BitPacking.set(this.target0, srcFactor, colorSrcFactorShift, factorMask); + this.target0 = BitPacking.set(this.target0, dstFactor, colorDstFactorShift, factorMask); + } + + setAlphaBlend(op, srcFactor, dstFactor) { + this.target0 = BitPacking.set(this.target0, op, alphaOpShift, opMask); + this.target0 = BitPacking.set(this.target0, srcFactor, alphaSrcFactorShift, factorMask); + this.target0 = BitPacking.set(this.target0, dstFactor, alphaDstFactorShift, factorMask); + } + + setColorWrite(redWrite, greenWrite, blueWrite, alphaWrite) { + this.redWrite = redWrite; + this.greenWrite = greenWrite; + this.blueWrite = blueWrite; + this.alphaWrite = alphaWrite; + } + + get colorOp() { + return BitPacking.get(this.target0, colorOpShift, opMask); + } + + get colorSrcFactor() { + return BitPacking.get(this.target0, colorSrcFactorShift, factorMask); + } + + get colorDstFactor() { + return BitPacking.get(this.target0, colorDstFactorShift, factorMask); + } + + get alphaOp() { + return BitPacking.get(this.target0, alphaOpShift, opMask); + } + + get alphaSrcFactor() { + return BitPacking.get(this.target0, alphaSrcFactorShift, factorMask); + } + + get alphaDstFactor() { + return BitPacking.get(this.target0, alphaDstFactorShift, factorMask); + } + + set redWrite(value) { + this.target0 = BitPacking.set(this.target0, value ? 1 : 0, redWriteShift); + } + + get redWrite() { + return BitPacking.all(this.target0, redWriteShift); + } + + set greenWrite(value) { + this.target0 = BitPacking.set(this.target0, value ? 1 : 0, greenWriteShift); + } + + get greenWrite() { + return BitPacking.all(this.target0, greenWriteShift); + } + + set blueWrite(value) { + this.target0 = BitPacking.set(this.target0, value ? 1 : 0, blueWriteShift); + } + + get blueWrite() { + return BitPacking.all(this.target0, blueWriteShift); + } + + set alphaWrite(value) { + this.target0 = BitPacking.set(this.target0, value ? 1 : 0, alphaWriteShift); + } + + get alphaWrite() { + return BitPacking.all(this.target0, alphaWriteShift); + } + + get allWrite() { + // return a number with all 4 bits, for fast compare + return BitPacking.get(this.target0, allWriteShift, allWriteMasks); + } + + copy(rhs) { + this.target0 = rhs.target0; + return this; + } + + /** + * Returns an identical copy of the specified blend state. + * + * @returns {this} The result of the cloning. + */ + clone() { + const clone = new this.constructor(); + return clone.copy(this); + } + + get key() { + return this.target0; + } + + /** + * Reports whether two BlendStates are equal. + * + * @param {BlendState} rhs - The blend state to compare to. + * @returns {boolean} True if the blend states are equal and false otherwise. + */ + equals(rhs) { + return this.target0 === rhs.target0; + } + + /** + * A default blend state, that is not blending and writes to all color channels. + * + * @type {BlendState} + * @readonly + */ + static DEFAULT = Object.freeze(new BlendState()); +} + +export { BlendState }; diff --git a/src/platform/graphics/constants.js b/src/platform/graphics/constants.js index e07267e41a9..7c041f48e57 100644 --- a/src/platform/graphics/constants.js +++ b/src/platform/graphics/constants.js @@ -98,32 +98,18 @@ export const BLENDMODE_DST_ALPHA = 9; export const BLENDMODE_ONE_MINUS_DST_ALPHA = 10; /** - * Multiplies all colors by a constant color. + * Multiplies all fragment components by a constant. * * @type {number} */ -export const BLENDMODE_CONSTANT_COLOR = 11; +export const BLENDMODE_CONSTANT = 11; /** - * Multiplies all colors by 1 minus a constant color. + * Multiplies all fragment components by 1 minus a constant. * * @type {number} */ -export const BLENDMODE_ONE_MINUS_CONSTANT_COLOR = 12; - -/** - * Multiplies all colors by a constant alpha value. - * - * @type {number} - */ -export const BLENDMODE_CONSTANT_ALPHA = 13; - -/** - * Multiplies all colors by 1 minus a constant alpha value. - * - * @type {number} - */ -export const BLENDMODE_ONE_MINUS_CONSTANT_ALPHA = 14; +export const BLENDMODE_ONE_MINUS_CONSTANT = 12; /** * Add the results of the source and destination fragment multiplies. diff --git a/src/platform/graphics/graphics-device.js b/src/platform/graphics/graphics-device.js index 2f4286c0c40..d1b66099532 100644 --- a/src/platform/graphics/graphics-device.js +++ b/src/platform/graphics/graphics-device.js @@ -2,6 +2,7 @@ import { Debug } from '../../core/debug.js'; import { EventHandler } from '../../core/event-handler.js'; import { platform } from '../../core/platform.js'; import { now } from '../../core/time.js'; +import { BlendState } from './blend-state.js'; import { BUFFER_STATIC, @@ -134,6 +135,13 @@ class GraphicsDevice extends EventHandler { */ quadVertexBuffer; + /** + * An object representing current blend state + * + * @ignore + */ + blendState = new BlendState(); + constructor(canvas) { super(); @@ -259,11 +267,23 @@ class GraphicsDevice extends EventHandler { } initializeRenderState() { + + this.blendState = new BlendState(); + // Cached viewport and scissor dimensions this.vx = this.vy = this.vw = this.vh = 0; this.sx = this.sy = this.sw = this.sh = 0; } + /** + * Sets the specified blend state. + * + * @param {BlendState} blendState - New blend state. + */ + setBlendState(blendState) { + Debug.assert(false); + } + /** * Sets the specified render target on the device. If null is passed as a parameter, the back * buffer becomes the current target for all rendering operations. diff --git a/src/platform/graphics/webgl/webgl-graphics-device.js b/src/platform/graphics/webgl/webgl-graphics-device.js index d9436180062..d8d528142e6 100644 --- a/src/platform/graphics/webgl/webgl-graphics-device.js +++ b/src/platform/graphics/webgl/webgl-graphics-device.js @@ -5,8 +5,6 @@ import { Color } from '../../../core/math/color.js'; import { ADDRESS_CLAMP_TO_EDGE, - BLENDEQUATION_ADD, - BLENDMODE_ZERO, BLENDMODE_ONE, CLEARFLAG_COLOR, CLEARFLAG_DEPTH, CLEARFLAG_STENCIL, CULLFACE_BACK, CULLFACE_NONE, FILTER_NEAREST, FILTER_LINEAR, FILTER_NEAREST_MIPMAP_NEAREST, FILTER_NEAREST_MIPMAP_LINEAR, @@ -35,6 +33,7 @@ import { WebglTexture } from './webgl-texture.js'; import { WebglRenderTarget } from './webgl-render-target.js'; import { ShaderUtils } from '../shader-utils.js'; import { Shader } from '../shader.js'; +import { BlendState } from '../blend-state.js'; const invalidateAttachments = []; @@ -89,14 +88,10 @@ function quadWithShader(device, target, shader) { const oldDepthTest = device.getDepthTest(); const oldDepthWrite = device.getDepthWrite(); const oldCullMode = device.getCullMode(); - const oldWR = device.writeRed; - const oldWG = device.writeGreen; - const oldWB = device.writeBlue; - const oldWA = device.writeAlpha; device.setDepthTest(false); device.setDepthWrite(false); device.setCullMode(CULLFACE_NONE); - device.setColorWrite(true, true, true, true); + device.setBlendState(BlendState.DEFAULT); device.setVertexBuffer(device.quadVertexBuffer, 0); device.setShader(shader); @@ -111,7 +106,6 @@ function quadWithShader(device, target, shader) { device.setDepthTest(oldDepthTest); device.setDepthWrite(oldDepthWrite); device.setCullMode(oldCullMode); - device.setColorWrite(oldWR, oldWG, oldWB, oldWA); device.updateEnd(); @@ -471,7 +465,7 @@ class WebglGraphicsDevice extends GraphicsDevice { this.webgl2 ? gl.MAX : this.extBlendMinmax ? this.extBlendMinmax.MAX_EXT : gl.FUNC_ADD ]; - this.glBlendFunction = [ + this.glBlendFunctionColor = [ gl.ZERO, gl.ONE, gl.SRC_COLOR, @@ -484,7 +478,21 @@ class WebglGraphicsDevice extends GraphicsDevice { gl.DST_ALPHA, gl.ONE_MINUS_DST_ALPHA, gl.CONSTANT_COLOR, - gl.ONE_MINUS_CONSTANT_COLOR, + gl.ONE_MINUS_CONSTANT_COLOR + ]; + + this.glBlendFunctionAlpha = [ + gl.ZERO, + gl.ONE, + gl.SRC_COLOR, + gl.ONE_MINUS_SRC_COLOR, + gl.DST_COLOR, + gl.ONE_MINUS_DST_COLOR, + gl.SRC_ALPHA, + gl.SRC_ALPHA_SATURATE, + gl.ONE_MINUS_SRC_ALPHA, + gl.DST_ALPHA, + gl.ONE_MINUS_DST_ALPHA, gl.CONSTANT_ALPHA, gl.ONE_MINUS_CONSTANT_ALPHA ]; @@ -1035,29 +1043,16 @@ class WebglGraphicsDevice extends GraphicsDevice { const gl = this.gl; // Initialize render state to a known start state - this.blending = false; - gl.disable(gl.BLEND); - this.blendSrc = BLENDMODE_ONE; - this.blendDst = BLENDMODE_ZERO; - this.blendSrcAlpha = BLENDMODE_ONE; - this.blendDstAlpha = BLENDMODE_ZERO; - this.separateAlphaBlend = false; - this.blendEquation = BLENDEQUATION_ADD; - this.blendAlphaEquation = BLENDEQUATION_ADD; - this.separateAlphaEquation = false; + // default blend state + gl.disable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ZERO); gl.blendEquation(gl.FUNC_ADD); + gl.colorMask(true, true, true, true); this.blendColor = new Color(0, 0, 0, 0); gl.blendColor(0, 0, 0, 0); - this.writeRed = true; - this.writeGreen = true; - this.writeBlue = true; - this.writeAlpha = true; - gl.colorMask(true, true, true, true); - this.cullMode = CULLFACE_BACK; gl.enable(gl.CULL_FACE); gl.cullFace(gl.BACK); @@ -2074,7 +2069,7 @@ class WebglGraphicsDevice extends GraphicsDevice { if (flags & CLEARFLAG_COLOR) { const color = options.color ?? defaultOptions.color; this.setClearColor(color[0], color[1], color[2], color[3]); - this.setColorWrite(true, true, true, true); + this.setBlendState(BlendState.DEFAULT); } if (flags & CLEARFLAG_DEPTH) { @@ -2235,31 +2230,6 @@ class WebglGraphicsDevice extends GraphicsDevice { } } - /** - * Enables or disables writes to the color buffer. Once this state is set, it persists until it - * is changed. By default, color writes are enabled for all color channels. - * - * @param {boolean} writeRed - True to enable writing of the red channel and false otherwise. - * @param {boolean} writeGreen - True to enable writing of the green channel and false otherwise. - * @param {boolean} writeBlue - True to enable writing of the blue channel and false otherwise. - * @param {boolean} writeAlpha - True to enable writing of the alpha channel and false otherwise. - * @example - * // Just write alpha into the frame buffer - * device.setColorWrite(false, false, false, true); - */ - setColorWrite(writeRed, writeGreen, writeBlue, writeAlpha) { - if ((this.writeRed !== writeRed) || - (this.writeGreen !== writeGreen) || - (this.writeBlue !== writeBlue) || - (this.writeAlpha !== writeAlpha)) { - this.gl.colorMask(writeRed, writeGreen, writeBlue, writeAlpha); - this.writeRed = writeRed; - this.writeGreen = writeGreen; - this.writeBlue = writeBlue; - this.writeAlpha = writeAlpha; - } - } - /** * Enables or disables alpha to coverage (WebGL2 only). * @@ -2356,32 +2326,6 @@ class WebglGraphicsDevice extends GraphicsDevice { this.gl.polygonOffset(slopeBias, constBias); } - /** - * Queries whether blending is enabled. - * - * @returns {boolean} True if blending is enabled and false otherwise. - */ - getBlending() { - return this.blending; - } - - /** - * Enables or disables blending. - * - * @param {boolean} blending - True to enable blending and false to disable it. - */ - setBlending(blending) { - if (this.blending !== blending) { - const gl = this.gl; - if (blending) { - gl.enable(gl.BLEND); - } else { - gl.disable(gl.BLEND); - } - this.blending = blending; - } - } - /** * Enables or disables stencil test. * @@ -2595,115 +2539,44 @@ class WebglGraphicsDevice extends GraphicsDevice { } } - /** - * Configures blending operations. Both source and destination blend modes can take the - * following values: - * - * - {@link BLENDMODE_ZERO} - * - {@link BLENDMODE_ONE} - * - {@link BLENDMODE_SRC_COLOR} - * - {@link BLENDMODE_ONE_MINUS_SRC_COLOR} - * - {@link BLENDMODE_DST_COLOR} - * - {@link BLENDMODE_ONE_MINUS_DST_COLOR} - * - {@link BLENDMODE_SRC_ALPHA} - * - {@link BLENDMODE_SRC_ALPHA_SATURATE} - * - {@link BLENDMODE_ONE_MINUS_SRC_ALPHA} - * - {@link BLENDMODE_DST_ALPHA} - * - {@link BLENDMODE_ONE_MINUS_DST_ALPHA} - * - {@link BLENDMODE_CONSTANT_COLOR} - * - {@link BLENDMODE_ONE_MINUS_CONSTANT_COLOR} - * - {@link BLENDMODE_CONSTANT_ALPHA} - * - {@link BLENDMODE_ONE_MINUS_CONSTANT_ALPHA} - * - * @param {number} blendSrc - The source blend function. - * @param {number} blendDst - The destination blend function. - */ - setBlendFunction(blendSrc, blendDst) { - if (this.blendSrc !== blendSrc || this.blendDst !== blendDst || this.separateAlphaBlend) { - this.gl.blendFunc(this.glBlendFunction[blendSrc], this.glBlendFunction[blendDst]); - this.blendSrc = blendSrc; - this.blendDst = blendDst; - this.separateAlphaBlend = false; - } - } + setBlendState(blendState) { + const currentBlendState = this.blendState; + if (!currentBlendState.equals(blendState)) { + const gl = this.gl; - /** - * Configures blending operations. Both source and destination blend modes can take the - * following values: - * - * - {@link BLENDMODE_ZERO} - * - {@link BLENDMODE_ONE} - * - {@link BLENDMODE_SRC_COLOR} - * - {@link BLENDMODE_ONE_MINUS_SRC_COLOR} - * - {@link BLENDMODE_DST_COLOR} - * - {@link BLENDMODE_ONE_MINUS_DST_COLOR} - * - {@link BLENDMODE_SRC_ALPHA} - * - {@link BLENDMODE_SRC_ALPHA_SATURATE} - * - {@link BLENDMODE_ONE_MINUS_SRC_ALPHA} - * - {@link BLENDMODE_DST_ALPHA} - * - {@link BLENDMODE_ONE_MINUS_DST_ALPHA} - * - * @param {number} blendSrc - The source blend function. - * @param {number} blendDst - The destination blend function. - * @param {number} blendSrcAlpha - The separate source blend function for the alpha channel. - * @param {number} blendDstAlpha - The separate destination blend function for the alpha channel. - */ - setBlendFunctionSeparate(blendSrc, blendDst, blendSrcAlpha, blendDstAlpha) { - if (this.blendSrc !== blendSrc || this.blendDst !== blendDst || this.blendSrcAlpha !== blendSrcAlpha || this.blendDstAlpha !== blendDstAlpha || !this.separateAlphaBlend) { - this.gl.blendFuncSeparate(this.glBlendFunction[blendSrc], this.glBlendFunction[blendDst], - this.glBlendFunction[blendSrcAlpha], this.glBlendFunction[blendDstAlpha]); - this.blendSrc = blendSrc; - this.blendDst = blendDst; - this.blendSrcAlpha = blendSrcAlpha; - this.blendDstAlpha = blendDstAlpha; - this.separateAlphaBlend = true; - } - } + // state values to set + const { blend, colorOp, alphaOp, colorSrcFactor, colorDstFactor, alphaSrcFactor, alphaDstFactor } = blendState; - /** - * Configures the blending equation. The default blend equation is {@link BLENDEQUATION_ADD}. - * - * @param {number} blendEquation - The blend equation. Can be: - * - * - {@link BLENDEQUATION_ADD} - * - {@link BLENDEQUATION_SUBTRACT} - * - {@link BLENDEQUATION_REVERSE_SUBTRACT} - * - {@link BLENDEQUATION_MIN} - * - {@link BLENDEQUATION_MAX} - * - * Note that MIN and MAX modes require either EXT_blend_minmax or WebGL2 to work (check - * device.extBlendMinmax). - */ - setBlendEquation(blendEquation) { - if (this.blendEquation !== blendEquation || this.separateAlphaEquation) { - this.gl.blendEquation(this.glBlendEquation[blendEquation]); - this.blendEquation = blendEquation; - this.separateAlphaEquation = false; - } - } + // enable blend + if (currentBlendState.blend !== blend) { + if (blend) { + gl.enable(gl.BLEND); + } else { + gl.disable(gl.BLEND); + } + } - /** - * Configures the blending equation. The default blend equation is {@link BLENDEQUATION_ADD}. - * - * @param {number} blendEquation - The blend equation. Can be: - * - * - {@link BLENDEQUATION_ADD} - * - {@link BLENDEQUATION_SUBTRACT} - * - {@link BLENDEQUATION_REVERSE_SUBTRACT} - * - {@link BLENDEQUATION_MIN} - * - {@link BLENDEQUATION_MAX} - * - * Note that MIN and MAX modes require either EXT_blend_minmax or WebGL2 to work (check - * device.extBlendMinmax). - * @param {number} blendAlphaEquation - A separate blend equation for the alpha channel. - * Accepts same values as `blendEquation`. - */ - setBlendEquationSeparate(blendEquation, blendAlphaEquation) { - if (this.blendEquation !== blendEquation || this.blendAlphaEquation !== blendAlphaEquation || !this.separateAlphaEquation) { - this.gl.blendEquationSeparate(this.glBlendEquation[blendEquation], this.glBlendEquation[blendAlphaEquation]); - this.blendEquation = blendEquation; - this.blendAlphaEquation = blendAlphaEquation; - this.separateAlphaEquation = true; + // blend ops + if (currentBlendState.colorOp !== colorOp || currentBlendState.alphaOp !== alphaOp) { + const glBlendEquation = this.glBlendEquation; + gl.blendEquationSeparate(glBlendEquation[colorOp], glBlendEquation[alphaOp]); + } + + // blend factors + if (currentBlendState.colorSrcFactor !== colorSrcFactor || currentBlendState.colorDstFactor !== colorDstFactor || + currentBlendState.alphaSrcFactor !== alphaSrcFactor || currentBlendState.alphaDstFactor !== alphaDstFactor) { + + gl.blendFuncSeparate(this.glBlendFunctionColor[colorSrcFactor], this.glBlendFunctionColor[colorDstFactor], + this.glBlendFunctionAlpha[alphaSrcFactor], this.glBlendFunctionAlpha[alphaDstFactor]); + } + + // color write + if (currentBlendState.allWrite !== blendState.allWrite) { + this.gl.colorMask(blendState.redWrite, blendState.greenWrite, blendState.blueWrite, blendState.alphaWrite); + } + + // update internal state + currentBlendState.copy(blendState); } } diff --git a/src/platform/graphics/webgpu/webgpu-graphics-device.js b/src/platform/graphics/webgpu/webgpu-graphics-device.js index 9faf7f6f71c..eb9f2e25f21 100644 --- a/src/platform/graphics/webgpu/webgpu-graphics-device.js +++ b/src/platform/graphics/webgpu/webgpu-graphics-device.js @@ -10,7 +10,6 @@ import { WebgpuBindGroup } from './webgpu-bind-group.js'; import { WebgpuBindGroupFormat } from './webgpu-bind-group-format.js'; import { WebgpuIndexBuffer } from './webgpu-index-buffer.js'; import { WebgpuRenderPipeline } from './webgpu-render-pipeline.js'; -import { WebgpuRenderState } from './webgpu-render-state.js'; import { WebgpuRenderTarget } from './webgpu-render-target.js'; import { WebgpuShader } from './webgpu-shader.js'; import { WebgpuTexture } from './webgpu-texture.js'; @@ -27,13 +26,6 @@ class WebgpuGraphicsDevice extends GraphicsDevice { */ frameBuffer; - /** - * Internal representation of the current render state, as requested by the renderer. - * In the future this can be completely replaced by a more optimal solution, where - * render states are bundled together (DX11 style) and set using a single call. - */ - renderState = new WebgpuRenderState(); - /** * Object responsible for caching and creation of render pipelines. */ @@ -71,12 +63,6 @@ class WebgpuGraphicsDevice extends GraphicsDevice { super(canvas); this.isWebGPU = true; - // TODO: refactor as needed - this.writeRed = true; - this.writeGreen = true; - this.writeBlue = true; - this.writeAlpha = true; - this.initDeviceCaps(); } @@ -111,6 +97,7 @@ class WebgpuGraphicsDevice extends GraphicsDevice { this.boneLimit = 1024; this.supportsImageBitmap = true; this.extStandardDerivatives = true; + this.extBlendMinmax = true; this.areaLightLutFormat = PIXELFORMAT_RGBA32F; this.supportsTextureFetch = true; } @@ -286,7 +273,7 @@ class WebgpuGraphicsDevice extends GraphicsDevice { // render pipeline const pipeline = this.renderPipeline.get(primitive, vb0.format, vb1?.format, this.shader, this.renderTarget, - this.bindGroupFormats, this.renderState); + this.bindGroupFormats, this.blendState); Debug.assert(pipeline); if (this.pipeline !== pipeline) { @@ -318,16 +305,12 @@ class WebgpuGraphicsDevice extends GraphicsDevice { return true; } - setBlending(blending) { - this.renderState.setBlending(blending); + setBlendState(blendState) { + this.blendState.copy(blendState); } - setBlendFunction(blendSrc, blendDst) { - this.renderState.setBlendFunction(blendSrc, blendDst); - } - - setBlendEquation(blendEquation) { - this.renderState.setBlendEquation(blendEquation); + setBlendColor(r, g, b, a) { + // TODO: this should use passEncoder.setBlendConstant(color) } setDepthFunc(func) { @@ -350,9 +333,6 @@ class WebgpuGraphicsDevice extends GraphicsDevice { setAlphaToCoverage(state) { } - setColorWrite(writeRed, writeGreen, writeBlue, writeAlpha) { - } - setDepthWrite(writeDepth) { } diff --git a/src/platform/graphics/webgpu/webgpu-render-pipeline.js b/src/platform/graphics/webgpu/webgpu-render-pipeline.js index b157ed4fe0f..41cff61fec1 100644 --- a/src/platform/graphics/webgpu/webgpu-render-pipeline.js +++ b/src/platform/graphics/webgpu/webgpu-render-pipeline.js @@ -36,10 +36,8 @@ const _blendFactor = [ 'one-minus-src-alpha', // BLENDMODE_ONE_MINUS_SRC_ALPHA 'dst-alpha', // BLENDMODE_DST_ALPHA 'one-minus-dst-alpha', // BLENDMODE_ONE_MINUS_DST_ALPHA - 'constant', // BLENDMODE_CONSTANT_COLOR - 'one-minus-constant', // BLENDMODE_ONE_MINUS_CONSTANT_COLOR - undefined, // BLENDMODE_CONSTANT_ALPHA - undefined // BLENDMODE_ONE_MINUS_CONSTANT_ALPHA + 'constant', // BLENDMODE_CONSTANT + 'one-minus-constant' // BLENDMODE_ONE_MINUS_CONSTANT ]; // temp array to avoid allocation @@ -68,10 +66,10 @@ class WebgpuRenderPipeline { this.cache = new Map(); } - get(primitive, vertexFormat0, vertexFormat1, shader, renderTarget, bindGroupFormats, renderState) { + get(primitive, vertexFormat0, vertexFormat1, shader, renderTarget, bindGroupFormats, blendState) { // render pipeline unique key - const key = this.getKey(primitive, vertexFormat0, vertexFormat1, shader, renderTarget, bindGroupFormats, renderState); + const key = this.getKey(primitive, vertexFormat0, vertexFormat1, shader, renderTarget, bindGroupFormats, blendState); // cached pipeline let pipeline = this.cache.get(key); @@ -87,7 +85,7 @@ class WebgpuRenderPipeline { const vertexBufferLayout = this.vertexBufferLayout.get(vertexFormat0, vertexFormat1); // pipeline - pipeline = this.create(primitiveTopology, shader.impl, renderTarget, pipelineLayout, renderState, vertexBufferLayout); + pipeline = this.create(primitiveTopology, shader.impl, renderTarget, pipelineLayout, blendState, vertexBufferLayout); this.cache.set(key, pipeline); } @@ -98,7 +96,7 @@ class WebgpuRenderPipeline { * Generate a unique key for the render pipeline. Keep this function as lean as possible, * as it executes for each draw call. */ - getKey(primitive, vertexFormat0, vertexFormat1, shader, renderTarget, bindGroupFormats, renderState) { + getKey(primitive, vertexFormat0, vertexFormat1, shader, renderTarget, bindGroupFormats, blendState) { let bindGroupKey = ''; for (let i = 0; i < bindGroupFormats.length; i++) { @@ -107,10 +105,9 @@ class WebgpuRenderPipeline { const vertexBufferLayoutKey = this.vertexBufferLayout.getKey(vertexFormat0, vertexFormat1); const renderTargetKey = renderTarget.impl.key; - const renderStateKey = renderState.blendKey; return vertexBufferLayoutKey + shader.impl.vertexCode + shader.impl.fragmentCode + - renderTargetKey + renderStateKey + primitive.type + bindGroupKey; + renderTargetKey + primitive.type + bindGroupKey + blendState.key; } // TODO: this could be cached using bindGroupKey @@ -143,24 +140,24 @@ class WebgpuRenderPipeline { return pipelineLayout; } - getBlend(renderState) { + getBlend(blendState) { // blend needs to be undefined when blending is disabled let blend; - if (renderState.blending) { + if (blendState.blend) { /** @type {GPUBlendState} */ blend = { color: { - operation: _blendOperation[renderState.blendEquationColor], - srcFactor: _blendFactor[renderState.blendSrcColor], - dstFactor: _blendFactor[renderState.blendDstColor] + operation: _blendOperation[blendState.colorOp], + srcFactor: _blendFactor[blendState.colorSrcFactor], + dstFactor: _blendFactor[blendState.colorDstFactor] }, alpha: { - operation: _blendOperation[renderState.blendEquationAlpha], - srcFactor: _blendFactor[renderState.blendSrcAlpha], - dstFactor: _blendFactor[renderState.blendDstAlpha] + operation: _blendOperation[blendState.alphaOp], + srcFactor: _blendFactor[blendState.alphaSrcFactor], + dstFactor: _blendFactor[blendState.alphaDstFactor] } }; @@ -174,7 +171,7 @@ class WebgpuRenderPipeline { return blend; } - create(primitiveTopology, webgpuShader, renderTarget, pipelineLayout, renderState, vertexBufferLayout) { + create(primitiveTopology, webgpuShader, renderTarget, pipelineLayout, blendState, vertexBufferLayout) { const wgpu = this.device.wgpu; @@ -222,14 +219,20 @@ class WebgpuRenderPipeline { }); DebugHelper.setLabel(fragmentModule, `Fragment ${webgpuShader.shader.label}`); + let writeMask = 0; + if (blendState.redWrite) writeMask |= GPUColorWrite.RED; + if (blendState.greenWrite) writeMask |= GPUColorWrite.GREEN; + if (blendState.blueWrite) writeMask |= GPUColorWrite.BLUE; + if (blendState.alphaWrite) writeMask |= GPUColorWrite.ALPHA; + /** @type {GPUFragmentState} */ descr.fragment = { module: fragmentModule, entryPoint: 'main', targets: [{ format: renderTarget.impl.colorFormat, - writeMask: GPUColorWrite.ALL, - blend: this.getBlend(renderState) + writeMask: writeMask, + blend: this.getBlend(blendState) }] }; } diff --git a/src/platform/graphics/webgpu/webgpu-render-state.js b/src/platform/graphics/webgpu/webgpu-render-state.js deleted file mode 100644 index 3d77e176012..00000000000 --- a/src/platform/graphics/webgpu/webgpu-render-state.js +++ /dev/null @@ -1,76 +0,0 @@ -import { - BLENDEQUATION_ADD, - BLENDMODE_ZERO, BLENDMODE_ONE -} from '../constants.js'; - -class WebgpuRenderState { - constructor() { - this.reset(); - } - - // TODO: When setDepthTest is implemented, a webgpu only workaround should be removed in drawQuadWithShader function. - - reset() { - - // blend state - this.blendStateDirty = true; - this.blendStateKey = ''; - this.blending = false; - this.blendSrcColor = BLENDMODE_ONE; - this.blendDstColor = BLENDMODE_ZERO; - this.blendEquationColor = BLENDEQUATION_ADD; - this.blendEquationAlpha = BLENDEQUATION_ADD; - this.blendSrcAlpha = BLENDMODE_ONE; - this.blendDstAlpha = BLENDMODE_ZERO; -// this.blendColor = new Color(0, 0, 0, 0); - this.writeRed = true; - this.writeGreen = true; - this.writeBlue = true; - this.writeAlpha = true; - } - - get blendKey() { - - if (this.blendStateDirty) { - this.blendStateDirty = false; - - this.blendStateKey = `${this.blending}_${this.blendSrcColor}_${this.blendSrcAlpha}`; - this.blendStateKey += `_${this.blendDstColor}_${this.blendDstAlpha}`; - this.blendStateKey += `_${this.blendEquationColor}_${this.blendEquationAlpha}`; - } - return this.blendStateKey; - - } - - setBlending(blending) { - if (this.blending !== blending) { - this.blending = blending; - - this.blendStateDirty = true; - } - } - - setBlendFunction(blendSrc, blendDst) { - if (this.blendSrcColor !== blendSrc || this.blendDstColor !== blendDst || - this.blendSrcAlpha !== blendSrc || this.blendDstAlpha !== blendDst) { - - this.blendSrcColor = blendSrc; - this.blendSrcAlpha = blendSrc; - this.blendDstColor = blendDst; - this.blendDstAlpha = blendDst; - - this.blendStateDirty = true; - } - } - - setBlendEquation(blendEquation) { - if (this.blendEquationColor !== blendEquation || this.blendEquationAlpha !== blendEquation) { - this.blendEquationColor = blendEquation; - this.blendEquationAlpha = blendEquation; - - this.blendStateDirty = true; - } - } -} - -export { WebgpuRenderState }; diff --git a/src/scene/graphics/post-effect.js b/src/scene/graphics/post-effect.js index e5fb25184e1..1cd666b2e8f 100644 --- a/src/scene/graphics/post-effect.js +++ b/src/scene/graphics/post-effect.js @@ -1,4 +1,5 @@ import { Vec4 } from '../../core/math/vec4.js'; +import { BlendState } from '../../platform/graphics/blend-state.js'; import { drawQuadWithShader } from './quad-render-utils.js'; const _viewport = new Vec4(); @@ -80,6 +81,7 @@ class PostEffect { viewport = _viewport.set(rect.x * w, rect.y * h, rect.z * w, rect.w * h); } + this.device.setBlendState(BlendState.DEFAULT); drawQuadWithShader(this.device, target, shader, viewport); } } diff --git a/src/scene/graphics/prefilter-cubemap.js b/src/scene/graphics/prefilter-cubemap.js index abd4134cd45..78312b7f208 100644 --- a/src/scene/graphics/prefilter-cubemap.js +++ b/src/scene/graphics/prefilter-cubemap.js @@ -9,6 +9,7 @@ import { drawQuadWithShader } from './quad-render-utils.js'; import { shaderChunks } from '../shader-lib/chunks/chunks.js'; import { RenderTarget } from '../../platform/graphics/render-target.js'; import { Texture } from '../../platform/graphics/texture.js'; +import { BlendState } from '../../platform/graphics/blend-state.js'; // https://seblagarde.wordpress.com/2012/06/10/amd-cubemapgen-for-physically-based-rendering/ function areaElement(x, y) { @@ -95,6 +96,7 @@ function shFromCubemap(device, source, dontFlipX) { depth: false }); constantTexSource.setValue(tex); + device.setBlendState(BlendState.DEFAULT); drawQuadWithShader(device, targ, shader); const gl = device.gl; diff --git a/src/scene/graphics/quad-render-utils.js b/src/scene/graphics/quad-render-utils.js index 11c56ae30c2..11ae7d83792 100644 --- a/src/scene/graphics/quad-render-utils.js +++ b/src/scene/graphics/quad-render-utils.js @@ -21,28 +21,28 @@ const _tempRect = new Vec4(); * pixels. Defaults to fullscreen (`0, 0, target.width, target.height`). * @param {import('../../core/math/vec4.js').Vec4} [scissorRect] - The scissor rectangle of the * quad, in pixels. Defaults to fullscreen (`0, 0, target.width, target.height`). - * @param {boolean} [useBlend] - True to enable blending. Defaults to false, disabling blending. */ -function drawQuadWithShader(device, target, shader, rect, scissorRect, useBlend = false) { +function drawQuadWithShader(device, target, shader, rect, scissorRect) { // a valid target or a null target (framebuffer) are supported Debug.assert(target !== undefined); + const useBlend = arguments[5]; + Debug.call(() => { + if (useBlend !== undefined) { + Debug.warnOnce('pc.drawQuadWithShader no longer accepts useBlend parameter, and blending state needs to be set up using GraphicsDevice.setBlendState.'); + } + }); + DebugGraphics.pushGpuMarker(device, "drawQuadWithShader"); const oldDepthTest = device.getDepthTest(); const oldDepthWrite = device.getDepthWrite(); const oldCullMode = device.getCullMode(); - const oldWR = device.writeRed; - const oldWG = device.writeGreen; - const oldWB = device.writeBlue; - const oldWA = device.writeAlpha; device.setDepthTest(false); device.setDepthWrite(false); device.setCullMode(CULLFACE_NONE); - device.setColorWrite(true, true, true, true); - if (!useBlend) device.setBlending(false); // prepare the quad for rendering with the shader const quad = new QuadRender(shader); @@ -76,7 +76,6 @@ function drawQuadWithShader(device, target, shader, rect, scissorRect, useBlend device.setDepthTest(oldDepthTest); device.setDepthWrite(oldDepthWrite); device.setCullMode(oldCullMode); - device.setColorWrite(oldWR, oldWG, oldWB, oldWA); DebugGraphics.popGpuMarker(device); } @@ -96,13 +95,20 @@ function drawQuadWithShader(device, target, shader, rect, scissorRect, useBlend * texture, in pixels. Defaults to fullscreen (`0, 0, target.width, target.height`). * @param {import('../../core/math/vec4.js').Vec4} [scissorRect] - The scissor rectangle to use for * the texture, in pixels. Defaults to fullscreen (`0, 0, target.width, target.height`). - * @param {boolean} [useBlend] - True to enable blending. Defaults to false, disabling blending. */ -function drawTexture(device, texture, target, shader, rect, scissorRect, useBlend = false) { +function drawTexture(device, texture, target, shader, rect, scissorRect) { Debug.assert(!device.isWebGPU, 'pc.drawTexture is not currently supported on WebGPU platform.'); + + const useBlend = arguments[6]; + Debug.call(() => { + if (useBlend !== undefined) { + Debug.warnOnce('pc.drawTexture no longer accepts useBlend parameter, and blending state needs to be set up using GraphicsDevice.setBlendState.'); + } + }); + shader = shader || device.getCopyShader(); device.constantTexSource.setValue(texture); - drawQuadWithShader(device, target, shader, rect, scissorRect, useBlend); + drawQuadWithShader(device, target, shader, rect, scissorRect); } export { drawQuadWithShader, drawTexture }; diff --git a/src/scene/graphics/reproject-texture.js b/src/scene/graphics/reproject-texture.js index 6f8743df18a..78c1f302896 100644 --- a/src/scene/graphics/reproject-texture.js +++ b/src/scene/graphics/reproject-texture.js @@ -17,6 +17,7 @@ import { ChunkUtils } from '../shader-lib/chunk-utils.js'; import { shaderChunks } from '../shader-lib/chunks/chunks.js'; import { getProgramLibrary } from '../shader-lib/get-program-library.js'; import { createShaderFromCode } from '../shader-lib/utils.js'; +import { BlendState } from '../../platform/graphics/blend-state.js'; const getProjectionName = (projection) => { switch (projection) { @@ -462,6 +463,10 @@ function reprojectTexture(source, target, options = {}) { DebugGraphics.pushGpuMarker(device, "ReprojectTexture"); + // render state + // TODO: set up other render state here to expected state + device.setBlendState(BlendState.DEFAULT); + const constantSource = device.scope.resolve(source.cubemap ? "sourceCube" : "sourceTex"); Debug.assert(constantSource); constantSource.setValue(source); diff --git a/src/scene/graphics/scene-grab.js b/src/scene/graphics/scene-grab.js index 3091089bf8f..94c98c2f0c7 100644 --- a/src/scene/graphics/scene-grab.js +++ b/src/scene/graphics/scene-grab.js @@ -8,6 +8,7 @@ import { import { RenderTarget } from '../../platform/graphics/render-target.js'; import { Texture } from '../../platform/graphics/texture.js'; +import { BlendState } from '../../platform/graphics/blend-state.js'; import { DebugGraphics } from '../../platform/graphics/debug-graphics.js'; import { @@ -386,7 +387,8 @@ class SceneGrab { }, onDrawCall: function () { - device.setColorWrite(true, true, true, true); + // writing depth to color render target, force no blending and writing to all channels + device.setBlendState(BlendState.DEFAULT); }, onPostRenderOpaque: function (cameraPass) { diff --git a/src/scene/materials/material.js b/src/scene/materials/material.js index e74e2510cdb..d727be106ad 100644 --- a/src/scene/materials/material.js +++ b/src/scene/materials/material.js @@ -1,10 +1,11 @@ import { Debug } from '../../core/debug.js'; +import { BlendState } from '../../platform/graphics/blend-state.js'; import { BLENDMODE_ZERO, BLENDMODE_ONE, BLENDMODE_SRC_COLOR, BLENDMODE_DST_COLOR, BLENDMODE_ONE_MINUS_DST_COLOR, BLENDMODE_SRC_ALPHA, BLENDMODE_ONE_MINUS_SRC_ALPHA, - BLENDEQUATION_ADD, + BLENDEQUATION_ADD, BLENDEQUATION_REVERSE_SUBTRACT, BLENDEQUATION_MIN, BLENDEQUATION_MAX, CULLFACE_BACK, FUNC_LESSEQUAL @@ -14,11 +15,25 @@ import { ShaderProcessorOptions } from '../../platform/graphics/shader-processor import { BLEND_ADDITIVE, BLEND_NORMAL, BLEND_NONE, BLEND_PREMULTIPLIED, BLEND_MULTIPLICATIVE, BLEND_ADDITIVEALPHA, BLEND_MULTIPLICATIVE2X, BLEND_SCREEN, - BLEND_MIN, BLEND_MAX + BLEND_MIN, BLEND_MAX, BLEND_SUBTRACTIVE } from '../constants.js'; import { processShader } from '../shader-lib/utils.js'; import { getDefaultMaterial } from './default-material.js'; +// blend mode mapping to op, srcBlend and dstBlend +const blendModes = []; +blendModes[BLEND_SUBTRACTIVE] = { src: BLENDMODE_ONE, dst: BLENDMODE_ONE, op: BLENDEQUATION_REVERSE_SUBTRACT }; +blendModes[BLEND_NONE] = { src: BLENDMODE_ONE, dst: BLENDMODE_ZERO, op: BLENDEQUATION_ADD }; +blendModes[BLEND_NORMAL] = { src: BLENDMODE_SRC_ALPHA, dst: BLENDMODE_ONE_MINUS_SRC_ALPHA, op: BLENDEQUATION_ADD }; +blendModes[BLEND_PREMULTIPLIED] = { src: BLENDMODE_ONE, dst: BLENDMODE_ONE_MINUS_SRC_ALPHA, op: BLENDEQUATION_ADD }; +blendModes[BLEND_ADDITIVE] = { src: BLENDMODE_ONE, dst: BLENDMODE_ONE, op: BLENDEQUATION_ADD }; +blendModes[BLEND_ADDITIVEALPHA] = { src: BLENDMODE_SRC_ALPHA, dst: BLENDMODE_ONE, op: BLENDEQUATION_ADD }; +blendModes[BLEND_MULTIPLICATIVE2X] = { src: BLENDMODE_DST_COLOR, dst: BLENDMODE_SRC_COLOR, op: BLENDEQUATION_ADD }; +blendModes[BLEND_SCREEN] = { src: BLENDMODE_ONE_MINUS_DST_COLOR, dst: BLENDMODE_ONE, op: BLENDEQUATION_ADD }; +blendModes[BLEND_MULTIPLICATIVE] = { src: BLENDMODE_DST_COLOR, dst: BLENDMODE_ZERO, op: BLENDEQUATION_ADD }; +blendModes[BLEND_MIN] = { src: BLENDMODE_ONE, dst: BLENDMODE_ONE, op: BLENDEQUATION_MIN }; +blendModes[BLEND_MAX] = { src: BLENDMODE_ONE, dst: BLENDMODE_ONE, op: BLENDEQUATION_MAX }; + let id = 0; /** @@ -79,21 +94,8 @@ class Material { */ alphaToCoverage = false; - blend = false; - - blendSrc = BLENDMODE_ONE; - - blendDst = BLENDMODE_ZERO; - - blendEquation = BLENDEQUATION_ADD; - - separateAlphaBlend = false; - - blendSrcAlpha = BLENDMODE_ONE; - - blendDstAlpha = BLENDMODE_ZERO; - - blendAlphaEquation = BLENDEQUATION_ADD; + /** @ignore */ + _blendState = new BlendState(); /** * Controls how triangles are culled based on their face direction with respect to the @@ -113,6 +115,8 @@ class Material { */ cull = CULLFACE_BACK; + // TODO: When setDepthTest is implemented, a webgpu only workaround should be removed in drawQuadWithShader function. + /** * If true, fragments generated by the shader of this material are only written to the current * render target if they pass the depth test. If false, fragments generated by the shader of @@ -180,6 +184,14 @@ class Material { */ slopeDepthBias = 0; + _shaderVersion = 0; + + _scene = null; + + _dirtyBlend = false; + + dirty = true; + /** * If true, the red component of fragments generated by the shader of this material is written * to the color buffer of the currently active render target. If false, the red component will @@ -187,7 +199,13 @@ class Material { * * @type {boolean} */ - redWrite = true; + set redWrite(value) { + this._blendState.redWrite = value; + } + + get redWrite() { + return this._blendState.redWrite; + } /** * If true, the green component of fragments generated by the shader of this material is @@ -196,7 +214,13 @@ class Material { * * @type {boolean} */ - greenWrite = true; + set greenWrite(value) { + this._blendState.greenWrite = value; + } + + get greenWrite() { + return this._blendState.greenWrite; + } /** * If true, the blue component of fragments generated by the shader of this material is @@ -205,7 +229,13 @@ class Material { * * @type {boolean} */ - blueWrite = true; + set blueWrite(value) { + this._blendState.blueWrite = value; + } + + get blueWrite() { + return this._blendState.blueWrite; + } /** * If true, the alpha component of fragments generated by the shader of this material is @@ -214,15 +244,13 @@ class Material { * * @type {boolean} */ - alphaWrite = true; - - _shaderVersion = 0; - - _scene = null; - - _dirtyBlend = false; + set alphaWrite(value) { + this._blendState.alphaWrite = value; + } - dirty = true; + get alphaWrite() { + return this._blendState.alphaWrite; + } /** * The shader used by this material to render mesh instances (default is null). @@ -239,12 +267,41 @@ class Material { // returns boolean depending on material being transparent get transparent() { - return this.blend; + return this._blendState.blend; } + // called when material changes transparency, for layer composition to add it to appropriate + // queue (opaque or transparent) + _markBlendDirty() { + if (this._scene) { + this._scene.layers._dirtyBlend = true; + } else { + this._dirtyBlend = true; + } + } + + /** + * Controls how fragment shader outputs are blended when being written to the currently active + * render target. This overwrites blending type set using {@link pc.Material#blendType}, and + * offers more control over blending. + * + * @type { BlendState } + */ + set blendState(value) { + if (this._blendState.blend !== value.blend) { + this._markBlendDirty(); + } + this._blendState.copy(value); + } + + get blendState() { + return this._blendState; + } + + /** - * Controls how primitives are blended when being written to the currently active render - * target. Can be: + * Controls how fragment shader outputs are blended when being written to the currently active + * render target. Can be: * * - {@link BLEND_SUBTRACTIVE}: Subtract the color of the source fragment from the destination * fragment and write the result to the frame buffer. @@ -270,119 +327,33 @@ class Material { * @type {number} */ set blendType(type) { - let blend = true; - switch (type) { - case BLEND_NONE: - blend = false; - this.blendSrc = BLENDMODE_ONE; - this.blendDst = BLENDMODE_ZERO; - this.blendEquation = BLENDEQUATION_ADD; - break; - case BLEND_NORMAL: - this.blendSrc = BLENDMODE_SRC_ALPHA; - this.blendDst = BLENDMODE_ONE_MINUS_SRC_ALPHA; - this.blendEquation = BLENDEQUATION_ADD; - break; - case BLEND_PREMULTIPLIED: - this.blendSrc = BLENDMODE_ONE; - this.blendDst = BLENDMODE_ONE_MINUS_SRC_ALPHA; - this.blendEquation = BLENDEQUATION_ADD; - break; - case BLEND_ADDITIVE: - this.blendSrc = BLENDMODE_ONE; - this.blendDst = BLENDMODE_ONE; - this.blendEquation = BLENDEQUATION_ADD; - break; - case BLEND_ADDITIVEALPHA: - this.blendSrc = BLENDMODE_SRC_ALPHA; - this.blendDst = BLENDMODE_ONE; - this.blendEquation = BLENDEQUATION_ADD; - break; - case BLEND_MULTIPLICATIVE2X: - this.blendSrc = BLENDMODE_DST_COLOR; - this.blendDst = BLENDMODE_SRC_COLOR; - this.blendEquation = BLENDEQUATION_ADD; - break; - case BLEND_SCREEN: - this.blendSrc = BLENDMODE_ONE_MINUS_DST_COLOR; - this.blendDst = BLENDMODE_ONE; - this.blendEquation = BLENDEQUATION_ADD; - break; - case BLEND_MULTIPLICATIVE: - this.blendSrc = BLENDMODE_DST_COLOR; - this.blendDst = BLENDMODE_ZERO; - this.blendEquation = BLENDEQUATION_ADD; - break; - case BLEND_MIN: - this.blendSrc = BLENDMODE_ONE; - this.blendDst = BLENDMODE_ONE; - this.blendEquation = BLENDEQUATION_MIN; - break; - case BLEND_MAX: - this.blendSrc = BLENDMODE_ONE; - this.blendDst = BLENDMODE_ONE; - this.blendEquation = BLENDEQUATION_MAX; - break; - } - if (this.blend !== blend) { - this.blend = blend; - if (this._scene) { - this._scene.layers._dirtyBlend = true; - } else { - this._dirtyBlend = true; - } + + const blendMode = blendModes[type]; + Debug.assert(blendMode, `Unknown blend mode ${type}`); + this._blendState.setColorBlend(blendMode.op, blendMode.src, blendMode.dst); + this._blendState.setAlphaBlend(blendMode.op, blendMode.src, blendMode.dst); + + const blend = type !== BLEND_NONE; + if (this._blendState.blend !== blend) { + this._blendState.blend = blend; + this._markBlendDirty(); } this._updateMeshInstanceKeys(); } get blendType() { - if (!this.blend) { + if (!this.transparent) { return BLEND_NONE; } - if ((this.blendSrc === BLENDMODE_SRC_ALPHA) && (this.blendDst === BLENDMODE_ONE_MINUS_SRC_ALPHA) && - (this.blendEquation === BLENDEQUATION_ADD)) { - return BLEND_NORMAL; - } + const { colorOp, colorSrcFactor, colorDstFactor, alphaOp, alphaSrcFactor, alphaDstFactor } = this._blendState; - if ((this.blendSrc === BLENDMODE_ONE) && (this.blendDst === BLENDMODE_ONE) && - (this.blendEquation === BLENDEQUATION_ADD)) { - return BLEND_ADDITIVE; - } - - if ((this.blendSrc === BLENDMODE_SRC_ALPHA) && (this.blendDst === BLENDMODE_ONE) && - (this.blendEquation === BLENDEQUATION_ADD)) { - return BLEND_ADDITIVEALPHA; - } - - if ((this.blendSrc === BLENDMODE_DST_COLOR) && (this.blendDst === BLENDMODE_SRC_COLOR) && - (this.blendEquation === BLENDEQUATION_ADD)) { - return BLEND_MULTIPLICATIVE2X; - } - - if ((this.blendSrc === BLENDMODE_ONE_MINUS_DST_COLOR) && (this.blendDst === BLENDMODE_ONE) && - (this.blendEquation === BLENDEQUATION_ADD)) { - return BLEND_SCREEN; - } - - if ((this.blendSrc === BLENDMODE_ONE) && (this.blendDst === BLENDMODE_ONE) && - (this.blendEquation === BLENDEQUATION_MIN)) { - return BLEND_MIN; - } - - if ((this.blendSrc === BLENDMODE_ONE) && (this.blendDst === BLENDMODE_ONE) && - (this.blendEquation === BLENDEQUATION_MAX)) { - return BLEND_MAX; - } - - if ((this.blendSrc === BLENDMODE_DST_COLOR) && (this.blendDst === BLENDMODE_ZERO) && - (this.blendEquation === BLENDEQUATION_ADD)) { - return BLEND_MULTIPLICATIVE; - } - - if ((this.blendSrc === BLENDMODE_ONE) && (this.blendDst === BLENDMODE_ONE_MINUS_SRC_ALPHA) && - (this.blendEquation === BLENDEQUATION_ADD)) { - return BLEND_PREMULTIPLIED; + for (let i = 0; i < blendModes.length; i++) { + const blendMode = blendModes[i]; + if (blendMode.src === colorSrcFactor && blendMode.dst === colorDstFactor && blendMode.op === colorOp && + blendMode.src === alphaSrcFactor && blendMode.dst === alphaDstFactor && blendMode.op === alphaOp) { + return i; + } } return BLEND_NORMAL; @@ -402,15 +373,7 @@ class Material { this.alphaTest = source.alphaTest; this.alphaToCoverage = source.alphaToCoverage; - this.blend = source.blend; - this.blendSrc = source.blendSrc; - this.blendDst = source.blendDst; - this.blendEquation = source.blendEquation; - - this.separateAlphaBlend = source.separateAlphaBlend; - this.blendSrcAlpha = source.blendSrcAlpha; - this.blendDstAlpha = source.blendDstAlpha; - this.blendAlphaEquation = source.blendAlphaEquation; + this._blendState.copy(source._blendState); this.cull = source.cull; @@ -428,11 +391,6 @@ class Material { } } - this.redWrite = source.redWrite; - this.greenWrite = source.greenWrite; - this.blueWrite = source.blueWrite; - this.alphaWrite = source.alphaWrite; - return this; } diff --git a/src/scene/morph-instance.js b/src/scene/morph-instance.js index d9a3a5da252..03e33346aad 100644 --- a/src/scene/morph-instance.js +++ b/src/scene/morph-instance.js @@ -6,6 +6,7 @@ import { RenderTarget } from '../platform/graphics/render-target.js'; import { DebugGraphics } from '../platform/graphics/debug-graphics.js'; import { createShaderFromCode } from './shader-lib/utils.js'; +import { BlendState } from '../platform/graphics/blend-state.js'; // vertex shader used to add morph targets from textures into render target const textureMorphVertexShader = ` @@ -17,6 +18,8 @@ const textureMorphVertexShader = ` } `; +const blendStateAdditive = new BlendState(true, BLENDEQUATION_ADD, BLENDMODE_ONE, BLENDMODE_ONE); + /** * An instance of {@link Morph}. Contains weights to assign to every {@link MorphTarget}, manages * selection of active morph targets. @@ -264,15 +267,11 @@ class MorphInstance { this.morphFactor.setValue(this._shaderMorphWeights); // alpha blending - first pass gets none, following passes are additive - device.setBlending(blending); - if (blending) { - device.setBlendFunction(BLENDMODE_ONE, BLENDMODE_ONE); - device.setBlendEquation(BLENDEQUATION_ADD); - } + device.setBlendState(blending ? blendStateAdditive : BlendState.DEFAULT); // render quad with shader for required number of textures const shader = this._getShader(usedCount); - drawQuadWithShader(device, renderTarget, shader, undefined, undefined, blending); + drawQuadWithShader(device, renderTarget, shader); }; // set up parameters for active blend targets diff --git a/src/scene/particle-system/gpu-updater.js b/src/scene/particle-system/gpu-updater.js index d2e406e88c0..f7ddfc7ef5a 100644 --- a/src/scene/particle-system/gpu-updater.js +++ b/src/scene/particle-system/gpu-updater.js @@ -5,6 +5,8 @@ import { Vec3 } from '../../core/math/vec3.js'; import { CULLFACE_NONE } from '../../platform/graphics/constants.js'; import { DebugGraphics } from '../../platform/graphics/debug-graphics.js'; +import { BlendState } from '../../platform/graphics/blend-state.js'; + import { drawQuadWithShader } from '../graphics/quad-render-utils.js'; import { EMITTERSHAPE_BOX } from '../constants.js'; @@ -89,8 +91,7 @@ class ParticleGPUUpdater { const emitter = this._emitter; - device.setBlending(false); - device.setColorWrite(true, true, true, true); + device.setBlendState(BlendState.DEFAULT); device.setCullMode(CULLFACE_NONE); device.setDepthTest(false); device.setDepthWrite(false); diff --git a/src/scene/renderer/cookie-renderer.js b/src/scene/renderer/cookie-renderer.js index 53af410c2bd..0f46bbf0497 100644 --- a/src/scene/renderer/cookie-renderer.js +++ b/src/scene/renderer/cookie-renderer.js @@ -9,6 +9,7 @@ import { Texture } from '../../platform/graphics/texture.js'; import { LIGHTTYPE_OMNI } from '../constants.js'; import { createShaderFromCode } from '../shader-lib/utils.js'; import { LightCamera } from './light-camera.js'; +import { BlendState } from '../../platform/graphics/blend-state.js'; const textureBlitVertexShader = ` attribute vec2 vertex_position; @@ -126,6 +127,9 @@ class CookieRenderer { // source texture this.blitTextureId.setValue(light.cookie); + // render state + device.setBlendState(BlendState.DEFAULT); + // render it to a viewport of the target for (let face = 0; face < faceCount; face++) { diff --git a/src/scene/renderer/forward-renderer.js b/src/scene/renderer/forward-renderer.js index 0f8c46a90ab..677cf86cc01 100644 --- a/src/scene/renderer/forward-renderer.js +++ b/src/scene/renderer/forward-renderer.js @@ -24,6 +24,7 @@ import { Renderer } from './renderer.js'; import { LightCamera } from './light-camera.js'; import { WorldClustersDebug } from '../lighting/world-clusters-debug.js'; import { SceneGrab } from '../graphics/scene-grab.js'; +import { BlendState } from '../../platform/graphics/blend-state.js'; const webgl1DepthClearColor = new Color(254.0 / 255, 254.0 / 255, 254.0 / 255, 254.0 / 255); @@ -601,17 +602,7 @@ class ForwardRenderer extends Renderer { this.alphaTestId.setValue(material.alphaTest); - device.setBlending(material.blend); - if (material.blend) { - if (material.separateAlphaBlend) { - device.setBlendFunctionSeparate(material.blendSrc, material.blendDst, material.blendSrcAlpha, material.blendDstAlpha); - device.setBlendEquationSeparate(material.blendEquation, material.blendAlphaEquation); - } else { - device.setBlendFunction(material.blendSrc, material.blendDst); - device.setBlendEquation(material.blendEquation); - } - } - device.setColorWrite(material.redWrite, material.greenWrite, material.blueWrite, material.alphaWrite); + device.setBlendState(material.blendState); device.setDepthWrite(material.depthWrite); // this fixes the case where the user wishes to turn off depth testing but wants to write depth @@ -1162,7 +1153,7 @@ class ForwardRenderer extends Renderer { // Revert temp frame stuff // TODO: this should not be here, as each rendering / clearing should explicitly set up what // it requires (the properties are part of render pipeline on WebGPU anyways) - device.setColorWrite(true, true, true, true); + device.setBlendState(BlendState.DEFAULT); device.setStencilTest(false); // don't leak stencil state device.setAlphaToCoverage(false); // don't leak a2c state device.setDepthBias(false); diff --git a/src/scene/renderer/shadow-renderer.js b/src/scene/renderer/shadow-renderer.js index 5f100995367..a20e48dfa42 100644 --- a/src/scene/renderer/shadow-renderer.js +++ b/src/scene/renderer/shadow-renderer.js @@ -23,6 +23,7 @@ import { createShaderFromCode } from '../shader-lib/utils.js'; import { LightCamera } from './light-camera.js'; import { UniformBufferFormat, UniformFormat } from '../../platform/graphics/uniform-buffer-format.js'; import { BindBufferFormat, BindGroupFormat } from '../../platform/graphics/bind-group-format.js'; +import { BlendState } from '../../platform/graphics/blend-state.js'; function gauss(x, sigma) { return Math.exp(-(x * x) / (2.0 * sigma * sigma)); @@ -112,6 +113,11 @@ class ShadowRenderer { // view bind group format with its uniform buffer format this.viewUniformFormat = null; this.viewBindGroupFormat = null; + + // blend states + this.blendStateWrite = new BlendState(); + this.blendStateNoWrite = new BlendState(); + this.blendStateNoWrite.setColorWrite(false, false, false, false); } // creates shadow camera for a light and sets up its constant properties @@ -190,7 +196,6 @@ class ShadowRenderer { } // Set standard shadowmap states - device.setBlending(false); device.setDepthWrite(true); device.setDepthTest(true); device.setDepthFunc(FUNC_LESSEQUAL); @@ -198,11 +203,8 @@ class ShadowRenderer { const useShadowSampler = isClustered ? light._isPcf && device.webgl2 : // both spot and omni light are using shadow sampler on webgl2 when clustered light._isPcf && device.webgl2 && light._type !== LIGHTTYPE_OMNI; // for non-clustered, point light is using depth encoded in color buffer (should change to shadow sampler) - if (useShadowSampler) { - device.setColorWrite(false, false, false, false); - } else { - device.setColorWrite(true, true, true, true); - } + + device.setBlendState(useShadowSampler ? this.blendStateNoWrite : this.blendStateWrite); } restoreRenderState(device) { @@ -483,6 +485,9 @@ class ShadowRenderer { DebugGraphics.pushGpuMarker(device, `VSM ${light._node.name}`); + // render state + device.setBlendState(BlendState.DEFAULT); + const lightRenderData = light.getRenderData(light._type === LIGHTTYPE_DIRECTIONAL ? camera : null, 0); const shadowCam = lightRenderData.shadowCamera; const origShadowMap = shadowCam.renderTarget; diff --git a/test/platform/graphics/blend-state.test.mjs b/test/platform/graphics/blend-state.test.mjs new file mode 100644 index 00000000000..f036d122539 --- /dev/null +++ b/test/platform/graphics/blend-state.test.mjs @@ -0,0 +1,62 @@ +import { BlendState } from '../../../src/platform/graphics/blend-state.js'; +import { + BLENDEQUATION_ADD, BLENDEQUATION_MAX, BLENDEQUATION_MIN, BLENDMODE_ONE, BLENDMODE_ZERO, + BLENDMODE_ONE_MINUS_DST_COLOR, BLENDMODE_SRC_ALPHA_SATURATE +} from '../../../src/platform/graphics/constants.js'; + +import { expect } from 'chai'; + +describe('BlendState', function () { + + describe('#constructor', function () { + + it('empty', function () { + const bs = new BlendState(); + expect(bs.blend).to.equal(false); + expect(bs.colorOp).to.equal(BLENDEQUATION_ADD); + expect(bs.colorSrcFactor).to.equal(BLENDMODE_ONE); + expect(bs.colorDstFactor).to.equal(BLENDMODE_ZERO); + expect(bs.alphaOp).to.equal(BLENDEQUATION_ADD); + expect(bs.alphaSrcFactor).to.equal(BLENDMODE_ONE); + expect(bs.alphaDstFactor).to.equal(BLENDMODE_ZERO); + expect(bs.redWrite).to.equal(true); + expect(bs.greenWrite).to.equal(true); + expect(bs.blueWrite).to.equal(true); + expect(bs.alphaWrite).to.equal(true); + }); + + it('minimal parameters', function () { + const bs = new BlendState(true, BLENDEQUATION_ADD, BLENDMODE_ONE, BLENDMODE_ZERO); + expect(bs.blend).to.equal(true); + expect(bs.colorOp).to.equal(BLENDEQUATION_ADD); + expect(bs.colorSrcFactor).to.equal(BLENDMODE_ONE); + expect(bs.colorDstFactor).to.equal(BLENDMODE_ZERO); + expect(bs.alphaOp).to.equal(BLENDEQUATION_ADD); + expect(bs.alphaSrcFactor).to.equal(BLENDMODE_ONE); + expect(bs.alphaDstFactor).to.equal(BLENDMODE_ZERO); + expect(bs.redWrite).to.equal(true); + expect(bs.greenWrite).to.equal(true); + expect(bs.blueWrite).to.equal(true); + expect(bs.alphaWrite).to.equal(true); + }); + + it('full parameters', function () { + const bs = new BlendState(true, BLENDEQUATION_MIN, BLENDMODE_ONE, BLENDMODE_ZERO, + BLENDEQUATION_MAX, BLENDMODE_ONE_MINUS_DST_COLOR, BLENDMODE_SRC_ALPHA_SATURATE, + false, false, false, false); + expect(bs.blend).to.equal(true); + expect(bs.colorOp).to.equal(BLENDEQUATION_MIN); + expect(bs.colorSrcFactor).to.equal(BLENDMODE_ONE); + expect(bs.colorDstFactor).to.equal(BLENDMODE_ZERO); + expect(bs.alphaOp).to.equal(BLENDEQUATION_MAX); + expect(bs.alphaSrcFactor).to.equal(BLENDMODE_ONE_MINUS_DST_COLOR); + expect(bs.alphaDstFactor).to.equal(BLENDMODE_SRC_ALPHA_SATURATE); + expect(bs.redWrite).to.equal(false); + expect(bs.greenWrite).to.equal(false); + expect(bs.blueWrite).to.equal(false); + expect(bs.alphaWrite).to.equal(false); + }); + + }); + +});