Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions examples/src/examples/graphics/custom-compose-shader.controls.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as pc from 'playcanvas';

/**
* @param {import('../../app/components/Example.mjs').ControlOptions} options - The options.
* @returns {JSX.Element} The returned JSX Element.
*/
export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
const { BindingTwoWay, SelectInput, LabelGroup, SliderInput } = ReactPCUI;
return fragment(
jsx(
LabelGroup,
{ text: 'Tonemap' },
jsx(SelectInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'data.sceneTonemapping' },
type: 'number',
options: [
{ v: pc.TONEMAP_LINEAR, t: 'LINEAR' },
{ v: pc.TONEMAP_FILMIC, t: 'FILMIC' },
{ v: pc.TONEMAP_HEJL, t: 'HEJL' },
{ v: pc.TONEMAP_ACES, t: 'ACES' },
{ v: pc.TONEMAP_ACES2, t: 'ACES2' },
{ v: pc.TONEMAP_NEUTRAL, t: 'NEUTRAL' }
]
})
),
jsx(
LabelGroup,
{ text: 'Pixel Size' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'data.pixelSize' },
min: 8,
max: 20,
precision: 0
})
),
jsx(
LabelGroup,
{ text: 'Intensity' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'data.pixelationIntensity' },
min: 0,
max: 1,
precision: 2
})
)
);
};
220 changes: 220 additions & 0 deletions examples/src/examples/graphics/custom-compose-shader.example.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// @config DESCRIPTION This example shows how to customize the final compose pass by injecting a simple pixelation post-effect. Useful if no additional render passes are needed. Changes are applied globally to all CameraFrames.
import { data } from 'examples/observer';
import { deviceType, rootPath } from 'examples/utils';
import * as pc from 'playcanvas';

const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
window.focus();

const assets = {
orbit: new pc.Asset('script', 'script', { url: `${rootPath}/static/scripts/camera/orbit-camera.js` }),
apartment: new pc.Asset('apartment', 'container', { url: `${rootPath}/static/assets/models/apartment.glb` }),
love: new pc.Asset('love', 'container', { url: `${rootPath}/static/assets/models/love.glb` }),
helipad: new pc.Asset(
'helipad-env-atlas',
'texture',
{ url: `${rootPath}/static/assets/cubemaps/helipad-env-atlas.png` },
{ type: pc.TEXTURETYPE_RGBP, mipmaps: false }
)
};

const gfxOptions = {
deviceTypes: [deviceType],
glslangUrl: `${rootPath}/static/lib/glslang/glslang.js`,
twgslUrl: `${rootPath}/static/lib/twgsl/twgsl.js`,

// The scene is rendered to an antialiased texture, so we disable antialiasing on the canvas
// to avoid the additional cost. This is only used for the UI which renders on top of the
// post-processed scene, and we're typically happy with some aliasing on the UI.
antialias: false
};

const device = await pc.createGraphicsDevice(canvas, gfxOptions);
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);

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(window);

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

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 with low intensity
app.scene.envAtlas = assets.helipad.resource;
app.scene.exposure = 1.2;

// create an instance of the apartment and add it to the scene
const platformEntity = assets.apartment.resource.instantiateRenderEntity();
platformEntity.setLocalScale(30, 30, 30);
app.root.addChild(platformEntity);

// load a love sign model and add it to the scene
const loveEntity = assets.love.resource.instantiateRenderEntity();
loveEntity.setLocalPosition(-80, 30, -20);
loveEntity.setLocalScale(130, 130, 130);
loveEntity.rotate(0, -90, 0);
app.root.addChild(loveEntity);

// make the love sign emissive to bloom
const loveMaterial = loveEntity.findByName('s.0009_Standard_FF00BB_0').render.meshInstances[0].material;
loveMaterial.emissive = pc.Color.YELLOW;
loveMaterial.emissiveIntensity = 200;
loveMaterial.update();

// adjust all materials of the love sign to disable dynamic refraction
loveEntity.findComponents('render').forEach((render) => {
render.meshInstances.forEach((meshInstance) => {
meshInstance.material.useDynamicRefraction = false;
});
});

// Create an Entity with a camera component
const cameraEntity = new pc.Entity();
cameraEntity.addComponent('camera', {
farClip: 1500,
fov: 80
});

const focusPoint = new pc.Entity();
focusPoint.setLocalPosition(-80, 80, -20);

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

cameraEntity.setLocalPosition(-50, 100, 220);
cameraEntity.lookAt(0, 0, 100);
app.root.addChild(cameraEntity);

// ------ Custom shader chunks for the camera frame ------

// Note: Override these empty chunks with your own custom code. Available chunk names:
// - composeDeclarationsPS: declarations for your custom code
// - composeMainStartPS: code to run at the start of the compose code
// - composeMainEndPS: code to run at the end of the compose code

// Pixelation shader is based on this shadertoy shader: https://www.shadertoy.com/view/4dsXWs

// Define the pixelation helper in declarations so it's available in main
pc.ShaderChunks.get(device, pc.SHADERLANGUAGE_GLSL).set('composeDeclarationsPS', `
uniform float pixelationTilePixels;
uniform float pixelationIntensity;
vec3 pixelateResult(vec3 color, vec2 uv, vec2 invRes) {
vec2 tileUV = vec2(pixelationTilePixels, pixelationTilePixels) * invRes;
vec2 centerUv = (floor(uv / tileUV) + 0.5) * tileUV;

vec2 local = (uv - centerUv) / tileUV;
float dist = length(local);
float radius = 0.35;
float edge = fwidth(dist) * 1.5;
float mask = 1.0 - smoothstep(radius, radius + edge, dist);
vec3 dotResult = mix(vec3(0.0), color, mask);
return mix(color, dotResult, pixelationIntensity);
}
`);

// WGSL equivalent declarations
pc.ShaderChunks.get(device, pc.SHADERLANGUAGE_WGSL).set('composeDeclarationsPS', `
uniform pixelationTilePixels: f32;
uniform pixelationIntensity: f32;
fn pixelateResult(color: vec3f, uv: vec2f, invRes: vec2f) -> vec3f {
let tileUV = vec2f(uniform.pixelationTilePixels, uniform.pixelationTilePixels) * invRes;
let centerUv = (floor(uv / tileUV) + vec2f(0.5, 0.5)) * tileUV;
let local = (uv - centerUv) / tileUV;
let dist = length(local);
let radius: f32 = 0.35;
let edge: f32 = fwidth(dist) * 1.5;
let mask: f32 = 1.0 - smoothstep(radius, radius + edge, dist);
let dotResult = vec3f(0.0) * (1.0 - mask) + color * mask;
return mix(color, dotResult, uniform.pixelationIntensity);
}
`);

// Call the helper at the end of compose to apply on top of previous effects
pc.ShaderChunks.get(device, pc.SHADERLANGUAGE_GLSL).set('composeMainEndPS', `
result = pixelateResult(result, uv, sceneTextureInvRes);
`);

// WGSL equivalent call
pc.ShaderChunks.get(device, pc.SHADERLANGUAGE_WGSL).set('composeMainEndPS', `
result = pixelateResult(result, uv, uniform.sceneTextureInvRes);
`);

// ------ Custom render passes set up ------

const cameraFrame = new pc.CameraFrame(app, cameraEntity.camera);
cameraFrame.rendering.samples = 4;
cameraFrame.bloom.intensity = 0.03;
cameraFrame.bloom.blurLevel = 7;
cameraFrame.vignette.inner = 0.5;
cameraFrame.vignette.outer = 1;
cameraFrame.vignette.curvature = 0.5;
cameraFrame.vignette.intensity = 0.8;

cameraFrame.update();

// apply UI changes (tone mapping only)
data.on('*:set', (/** @type {string} */ path, value) => {
if (path === 'data.sceneTonemapping') {
// postprocessing tone mapping
cameraFrame.rendering.toneMapping = value;
cameraFrame.update();
}

if (path === 'data.pixelSize') {
// global uniform for pixelation tile size
device.scope.resolve('pixelationTilePixels').setValue(value);
}

if (path === 'data.pixelationIntensity') {
device.scope.resolve('pixelationIntensity').setValue(value);
}
});

// set initial values
data.set('data', {
sceneTonemapping: pc.TONEMAP_NEUTRAL,
pixelSize: 8,
pixelationIntensity: 0.5
});
});

export { app };
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 35 additions & 5 deletions src/extras/render-passes/render-pass-compose.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Color } from '../../core/math/color.js';
import { RenderPassShaderQuad } from '../../scene/graphics/render-pass-shader-quad.js';
import { GAMMA_NONE, GAMMA_SRGB, gammaNames, TONEMAP_LINEAR, tonemapNames } from '../../scene/constants.js';
import { ShaderChunks } from '../../scene/shader-lib/shader-chunks.js';
import { hashCode } from '../../core/hash.js';
import { SEMANTIC_POSITION, SHADERLANGUAGE_GLSL, SHADERLANGUAGE_WGSL } from '../../platform/graphics/constants.js';
import { ShaderUtils } from '../../scene/shader-lib/shader-utils.js';
import { composeChunksGLSL } from '../../scene/shader-lib/glsl/collections/compose-chunks-glsl.js';
Expand All @@ -19,6 +20,9 @@ import { composeChunksWGSL } from '../../scene/shader-lib/wgsl/collections/compo
* @ignore
*/
class RenderPassCompose extends RenderPassShaderQuad {
/**
* @type {Texture|null}
*/
sceneTexture = null;

bloomIntensity = 0.01;
Expand Down Expand Up @@ -78,6 +82,13 @@ class RenderPassCompose extends RenderPassShaderQuad {

_debug = null;

// track user-provided custom compose chunks
_customComposeChunks = new Map([
['composeDeclarationsPS', ''],
['composeMainStartPS', ''],
['composeMainEndPS', '']
]);

constructor(graphicsDevice) {
super(graphicsDevice);

Expand Down Expand Up @@ -247,12 +258,29 @@ class RenderPassCompose extends RenderPassShaderQuad {
this._shaderDirty = true;
}

const shaderChunks = ShaderChunks.get(this.device, this.device.isWebGPU ? SHADERLANGUAGE_WGSL : SHADERLANGUAGE_GLSL);

// detect changes to custom compose chunks and mark shader dirty
for (const [name, prevValue] of this._customComposeChunks.entries()) {
const currentValue = shaderChunks.get(name);
if (currentValue !== prevValue) {
this._customComposeChunks.set(name, currentValue);
this._shaderDirty = true;
}
}

// need to rebuild shader
if (this._shaderDirty) {
this._shaderDirty = false;

const gammaCorrectionName = gammaNames[this._gammaCorrection];

// include hashes of custom compose chunks to ensure unique program for overrides
const customChunks = this._customComposeChunks;
const declHash = hashCode(customChunks.get('composeDeclarationsPS') ?? '');
const startHash = hashCode(customChunks.get('composeMainStartPS') ?? '');
const endHash = hashCode(customChunks.get('composeMainEndPS') ?? '');

const key =
`${this.toneMapping}` +
`-${gammaCorrectionName}` +
Expand All @@ -266,7 +294,8 @@ class RenderPassCompose extends RenderPassShaderQuad {
`-${this.fringingEnabled ? 'fringing' : 'nofringing'}` +
`-${this.taaEnabled ? 'taa' : 'notaa'}` +
`-${this.isSharpnessEnabled ? 'cas' : 'nocas'}` +
`-${this._debug ?? ''}`;
`-${this._debug ?? ''}` +
`-decl${declHash}-start${startHash}-end${endHash}`;

if (this._key !== key) {
this._key = key;
Expand All @@ -286,7 +315,7 @@ class RenderPassCompose extends RenderPassShaderQuad {
if (this.isSharpnessEnabled) defines.set('CAS', true);
if (this._debug) defines.set('DEBUG_COMPOSE', this._debug);

const includes = new Map(ShaderChunks.get(this.device, this.device.isWebGPU ? SHADERLANGUAGE_WGSL : SHADERLANGUAGE_GLSL));
const includes = new Map(shaderChunks);

this.shader = ShaderUtils.createShader(this.device, {
uniqueName: `ComposeShader-${key}`,
Expand All @@ -302,9 +331,10 @@ class RenderPassCompose extends RenderPassShaderQuad {

execute() {

this.sceneTextureId.setValue(this.sceneTexture);
this.sceneTextureInvResValue[0] = 1.0 / this.sceneTexture.width;
this.sceneTextureInvResValue[1] = 1.0 / this.sceneTexture.height;
const sceneTex = this.sceneTexture;
this.sceneTextureId.setValue(sceneTex);
this.sceneTextureInvResValue[0] = 1.0 / sceneTex.width;
this.sceneTextureInvResValue[1] = 1.0 / sceneTex.height;
this.sceneTextureInvResId.setValue(this.sceneTextureInvResValue);

if (this._bloomTexture) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ export default /* glsl */`
#include "composeCasPS"
#include "composeColorLutPS"

#include "composeDeclarationsPS"

void main() {

#include "composeMainStartPS"

vec2 uv = uv0;

// TAA pass renders upside-down on WebGPU, flip it here
Expand Down Expand Up @@ -71,6 +76,8 @@ export default /* glsl */`
result = applyVignette(result, uv);
#endif

#include "composeMainEndPS"

// Debug output handling in one centralized location
#ifdef DEBUG_COMPOSE
#if DEBUG_COMPOSE == scene
Expand Down
Loading