diff --git a/examples/src/examples/graphics/dithered-transparency.mjs b/examples/src/examples/graphics/dithered-transparency.mjs index c954d252e84..b73527c0be0 100644 --- a/examples/src/examples/graphics/dithered-transparency.mjs +++ b/examples/src/examples/graphics/dithered-transparency.mjs @@ -5,7 +5,7 @@ import * as pc from 'playcanvas'; * @returns {JSX.Element} The returned JSX Element. */ function controls({ observer, ReactPCUI, React, jsx, fragment }) { - const { BindingTwoWay, LabelGroup, Panel, SliderInput, BooleanInput } = ReactPCUI; + const { BindingTwoWay, LabelGroup, Panel, SliderInput, BooleanInput, SelectInput } = ReactPCUI; return fragment( jsx(Panel, { headerText: 'Settings' }, jsx(LabelGroup, { text: 'Opacity' }, @@ -18,19 +18,27 @@ function controls({ observer, ReactPCUI, React, jsx, fragment }) { }) ), jsx(LabelGroup, { text: 'Dither Color' }, - jsx(BooleanInput, { - type: 'toggle', + jsx(SelectInput, { binding: new BindingTwoWay(), link: { observer, path: 'data.opacityDither' }, - value: true + type: "string", + options: [ + { v: pc.DITHER_NONE, t: 'None' }, + { v: pc.DITHER_BAYER8, t: 'Bayer8' }, + { v: pc.DITHER_BLUENOISE, t: 'BlueNoise' } + ] }) ), jsx(LabelGroup, { text: 'Dither Shadow' }, - jsx(BooleanInput, { - type: 'toggle', + jsx(SelectInput, { binding: new BindingTwoWay(), link: { observer, path: 'data.opacityShadowDither' }, - value: true + type: "string", + options: [ + { v: pc.DITHER_NONE, t: 'None' }, + { v: pc.DITHER_BAYER8, t: 'Bayer8' }, + { v: pc.DITHER_BLUENOISE, t: 'BlueNoise' } + ] }) ) ) @@ -62,6 +70,9 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath, createOptions.touch = new pc.TouchDevice(document.body); createOptions.keyboard = new pc.Keyboard(document.body); + // render at full native resolution + device.maxPixelRatio = window.devicePixelRatio; + createOptions.componentSystems = [ pc.RenderComponentSystem, pc.CameraComponentSystem, @@ -197,7 +208,7 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath, // turn on / off blending depending on the dithering of the color if (propertyName === 'opacityDither') { - material.blendType = value ? pc.BLEND_NONE : pc.BLEND_NORMAL; + material.blendType = value === pc.DITHER_NONE ? pc.BLEND_NORMAL : pc.BLEND_NONE; } material.update(); }); @@ -206,8 +217,8 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath, // initial values data.set('data', { opacity: 0.5, - opacityDither: true, - opacityShadowDither: true + opacityDither: pc.DITHER_BAYER8, + opacityShadowDither: pc.DITHER_BAYER8 }); }); return app; diff --git a/extras/splat/shader-generator-splat.js b/extras/splat/shader-generator-splat.js index 5d45b5e30df..0c18a696848 100644 --- a/extras/splat/shader-generator-splat.js +++ b/extras/splat/shader-generator-splat.js @@ -1,5 +1,6 @@ import { ShaderUtils, + DITHER_NONE, SEMANTIC_ATTR13, SEMANTIC_POSITION, ShaderGenerator, @@ -28,6 +29,7 @@ const splatCoreVS = ` varying vec2 texCoord; varying vec4 color; + varying float id; mat3 quatToMat3(vec3 R) { @@ -233,12 +235,15 @@ const splatCoreVS = ` vec4((vertex_position.x * v1 + vertex_position.y * v2) / viewport * 2.0, 0.0, 0.0) * splat_proj.w; #endif + + id = float(vertex_id); } `; const splatCoreFS = /* glsl_ */ ` varying vec2 texCoord; varying vec4 color; + varying float id; vec4 evalSplat() { @@ -253,6 +258,10 @@ const splatCoreFS = /* glsl_ */ ` if (A < -4.0) discard; float B = exp(A) * color.a; + #ifndef DITHER_NONE + opacityDither(B, id * 0.013); + #endif + // the color here is in gamma space, so bring it to linear vec3 diffuse = decodeGamma(color.rgb); @@ -260,10 +269,6 @@ const splatCoreFS = /* glsl_ */ ` diffuse = toneMap(diffuse); diffuse = gammaCorrectOutput(diffuse); - #ifdef DITHER - opacityDither(B); - #endif - return vec4(diffuse, B); #endif @@ -282,11 +287,11 @@ class ShaderGeneratorSplat { const defines = (options.debugRender ? '#define DEBUG_RENDER\n' : '') + (device.isWebGL1 ? '' : '#define INT_INDICES\n') + - (options.dither ? '#define DITHER\n' : ''); + `#define DITHER_${options.dither.toUpperCase()}\n`; const vs = defines + splatCoreVS + options.vertex; const fs = defines + shaderChunks.decodePS + - (options.dither ? shaderChunks.bayerPS + shaderChunks.opacityDitherPS : '') + + (options.dither === DITHER_NONE ? '' : shaderChunks.bayerPS + shaderChunks.opacityDitherPS) + ShaderGenerator.tonemapCode(options.toneMapping) + ShaderGenerator.gammaCode(options.gamma) + splatCoreFS + options.fragment; diff --git a/extras/splat/splat-instance.js b/extras/splat/splat-instance.js index 0991bd42a90..316fa563b70 100644 --- a/extras/splat/splat-instance.js +++ b/extras/splat/splat-instance.js @@ -5,6 +5,7 @@ import { Mat4, createBox, BUFFER_DYNAMIC, + DITHER_NONE, VertexBuffer } from "playcanvas"; @@ -96,7 +97,7 @@ class SplatInstance { // clone centers to allow multiple instancing of sorter this.centers = new Float32Array(splat.centers); - if (!options.dither) { + if (options.dither !== DITHER_NONE) { this.sorter = new SplatSorter(); this.sorter.init(this.vb, this.centers, !this.splat.device.isWebGL1); diff --git a/extras/splat/splat-material.js b/extras/splat/splat-material.js index db5d9cd891e..891289c89c3 100644 --- a/extras/splat/splat-material.js +++ b/extras/splat/splat-material.js @@ -5,6 +5,7 @@ import { CULLFACE_BACK, CULLFACE_NONE, Material, + DITHER_NONE, SHADER_FORWARDHDR, TONEMAP_LINEAR, GAMMA_SRGBHDR, @@ -45,12 +46,13 @@ const splatMainFS = ` */ const createSplatMaterial = (options = {}) => { - const { debugRender, dither } = options; + const { debugRender } = options; + const dither = options.dither ?? DITHER_NONE; const material = new Material(); material.name = 'splatMaterial'; material.cull = debugRender ? CULLFACE_BACK : CULLFACE_NONE; - material.blendType = dither ? BLEND_NONE : BLEND_NORMAL; + material.blendType = dither === DITHER_NONE ? BLEND_NORMAL : BLEND_NONE; material.depthWrite = dither; material.getShaderVariant = function (device, scene, defs, unused, pass, sortedLights, viewUniformFormat, viewBindGroupFormat) { @@ -62,7 +64,7 @@ const createSplatMaterial = (options = {}) => { vertex: options.vertex ?? splatMainVS, fragment: options.fragment ?? splatMainFS, debugRender: debugRender, - dither: !!dither + dither: dither }; const processingOptions = new ShaderProcessorOptions(viewUniformFormat, viewBindGroupFormat); diff --git a/src/core/math/blue-noise.js b/src/core/math/blue-noise.js new file mode 100644 index 00000000000..8b0ed6adb87 --- /dev/null +++ b/src/core/math/blue-noise.js @@ -0,0 +1,50 @@ +import { Vec4 } from "./vec4.js"; + +// this is a 32x32 blue noise texture LDR_RGBA_53.png from the database mentioned here: https://momentsingraphics.de/BlueNoise.html +const base64String = "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"; + +let data = null; +const initData = () => { + if (!data) { + const binaryString = atob(base64String); + + // the data stores an array of RGBA 8-bit values, each of the color channels represents + // a separate blue noise stream + data = Uint8Array.from(binaryString, char => char.charCodeAt(0)); + } +}; + +const blueNoiseData = () => { + initData(); + return data; +}; + +/** + * Blue noise based random numbers API. + * + * @ignore + */ +class BlueNoise { + seed = 0; + + constructor(seed = 0) { + this.seed = seed * 4; + initData(); + } + + _next() { + this.seed = (this.seed + 4) % data.length; + } + + value() { + this._next(); + return data[this.seed] / 255; + } + + vec4(dest = new Vec4()) { + this._next(); + return dest.set(data[this.seed], data[this.seed + 1], data[this.seed + 2], data[this.seed + 3]).mulScalar(1 / 255); + } +} + +export { BlueNoise, blueNoiseData }; diff --git a/src/scene/constants.js b/src/scene/constants.js index 656efeb3986..6d2ac1cc6fd 100644 --- a/src/scene/constants.js +++ b/src/scene/constants.js @@ -887,3 +887,24 @@ export const SKYTYPE_BOX = 'box'; * @type {string} */ export const SKYTYPE_DOME = 'dome'; + +/** + * Opacity dithering is disabled. + * + * @type {string} + */ +export const DITHER_NONE = 'none'; + +/** + * Opacity is dithered using a Bayer 8 matrix. + * + * @type {string} + */ +export const DITHER_BAYER8 = 'bayer8'; + +/** + * Opacity is dithered using a blue noise texture. + * + * @type {string} + */ +export const DITHER_BLUENOISE = 'bluenoise'; diff --git a/src/scene/graphics/blue-noise-texture.js b/src/scene/graphics/blue-noise-texture.js new file mode 100644 index 00000000000..dda32a6c670 --- /dev/null +++ b/src/scene/graphics/blue-noise-texture.js @@ -0,0 +1,37 @@ +import { blueNoiseData } from "../../core/math/blue-noise.js"; +import { ADDRESS_REPEAT, FILTER_NEAREST, PIXELFORMAT_RGBA8, TEXTURETYPE_DEFAULT } from "../../platform/graphics/constants.js"; +import { DeviceCache } from "../../platform/graphics/device-cache.js"; +import { Texture } from "../../platform/graphics/texture.js"; + +// device cache storing the blue noise texture for the device +const deviceCache = new DeviceCache(); + +function getBlueNoiseTexture(device) { + + return deviceCache.get(device, () => { + + const data = blueNoiseData(); + const size = Math.sqrt(data.length / 4); + + const texture = new Texture(device, { + name: `BlueNoise${size}`, + width: size, + height: size, + format: PIXELFORMAT_RGBA8, + addressU: ADDRESS_REPEAT, + addressV: ADDRESS_REPEAT, + type: TEXTURETYPE_DEFAULT, + magFilter: FILTER_NEAREST, + minFilter: FILTER_NEAREST, + anisotropy: 1, + mipmaps: false + }); + + texture.lock().set(data); + texture.unlock(); + + return texture; + }); +} + +export { getBlueNoiseTexture }; diff --git a/src/scene/materials/lit-material.js b/src/scene/materials/lit-material.js index a18e0f4087e..d926550f216 100644 --- a/src/scene/materials/lit-material.js +++ b/src/scene/materials/lit-material.js @@ -1,5 +1,5 @@ import { ShaderProcessorOptions } from '../../platform/graphics/shader-processor-options.js'; -import { FRESNEL_SCHLICK, SPECOCC_AO, SPECULAR_BLINN } from "../constants.js"; +import { DITHER_NONE, FRESNEL_SCHLICK, SPECOCC_AO, SPECULAR_BLINN } from "../constants.js"; import { Material } from './material.js'; import { LitMaterialOptions } from './lit-material-options.js'; import { LitMaterialOptionsBuilder } from './lit-material-options-builder.js'; @@ -53,9 +53,9 @@ class LitMaterial extends Material { opacityFadesSpecular = true; - opacityDither = false; + opacityDither = DITHER_NONE; - opacityShadowDither = false; + opacityShadowDither = DITHER_NONE; conserveEnergy = true; diff --git a/src/scene/materials/standard-material-options-builder.js b/src/scene/materials/standard-material-options-builder.js index a395bb64846..ee14c3c9aa6 100644 --- a/src/scene/materials/standard-material-options-builder.js +++ b/src/scene/materials/standard-material-options-builder.js @@ -11,7 +11,8 @@ import { SHADERDEF_DIRLM, SHADERDEF_INSTANCING, SHADERDEF_LM, SHADERDEF_MORPH_POSITION, SHADERDEF_MORPH_NORMAL, SHADERDEF_NOSHADOW, SHADERDEF_MORPH_TEXTURE_BASED, SHADERDEF_SCREENSPACE, SHADERDEF_SKIN, SHADERDEF_TANGENTS, SHADERDEF_UV0, SHADERDEF_UV1, SHADERDEF_VCOLOR, SHADERDEF_LMAMBIENT, TONEMAP_LINEAR, - SPECULAR_PHONG + SPECULAR_PHONG, + DITHER_NONE } from '../constants.js'; import { _matTex2D } from '../shader-lib/programs/standard.js'; import { LitMaterialOptionsBuilder } from './lit-material-options-builder.js'; @@ -150,7 +151,7 @@ class StandardMaterialOptionsBuilder { options[vname] = false; options[vcname] = ''; - if (isOpacity && stdMat.blendType === BLEND_NONE && stdMat.alphaTest === 0.0 && !stdMat.alphaToCoverage && !stdMat.opacityDither) { + if (isOpacity && stdMat.blendType === BLEND_NONE && stdMat.alphaTest === 0.0 && !stdMat.alphaToCoverage && stdMat.opacityDither === DITHER_NONE) { return; } @@ -188,7 +189,7 @@ class StandardMaterialOptionsBuilder { } _updateMinOptions(options, stdMat) { - options.opacityTint = stdMat.opacity !== 1 && (stdMat.blendType !== BLEND_NONE || stdMat.opacityShadowDither); + options.opacityTint = stdMat.opacity !== 1 && (stdMat.blendType !== BLEND_NONE || stdMat.opacityShadowDither !== DITHER_NONE); options.litOptions.opacityShadowDither = stdMat.opacityShadowDither; options.litOptions.lights = []; } @@ -215,7 +216,7 @@ class StandardMaterialOptionsBuilder { const isPackedNormalMap = stdMat.normalMap ? (stdMat.normalMap.format === PIXELFORMAT_DXT5 || stdMat.normalMap.type === TEXTURETYPE_SWIZZLEGGGR) : false; - options.opacityTint = (stdMat.opacity !== 1 && (stdMat.blendType !== BLEND_NONE || stdMat.alphaTest > 0 || stdMat.opacityDither)) ? 1 : 0; + options.opacityTint = (stdMat.opacity !== 1 && (stdMat.blendType !== BLEND_NONE || stdMat.alphaTest > 0 || stdMat.opacityDither !== DITHER_NONE)) ? 1 : 0; options.ambientTint = stdMat.ambientTint; options.diffuseTint = diffuseTint ? 2 : 0; options.specularTint = specularTint ? 2 : 0; diff --git a/src/scene/materials/standard-material-parameters.js b/src/scene/materials/standard-material-parameters.js index 604474a1296..c805d949c9f 100644 --- a/src/scene/materials/standard-material-parameters.js +++ b/src/scene/materials/standard-material-parameters.js @@ -95,8 +95,8 @@ const standardMaterialParameterTypes = { opacity: 'number', ..._textureParameter('opacity'), opacityFadesSpecular: 'boolean', - opacityDither: 'boolean', - opacityShadowDither: 'boolean', + opacityDither: 'string', + opacityShadowDither: 'string', reflectivity: 'number', refraction: 'number', diff --git a/src/scene/materials/standard-material.js b/src/scene/materials/standard-material.js index f7cf5caab6b..e456ac81f58 100644 --- a/src/scene/materials/standard-material.js +++ b/src/scene/materials/standard-material.js @@ -8,6 +8,7 @@ import { ShaderProcessorOptions } from '../../platform/graphics/shader-processor import { CUBEPROJ_BOX, CUBEPROJ_NONE, DETAILMODE_MUL, + DITHER_NONE, FRESNEL_SCHLICK, SHADER_DEPTH, SHADER_PICK, SPECOCC_AO, @@ -375,10 +376,22 @@ let _params = new Set(); * @property {boolean} opacityFadesSpecular Used to specify whether specular and reflections are * faded out using {@link StandardMaterial#opacity}. Default is true. When set to false use * {@link Material#alphaFade} to fade out materials. - * @property {boolean} opacityDither Used to specify whether opacity is dithered, which allows - * transparency without alpha blending. Defaults to false. + * @property {string} opacityDither Used to specify whether opacity is dithered, which allows + * transparency without alpha blending. Can be: + * + * - {@link DITHER_NONE}: Opacity dithering is disabled. + * - {@link DITHER_BAYER8}: Opacity is dithered using a Bayer 8 matrix. + * - {@link DITHER_BLUENOISE}: Opacity is dithered using a blue noise texture. + * + * Defaults to {@link DITHER_NONE}. * @property {boolean} opacityShadowDither Used to specify whether shadow opacity is dithered, which - * allows shadow transparency without alpha blending. Defaults to false. + * allows shadow transparency without alpha blending. Can be: + * + * - {@link DITHER_NONE}: Opacity dithering is disabled. + * - {@link DITHER_BAYER8}: Opacity is dithered using a Bayer 8 matrix. + * - {@link DITHER_BLUENOISE}: Opacity is dithered using a blue noise texture. + * + * Defaults to {@link DITHER_NONE}. * @property {number} alphaFade Used to fade out materials when * {@link StandardMaterial#opacityFadesSpecular} is set to false. * @property {import('../../platform/graphics/texture.js').Texture|null} normalMap The main @@ -1232,8 +1245,8 @@ function _defineMaterialProps() { _defineFlag('glossInvert', false); _defineFlag('sheenGlossInvert', false); _defineFlag('clearCoatGlossInvert', false); - _defineFlag('opacityDither', false); - _defineFlag('opacityShadowDither', false); + _defineFlag('opacityDither', DITHER_NONE); + _defineFlag('opacityShadowDither', DITHER_NONE); _defineTex2D('diffuse'); _defineTex2D('specular'); diff --git a/src/scene/renderer/renderer.js b/src/scene/renderer/renderer.js index 1911d7f7edb..1c1165f990e 100644 --- a/src/scene/renderer/renderer.js +++ b/src/scene/renderer/renderer.js @@ -2,6 +2,7 @@ import { Debug, DebugHelper } from '../../core/debug.js'; import { now } from '../../core/time.js'; import { Vec2 } from '../../core/math/vec2.js'; import { Vec3 } from '../../core/math/vec3.js'; +import { Vec4 } from '../../core/math/vec4.js'; import { Mat3 } from '../../core/math/mat3.js'; import { Mat4 } from '../../core/math/mat4.js'; import { BoundingSphere } from '../../core/shape/bounding-sphere.js'; @@ -37,6 +38,8 @@ import { ShadowRendererDirectional } from './shadow-renderer-directional.js'; import { ShadowRenderer } from './shadow-renderer.js'; import { WorldClustersAllocator } from './world-clusters-allocator.js'; import { RenderPassUpdateClustered } from './render-pass-update-clustered.js'; +import { getBlueNoiseTexture } from '../graphics/blue-noise-texture.js'; +import { BlueNoise } from '../../core/math/blue-noise.js'; let _skinUpdateIndex = 0; const boneTextureSize = [0, 0, 0, 0]; @@ -48,6 +51,7 @@ const tempSphere = new BoundingSphere(); const _flipYMat = new Mat4().setScale(1, -1, 1); const _tempLightSet = new Set(); const _tempLayerSet = new Set(); +const _tempVec4 = new Vec4(); // Converts a projection matrix in OpenGL style (depth range of -1..1) to a DirectX style (depth range of 0..1). const _fixProjRangeMat = new Mat4().set([ @@ -143,6 +147,8 @@ class Renderer { */ dirLightShadows = new Map(); + blueNoise = new BlueNoise(123); + /** * Create a new instance. * @@ -215,6 +221,9 @@ class Renderer { this.cameraParams = new Float32Array(4); this.cameraParamsId = scope.resolve('camera_params'); + this.blueNoiseJitterId = scope.resolve('blueNoiseJitter'); + this.blueNoiseTextureId = scope.resolve('blueNoiseTex32'); + this.alphaTestId = scope.resolve('alpha_ref'); this.opacityMapId = scope.resolve('texture_opacityMap'); @@ -366,6 +375,7 @@ class Renderer { // camera jitter const { jitter } = camera; + let noise = Vec4.ZERO; if (jitter > 0) { // render target size @@ -386,8 +396,13 @@ class Renderer { projMatSkybox = _tempProjMat5.copy(projMatSkybox); projMatSkybox.data[8] = offsetX; projMatSkybox.data[9] = offsetY; + + // blue noise vec4 - only set when jitter is enabled + noise = this.blueNoise.vec4(_tempVec4); } + this.blueNoiseJitterId.setValue([noise.x, noise.y, noise.z, noise.w]); + this.projId.setValue(projMat.data); this.projSkyboxId.setValue(projMatSkybox.data); @@ -1207,6 +1222,11 @@ class Renderer { _tempSet.clear(); } + updateFrameUniforms() { + // blue noise texture + this.blueNoiseTextureId.setValue(getBlueNoiseTexture(this.device)); + } + /** * @param {import('../composition/layer-composition.js').LayerComposition} comp - The layer * composition to update. @@ -1257,6 +1277,8 @@ class Renderer { scene._shaderVersion++; } + this.updateFrameUniforms(); + // Update all skin matrices to properly cull skinned objects (but don't update rendering data yet) this.updateCpuSkinMatrices(_tempMeshInstancesSkinned); diff --git a/src/scene/shader-lib/chunks/standard/frag/opacity-dither.js b/src/scene/shader-lib/chunks/standard/frag/opacity-dither.js index 4bf70daeee0..0ef93d0dc80 100644 --- a/src/scene/shader-lib/chunks/standard/frag/opacity-dither.js +++ b/src/scene/shader-lib/chunks/standard/frag/opacity-dither.js @@ -1,6 +1,24 @@ export default /* glsl */` -void opacityDither(float alpha) { - if (alpha <= bayer8(floor(mod(gl_FragCoord.xy, 8.0))) / 64.0) + +uniform vec4 blueNoiseJitter; + +#ifndef DITHER_BAYER8 + uniform sampler2D blueNoiseTex32; +#endif + +void opacityDither(float alpha, float id) { + #ifdef DITHER_BAYER8 + + float noise = bayer8(floor(mod(gl_FragCoord.xy + blueNoiseJitter.xy + id, 8.0))) / 64.0; + + #else // blue noise + + vec2 uv = fract(gl_FragCoord.xy / 32.0 + blueNoiseJitter.xy + id); + float noise = texture2DLodEXT(blueNoiseTex32, uv, 0.0).y; + + #endif + + if (alpha < noise) discard; } `; diff --git a/src/scene/shader-lib/programs/lit-shader-options.js b/src/scene/shader-lib/programs/lit-shader-options.js index 7fcc6d9c72b..e8aa8ac40f3 100644 --- a/src/scene/shader-lib/programs/lit-shader-options.js +++ b/src/scene/shader-lib/programs/lit-shader-options.js @@ -1,4 +1,4 @@ -import { BLEND_NONE, FOG_NONE, GAMMA_NONE } from '../../constants.js'; +import { BLEND_NONE, DITHER_NONE, FOG_NONE, GAMMA_NONE } from '../../constants.js'; /** * The lit shader options determines how the lit-shader gets generated. It specifies a set of @@ -175,16 +175,16 @@ class LitShaderOptions { /** * Enable opacity dithering. See {@link StandardMaterial#opacityDither}. * - * @type {boolean} + * @type {string} */ - opacityDither = false; + opacityDither = DITHER_NONE; /** * Enable opacity shadow dithering. See {@link StandardMaterial#opacityShadowDither}. * - * @type {boolean} + * @type {string} */ - opacityShadowDither = false; + opacityShadowDither = DITHER_NONE; /** * The value of {@link StandardMaterial#cubeMapProjection}. diff --git a/src/scene/shader-lib/programs/standard.js b/src/scene/shader-lib/programs/standard.js index d32f85ba8a8..90a0951ad5d 100644 --- a/src/scene/shader-lib/programs/standard.js +++ b/src/scene/shader-lib/programs/standard.js @@ -1,7 +1,7 @@ import { Debug } from '../../../core/debug.js'; import { - BLEND_NONE, FRESNEL_SCHLICK, + BLEND_NONE, DITHER_BAYER8, DITHER_NONE, FRESNEL_SCHLICK, SHADER_FORWARD, SHADER_FORWARDHDR, SPECULAR_PHONG, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED @@ -292,7 +292,7 @@ class ShaderGeneratorStandard extends ShaderGenerator { } // opacity - if (options.litOptions.blendType !== BLEND_NONE || options.litOptions.alphaTest || options.litOptions.alphaToCoverage || options.litOptions.opacityDither) { + if (options.litOptions.blendType !== BLEND_NONE || options.litOptions.alphaTest || options.litOptions.alphaToCoverage || options.litOptions.opacityDither !== DITHER_NONE) { decl.append("float dAlpha;"); code.append(this._addMap("opacity", "opacityPS", options, litShader.chunks, textureMapping)); func.append("getOpacity();"); @@ -303,10 +303,13 @@ class ShaderGeneratorStandard extends ShaderGenerator { func.append("alphaTest(dAlpha);"); } - if (options.litOptions.opacityDither) { - decl.append(litShader.chunks.bayerPS); + const opacityDither = options.litOptions.opacityDither; + if (opacityDither !== DITHER_NONE) { + if (opacityDither === DITHER_BAYER8) + decl.append(litShader.chunks.bayerPS); + decl.append(`#define DITHER_${opacityDither.toUpperCase()}\n`); decl.append(litShader.chunks.opacityDitherPS); - func.append("opacityDither(dAlpha);"); + func.append("opacityDither(dAlpha, 0.0);"); } } else { decl.append("float dAlpha = 1.0;"); @@ -489,7 +492,8 @@ class ShaderGeneratorStandard extends ShaderGenerator { } } else { // all other passes require only opacity - if (options.litOptions.alphaTest || options.litOptions.opacityShadowDither) { + const opacityShadowDither = options.litOptions.opacityShadowDither; + if (options.litOptions.alphaTest || opacityShadowDither) { decl.append("float dAlpha;"); code.append(this._addMap("opacity", "opacityPS", options, litShader.chunks, textureMapping)); func.append("getOpacity();"); @@ -498,10 +502,12 @@ class ShaderGeneratorStandard extends ShaderGenerator { code.append(litShader.chunks.alphaTestPS); func.append("alphaTest(dAlpha);"); } - if (options.litOptions.opacityShadowDither) { - decl.append(litShader.chunks.bayerPS); + if (opacityShadowDither) { + if (opacityShadowDither === DITHER_BAYER8) + decl.append(litShader.chunks.bayerPS); + decl.append(`#define DITHER_${opacityShadowDither.toUpperCase()}\n`); decl.append(litShader.chunks.opacityDitherPS); - func.append("opacityDither(dAlpha);"); + func.append("opacityDither(dAlpha, 0.0);"); } } } diff --git a/test/scene/materials/standard-material.test.mjs b/test/scene/materials/standard-material.test.mjs index 3971eeec7b1..f7bb14259f9 100644 --- a/test/scene/materials/standard-material.test.mjs +++ b/test/scene/materials/standard-material.test.mjs @@ -1,4 +1,4 @@ -import { CUBEPROJ_NONE, DETAILMODE_MUL, FRESNEL_SCHLICK, SPECOCC_AO, SPECULAR_BLINN } from '../../../src/scene/constants.js'; +import { CUBEPROJ_NONE, DETAILMODE_MUL, DITHER_NONE, FRESNEL_SCHLICK, SPECOCC_AO, SPECULAR_BLINN } from '../../../src/scene/constants.js'; import { Color } from '../../../src/core/math/color.js'; import { Material } from '../../../src/scene/materials/material.js'; import { StandardMaterial } from '../../../src/scene/materials/standard-material.js'; @@ -221,8 +221,8 @@ describe('StandardMaterial', function () { expect(material.opacity).to.equal(1); expect(material.opacityFadesSpecular).to.equal(true); - expect(material.opacityDither).to.equal(false); - expect(material.opacityShadowDither).to.equal(false); + expect(material.opacityDither).to.equal(DITHER_NONE); + expect(material.opacityShadowDither).to.equal(DITHER_NONE); expect(material.opacityMap).to.be.null; expect(material.opacityMapChannel).to.equal('a'); expect(material.opacityMapOffset).to.be.an.instanceof(Vec2); diff --git a/utils/types-fixup.mjs b/utils/types-fixup.mjs index 5a876b57286..68654b0116f 100644 --- a/utils/types-fixup.mjs +++ b/utils/types-fixup.mjs @@ -460,8 +460,8 @@ const standardMaterialProps = [ ['occludeSpecularIntensity', 'number'], ['onUpdateShader', 'UpdateShaderCallback'], ['opacity', 'number'], - ['opacityDither', 'boolean'], - ['opacityShadowDither', 'boolean'], + ['opacityDither', 'string'], + ['opacityShadowDither', 'string'], ['opacityFadesSpecular', 'boolean'], ['opacityMap', 'Texture|null'], ['opacityMapChannel', 'string'],