Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Opacity dithering support #5903

Merged
merged 5 commits into from Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file added examples/assets/models/glass-table.glb
Binary file not shown.
5 changes: 5 additions & 0 deletions examples/assets/models/glass-table.txt
@@ -0,0 +1,5 @@
The glass-table model has been obtained from this address:
https://sketchfab.com/3d-models/low-poly-glass-table-6acac6d9201e448b92dff859b6f63aad#download

It's distributed under CC license:
https://creativecommons.org/licenses/by/4.0/
Binary file modified examples/assets/textures/pc-gray.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
221 changes: 221 additions & 0 deletions examples/src/examples/graphics/dithered-transparency.mjs
@@ -0,0 +1,221 @@
import * as pc from 'playcanvas';

/**
* @param {import('../../app/example.mjs').ControlOptions} options - The options.
* @returns {JSX.Element} The returned JSX Element.
*/
function controls({ observer, ReactPCUI, React, jsx, fragment }) {
const { BindingTwoWay, LabelGroup, Panel, SliderInput, BooleanInput } = ReactPCUI;
return fragment(
jsx(Panel, { headerText: 'Settings' },
jsx(LabelGroup, { text: 'Opacity' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'data.opacity' },
min: 0.0,
max: 1,
precision: 2
})
),
jsx(LabelGroup, { text: 'Dither Color' },
jsx(BooleanInput, {
type: 'toggle',
binding: new BindingTwoWay(),
link: { observer, path: 'data.opacityDither' },
value: true
})
),
jsx(LabelGroup, { text: 'Dither Shadow' },
jsx(BooleanInput, {
type: 'toggle',
binding: new BindingTwoWay(),
link: { observer, path: 'data.opacityShadowDither' },
value: true
})
)
)
);
}

/**
* @param {import('../../options.mjs').ExampleOptions} options - The example options.
* @returns {Promise<pc.AppBase>} The example application.
*/
async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath, scriptsPath, data }) {

const assets = {
envAtlas: new pc.Asset('env-atlas', 'texture', { url: assetPath + 'cubemaps/table-mountain-env-atlas.png' }, { type: pc.TEXTURETYPE_RGBP, mipmaps: false }),
table: new pc.Asset('table', 'container', { url: assetPath + 'models/glass-table.glb' }),
'script': new pc.Asset('script', 'script', { url: scriptsPath + 'camera/orbit-camera.js' })
};

const gfxOptions = {
deviceTypes: [deviceType],
glslangUrl: glslangPath + 'glslang.js',
twgslUrl: twgslPath + 'twgsl.js'
};

const device = await pc.createGraphicsDevice(canvas, gfxOptions);
const createOptions = new pc.AppOptions();
createOptions.graphicsDevice = device;
createOptions.mouse = new pc.Mouse(document.body);
createOptions.touch = new pc.TouchDevice(document.body);
createOptions.keyboard = new pc.Keyboard(document.body);

createOptions.componentSystems = [
pc.RenderComponentSystem,
pc.CameraComponentSystem,
pc.LightComponentSystem,
pc.ScriptComponentSystem
];
createOptions.resourceHandlers = [
pc.TextureHandler,
pc.ScriptHandler,
pc.ContainerHandler
];

const app = new pc.AppBase(canvas);
app.init(createOptions);

// Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);

// Ensure canvas is resized when window changes size
const resize = () => app.resizeCanvas();
window.addEventListener('resize', resize);
app.on('destroy', () => {
window.removeEventListener('resize', resize);
});

const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
assetListLoader.load(() => {

app.start();

// setup skydome
app.scene.envAtlas = assets.envAtlas.resource;
app.scene.skyboxMip = 2;
app.scene.exposure = 1;
app.scene.toneMapping = pc.TONEMAP_ACES;

/**
* helper function to create a primitive with shape type, position, scale, color and layer
* @param {string} primitiveType - The primitive type.
* @param {number | pc.Vec3} position - The position.
* @param {number | pc.Vec3} scale - The scale.
* @param {pc.Color} color - The color.
* @returns {pc.Material} The returned entity.
*/
function createPrimitive(primitiveType, position, scale, color) {
// create material of specified color
const material = new pc.StandardMaterial();
material.diffuse = color;
material.update();

// create primitive
const primitive = new pc.Entity(primitiveType);
primitive.addComponent('render', {
type: primitiveType,
material: material
});

// set position and scale and add it to scene
primitive.setLocalPosition(position);
primitive.setLocalScale(scale);
app.root.addChild(primitive);

return material;
}

// create a ground plane
createPrimitive("plane", new pc.Vec3(0, 0, 0), new pc.Vec3(30, 1, 30), new pc.Color(0.5, 0.5, 0.5));

// create an instance of the table
const tableEntity = assets.table.resource.instantiateRenderEntity();
tableEntity.setLocalScale(3, 3, 3);
app.root.addChild(tableEntity);

// get all materials that have blending enabled
const materials = [];
tableEntity.findComponents("render").forEach((render) => {
render.meshInstances.forEach((meshInstance) => {
if (meshInstance.material.blendType !== pc.BLEND_NONE) {
materials.push(meshInstance.material);
}
});
});

// Create the camera
const camera = new pc.Entity("Camera");
camera.addComponent("camera", {
fov: 70
});
camera.translate(-14, 12, 12);
camera.lookAt(1, 4, 0);
app.root.addChild(camera);

// enable the camera to render the scene's color map, as the table material needs it
camera.camera.requestSceneColorMap(true);

// add orbit camera script with a mouse and a touch support
camera.addComponent("script");
camera.script.create("orbitCamera", {
attributes: {
inertiaFactor: 0.2,
focusEntity: tableEntity,
distanceMax: 30,
frameOnStart: false
}
});
camera.script.create("orbitCameraInputMouse");
camera.script.create("orbitCameraInputTouch");

// Create an Entity with a directional light, casting soft VSM shadow
const light = new pc.Entity();
light.addComponent("light", {
type: "directional",
color: pc.Color.WHITE,
range: 200,
castShadows: true,
shadowResolution: 2048,
shadowType: pc.SHADOW_VSM16,
vsmBlurSize: 20,
shadowBias: 0.1,
normalOffsetBias: 0.1
});
light.setLocalEulerAngles(75, 120, 20);
app.root.addChild(light);

// handle UI changes
data.on('*:set', (/** @type {string} */ path, value) => {
const propertyName = path.split('.')[1];
materials.forEach((material) => {

// apply the value to the material
material[propertyName] = value;

// turn on / off blending depending on the dithering of the color
if (propertyName === 'opacityDither') {
material.blendType = value ? pc.BLEND_NONE : pc.BLEND_NORMAL;
}
material.update();
});
});

// initial values
data.set('data', {
opacity: 0.5,
opacityDither: true,
opacityShadowDither: true
});
});
return app;
}

export class DitheredTransparencyExample {
static CATEGORY = 'Graphics';
static WEBGPU_ENABLED = true;
static controls = controls;
static example = example;
}
1 change: 1 addition & 0 deletions examples/src/examples/graphics/index.mjs
Expand Up @@ -8,6 +8,7 @@ export * from "./clustered-lighting.mjs";
export * from "./clustered-omni-shadows.mjs";
export * from "./clustered-spot-shadows.mjs";
export * from "./contact-hardening-shadows.mjs";
export * from "./dithered-transparency.mjs";
export * from "./lit-material.mjs";
export * from "./grab-pass.mjs";
export * from "./ground-fog.mjs";
Expand Down
2 changes: 2 additions & 0 deletions src/scene/materials/lit-material-options-builder.js
Expand Up @@ -63,6 +63,8 @@ class LitMaterialOptionsBuilder {

litOptions.alphaToCoverage = material.alphaToCoverage;
litOptions.opacityFadesSpecular = material.opacityFadesSpecular;
litOptions.opacityDither = material.opacityDither;
litOptions.opacityShadowDither = material.opacityShadowDither;

litOptions.cubeMapProjection = CUBEPROJ_NONE;

Expand Down
4 changes: 4 additions & 0 deletions src/scene/materials/lit-material.js
Expand Up @@ -53,6 +53,10 @@ class LitMaterial extends Material {

opacityFadesSpecular = true;

opacityDither = false;

opacityShadowDither = false;

conserveEnergy = true;

ggxSpecular = false;
Expand Down
8 changes: 5 additions & 3 deletions src/scene/materials/standard-material-options-builder.js
Expand Up @@ -150,7 +150,7 @@ class StandardMaterialOptionsBuilder {
options[vname] = false;
options[vcname] = '';

if (isOpacity && stdMat.blendType === BLEND_NONE && stdMat.alphaTest === 0.0 && !stdMat.alphaToCoverage) {
if (isOpacity && stdMat.blendType === BLEND_NONE && stdMat.alphaTest === 0.0 && !stdMat.alphaToCoverage && !stdMat.opacityDither) {
return;
}

Expand Down Expand Up @@ -188,7 +188,8 @@ class StandardMaterialOptionsBuilder {
}

_updateMinOptions(options, stdMat) {
options.opacityTint = stdMat.opacity !== 1 && stdMat.blendType !== BLEND_NONE;
options.opacityTint = stdMat.opacity !== 1 && (stdMat.blendType !== BLEND_NONE || stdMat.opacityShadowDither);
options.litOptions.opacityShadowDither = stdMat.opacityShadowDither;
options.litOptions.lights = [];
}

Expand All @@ -214,7 +215,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) ? 1 : 0;
options.opacityTint = (stdMat.opacity !== 1 && (stdMat.blendType !== BLEND_NONE || stdMat.alphaTest > 0 || stdMat.opacityDither)) ? 1 : 0;
options.ambientTint = stdMat.ambientTint;
options.diffuseTint = diffuseTint ? 2 : 0;
options.specularTint = specularTint ? 2 : 0;
Expand Down Expand Up @@ -272,6 +273,7 @@ class StandardMaterialOptionsBuilder {

options.litOptions.alphaToCoverage = stdMat.alphaToCoverage;
options.litOptions.opacityFadesSpecular = stdMat.opacityFadesSpecular;
options.litOptions.opacityDither = stdMat.opacityDither;

options.litOptions.cubeMapProjection = stdMat.cubeMapProjection;

Expand Down
2 changes: 2 additions & 0 deletions src/scene/materials/standard-material-parameters.js
Expand Up @@ -95,6 +95,8 @@ const standardMaterialParameterTypes = {
opacity: 'number',
..._textureParameter('opacity'),
opacityFadesSpecular: 'boolean',
opacityDither: 'boolean',
opacityShadowDither: 'boolean',

reflectivity: 'number',
refraction: 'number',
Expand Down
6 changes: 6 additions & 0 deletions src/scene/materials/standard-material.js
Expand Up @@ -375,6 +375,10 @@ 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 is false.
* @property {boolean} opacityShadowDither Used to specify whether shadow opacity is dithered, which
* allows shadow transparency without alpha blending. Defaults is false.
mvaligursky marked this conversation as resolved.
Show resolved Hide resolved
* @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
Expand Down Expand Up @@ -1228,6 +1232,8 @@ function _defineMaterialProps() {
_defineFlag('glossInvert', false);
_defineFlag('sheenGlossInvert', false);
_defineFlag('clearCoatGlossInvert', false);
_defineFlag('opacityDither', false);
_defineFlag('opacityShadowDither', false);

_defineTex2D('diffuse');
_defineTex2D('specular');
Expand Down
4 changes: 4 additions & 0 deletions src/scene/shader-lib/chunks/chunks.js
Expand Up @@ -14,6 +14,7 @@ import baseVS from './lit/vert/base.js';
import baseNineSlicedPS from './lit/frag/baseNineSliced.js';
import baseNineSlicedVS from './lit/vert/baseNineSliced.js';
import baseNineSlicedTiledPS from './lit/frag/baseNineSlicedTiled.js';
import bayerPS from './common/frag/bayer.js';
import biasConstPS from './lit/frag/biasConst.js';
import blurVSMPS from './lit/frag/blurVSM.js';
import clearCoatPS from './standard/frag/clearCoat.js';
Expand Down Expand Up @@ -91,6 +92,7 @@ import normalSkinnedVS from './lit/vert/normalSkinned.js';
import normalXYPS from './standard/frag/normalXY.js';
import normalXYZPS from './standard/frag/normalXYZ.js';
import opacityPS from './standard/frag/opacity.js';
import opacityDitherPS from './standard/frag/opacity-dither.js';
import outputPS from './lit/frag/output.js';
import outputAlphaPS from './lit/frag/outputAlpha.js';
import outputAlphaOpaquePS from './lit/frag/outputAlphaOpaque.js';
Expand Down Expand Up @@ -223,6 +225,7 @@ const shaderChunks = {
baseNineSlicedPS,
baseNineSlicedVS,
baseNineSlicedTiledPS,
bayerPS,
biasConstPS,
blurVSMPS,
clearCoatPS,
Expand Down Expand Up @@ -300,6 +303,7 @@ const shaderChunks = {
normalXYPS,
normalXYZPS,
opacityPS,
opacityDitherPS,
outputPS,
outputAlphaPS,
outputAlphaOpaquePS,
Expand Down
23 changes: 23 additions & 0 deletions src/scene/shader-lib/chunks/common/frag/bayer.js
@@ -0,0 +1,23 @@
// procedural Bayer matrix, based on: https://www.shadertoy.com/view/Mlt3z8

export default /* glsl */`
// 2x2 bayer matrix [1 2][3 0], p in [0,1]
float bayer2(vec2 p) {
return mod(2.0 * p.y + p.x + 1.0, 4.0);
}

// 4x4 matrix, p - pixel coordinate
float bayer4(vec2 p) {
vec2 p1 = mod(p, 2.0);
vec2 p2 = floor(0.5 * mod(p, 4.0));
return 4.0 * bayer2(p1) + bayer2(p2);
}

// 8x8 matrix, p - pixel coordinate
float bayer8(vec2 p) {
vec2 p1 = mod(p, 2.0);
vec2 p2 = floor(0.5 * mod(p, 4.0));
vec2 p4 = floor(0.25 * mod(p, 8.0));
return 4.0 * (4.0 * bayer2(p1) + bayer2(p2)) + bayer2(p4);
}
`;
6 changes: 6 additions & 0 deletions src/scene/shader-lib/chunks/standard/frag/opacity-dither.js
@@ -0,0 +1,6 @@
export default /* glsl */`
void opacityDither(float alpha) {
if (alpha <= bayer8(floor(mod(gl_FragCoord.xy, 8.0))) / 64.0)
discard;
}
`;