UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

222 lines (219 loc) 17.7 kB
import { math } from '../../core/math/math.js'; import { Color } from '../../core/math/color.js'; import { RenderPassShaderQuad } from '../../scene/graphics/render-pass-shader-quad.js'; import { shaderChunks } from '../../scene/shader-lib/chunks/chunks.js'; import { GAMMA_NONE, GAMMA_SRGB, gammaNames, tonemapNames, TONEMAP_LINEAR } from '../../scene/constants.js'; // Contrast Adaptive Sharpening (CAS) is used to apply the sharpening. It's based on AMD's // FidelityFX CAS, WebGL implementation: https://www.shadertoy.com/view/wtlSWB. It's best to run it // on a tone-mapped color buffer after post-processing, but before the UI, and so this is the // obvious place to put it to avoid a separate render pass, even though we need to handle running it // before the tone-mapping. var fragmentShader = '\n\n #include "tonemappingPS"\n #include "gammaPS"\n\n varying vec2 uv0;\n uniform sampler2D sceneTexture;\n uniform vec2 sceneTextureInvRes;\n\n #ifdef BLOOM\n uniform sampler2D bloomTexture;\n uniform float bloomIntensity;\n #endif\n\n #ifdef DOF\n uniform sampler2D cocTexture;\n uniform sampler2D blurTexture;\n\n // Samples the DOF blur and CoC textures. When the blur texture was generated at lower resolution,\n // upscale it to the full resolution using bilinear interpolation to hide the blockiness along COC edges.\n vec3 dofBlur(vec2 uv, out vec2 coc) {\n coc = texture2DLod(cocTexture, uv, 0.0).rg;\n\n #if DOF_UPSCALE\n vec2 blurTexelSize = 1.0 / vec2(textureSize(blurTexture, 0));\n vec3 bilinearBlur = vec3(0.0);\n float totalWeight = 0.0;\n\n // 3x3 grid of neighboring texels\n for (int i = -1; i <= 1; i++) {\n for (int j = -1; j <= 1; j++) {\n vec2 offset = vec2(i, j) * blurTexelSize;\n vec2 cocSample = texture2DLod(cocTexture, uv + offset, 0.0).rg;\n vec3 blurSample = texture2DLod(blurTexture, uv + offset, 0.0).rgb;\n\n // Accumulate the weighted blur sample\n float cocWeight = clamp(cocSample.r + cocSample.g, 0.0, 1.0);\n bilinearBlur += blurSample * cocWeight;\n totalWeight += cocWeight;\n }\n }\n\n // normalize the accumulated color\n if (totalWeight > 0.0) {\n bilinearBlur /= totalWeight;\n }\n\n return bilinearBlur;\n #else\n // when blurTexture is full resolution, just sample it, no upsampling\n return texture2DLod(blurTexture, uv, 0.0).rgb;\n #endif\n }\n\n #endif\n\n #ifdef SSAO\n #define SSAO_TEXTURE\n #endif\n\n #if DEBUG_COMPOSE == ssao\n #define SSAO_TEXTURE\n #endif\n\n #ifdef SSAO_TEXTURE\n uniform sampler2D ssaoTexture;\n #endif\n\n #ifdef GRADING\n uniform vec3 brightnessContrastSaturation;\n uniform vec3 tint;\n\n // for all parameters, 1.0 is the no-change value\n vec3 colorGradingHDR(vec3 color, float brt, float sat, float con)\n {\n // tint\n color *= tint;\n\n // brightness\n color = color * brt;\n\n // saturation\n float grey = dot(color, vec3(0.3, 0.59, 0.11));\n grey = grey / max(1.0, max(color.r, max(color.g, color.b))); // Normalize luminance in HDR to preserve intensity (optional)\n color = mix(vec3(grey), color, sat);\n\n // contrast\n return mix(vec3(0.5), color, con);\n }\n \n #endif\n\n #ifdef VIGNETTE\n\n uniform vec4 vignetterParams;\n\n float vignette(vec2 uv) {\n\n float inner = vignetterParams.x;\n float outer = vignetterParams.y;\n float curvature = vignetterParams.z;\n float intensity = vignetterParams.w;\n\n // edge curvature\n vec2 curve = pow(abs(uv * 2.0 -1.0), vec2(1.0 / curvature));\n\n // distance to edge\n float edge = pow(length(curve), curvature);\n\n // gradient and intensity\n return 1.0 - intensity * smoothstep(inner, outer, edge);\n } \n\n #endif\n\n #ifdef FRINGING\n\n uniform float fringingIntensity;\n\n vec3 fringing(vec2 uv, vec3 color) {\n\n // offset depends on the direction from the center, raised to power to make it stronger away from the center\n vec2 centerDistance = uv - 0.5;\n vec2 offset = fringingIntensity * pow(centerDistance, vec2(2.0, 2.0));\n\n color.r = texture2D(sceneTexture, uv - offset).r;\n color.b = texture2D(sceneTexture, uv + offset).b;\n return color;\n }\n\n #endif\n\n #ifdef CAS\n\n uniform float sharpness;\n\n // reversible LDR <-> HDR tone mapping, as CAS needs LDR input\n // based on: https://gpuopen.com/learn/optimized-reversible-tonemapper-for-resolve/\n float maxComponent(float x, float y, float z) { return max(x, max(y, z)); }\n vec3 toSDR(vec3 c) { return c / (1.0 + maxComponent(c.r, c.g, c.b)); }\n vec3 toHDR(vec3 c) { return c / (1.0 - maxComponent(c.r, c.g, c.b)); }\n\n vec3 cas(vec3 color, vec2 uv, float sharpness) {\n\n float x = sceneTextureInvRes.x;\n float y = sceneTextureInvRes.y;\n\n // sample 4 neighbors around the already sampled pixel, and convert it to SDR\n vec3 a = toSDR(texture2DLod(sceneTexture, uv + vec2(0.0, -y), 0.0).rgb);\n vec3 b = toSDR(texture2DLod(sceneTexture, uv + vec2(-x, 0.0), 0.0).rgb);\n vec3 c = toSDR(color.rgb);\n vec3 d = toSDR(texture2DLod(sceneTexture, uv + vec2(x, 0.0), 0.0).rgb);\n vec3 e = toSDR(texture2DLod(sceneTexture, uv + vec2(0.0, y), 0.0).rgb);\n\n // apply the sharpening\n float min_g = min(a.g, min(b.g, min(c.g, min(d.g, e.g))));\n float max_g = max(a.g, max(b.g, max(c.g, max(d.g, e.g))));\n float sharpening_amount = sqrt(min(1.0 - max_g, min_g) / max_g);\n float w = sharpening_amount * sharpness;\n vec3 res = (w * (a + b + d + e) + c) / (4.0 * w + 1.0);\n\n // remove negative colors\n res = max(res, 0.0);\n\n // convert back to HDR\n return toHDR(res);\n }\n\n #endif\n\n void main() {\n\n vec2 uv = uv0;\n\n // TAA pass renders upside-down on WebGPU, flip it here\n #ifdef TAA\n #ifdef WEBGPU\n uv.y = 1.0 - uv.y;\n #endif\n #endif\n\n vec4 scene = texture2DLod(sceneTexture, uv, 0.0);\n vec3 result = scene.rgb;\n\n #ifdef CAS\n result = cas(result, uv, sharpness);\n #endif\n\n #ifdef DOF\n vec2 coc;\n vec3 blur = dofBlur(uv0, coc);\n result = mix(result, blur, coc.r + coc.g);\n #endif\n\n #ifdef SSAO_TEXTURE\n mediump float ssao = texture2DLod(ssaoTexture, uv0, 0.0).r;\n #endif\n\n #ifdef SSAO\n result *= ssao;\n #endif\n\n #ifdef FRINGING\n result = fringing(uv, result);\n #endif\n\n #ifdef BLOOM\n vec3 bloom = texture2DLod(bloomTexture, uv0, 0.0).rgb;\n result += bloom * bloomIntensity;\n #endif\n\n #ifdef GRADING\n // color grading takes place in HDR space before tone mapping\n result = colorGradingHDR(result, brightnessContrastSaturation.x, brightnessContrastSaturation.z, brightnessContrastSaturation.y);\n #endif\n\n result = toneMap(result);\n\n #ifdef VIGNETTE\n mediump float vig = vignette(uv);\n result *= vig;\n #endif\n\n // debug output\n #ifdef DEBUG_COMPOSE\n\n #ifdef BLOOM\n #if DEBUG_COMPOSE == bloom\n result = bloom * bloomIntensity;\n #endif\n #endif\n\n #ifdef DOF\n #ifdef DEBUG_COMPOSE == dofcoc\n result = vec3(coc, 0.0);\n #endif\n #ifdef DEBUG_COMPOSE == dofblur\n result = blur;\n #endif\n #endif\n\n #if DEBUG_COMPOSE == ssao\n result = vec3(ssao);\n #endif\n\n #if DEBUG_COMPOSE == vignette\n result = vec3(vig);\n #endif\n\n #if DEBUG_COMPOSE == scene\n result = scene.rgb;\n #endif\n\n #endif\n\n result = gammaCorrectOutput(result);\n\n gl_FragColor = vec4(result, scene.a);\n }\n'; /** * Render pass implementation of the final post-processing composition. * * @category Graphics * @ignore */ class RenderPassCompose extends RenderPassShaderQuad { set debug(value) { if (this._debug !== value) { this._debug = value; this._shaderDirty = true; } } get debug() { return this._debug; } set bloomTexture(value) { if (this._bloomTexture !== value) { this._bloomTexture = value; this._shaderDirty = true; } } get bloomTexture() { return this._bloomTexture; } set cocTexture(value) { if (this._cocTexture !== value) { this._cocTexture = value; this._shaderDirty = true; } } get cocTexture() { return this._cocTexture; } set ssaoTexture(value) { if (this._ssaoTexture !== value) { this._ssaoTexture = value; this._shaderDirty = true; } } get ssaoTexture() { return this._ssaoTexture; } set taaEnabled(value) { if (this._taaEnabled !== value) { this._taaEnabled = value; this._shaderDirty = true; } } get taaEnabled() { return this._taaEnabled; } set gradingEnabled(value) { if (this._gradingEnabled !== value) { this._gradingEnabled = value; this._shaderDirty = true; } } get gradingEnabled() { return this._gradingEnabled; } set vignetteEnabled(value) { if (this._vignetteEnabled !== value) { this._vignetteEnabled = value; this._shaderDirty = true; } } get vignetteEnabled() { return this._vignetteEnabled; } set fringingEnabled(value) { if (this._fringingEnabled !== value) { this._fringingEnabled = value; this._shaderDirty = true; } } get fringingEnabled() { return this._fringingEnabled; } set toneMapping(value) { if (this._toneMapping !== value) { this._toneMapping = value; this._shaderDirty = true; } } get toneMapping() { return this._toneMapping; } set sharpness(value) { if (this._sharpness !== value) { this._sharpness = value; this._shaderDirty = true; } } get sharpness() { return this._sharpness; } get isSharpnessEnabled() { return this._sharpness > 0; } postInit() { // clear all buffers to avoid them being loaded from memory this.setClearColor(Color.BLACK); this.setClearDepth(1.0); this.setClearStencil(0); } frameUpdate() { var _this_renderTarget; // detect if the render target is srgb vs execute manual srgb conversion var rt = (_this_renderTarget = this.renderTarget) != null ? _this_renderTarget : this.device.backBuffer; var srgb = rt.isColorBufferSrgb(0); var neededGammaCorrection = srgb ? GAMMA_NONE : GAMMA_SRGB; if (this._gammaCorrection !== neededGammaCorrection) { this._gammaCorrection = neededGammaCorrection; this._shaderDirty = true; } // need to rebuild shader if (this._shaderDirty) { this._shaderDirty = false; var gammaCorrectionName = gammaNames[this._gammaCorrection]; var _this__debug; var key = "" + this.toneMapping + ("-" + gammaCorrectionName) + ("-" + (this.bloomTexture ? 'bloom' : 'nobloom')) + ("-" + (this.cocTexture ? 'dof' : 'nodof')) + ("-" + (this.blurTextureUpscale ? 'dofupscale' : '')) + ("-" + (this.ssaoTexture ? 'ssao' : 'nossao')) + ("-" + (this.gradingEnabled ? 'grading' : 'nograding')) + ("-" + (this.vignetteEnabled ? 'vignette' : 'novignette')) + ("-" + (this.fringingEnabled ? 'fringing' : 'nofringing')) + ("-" + (this.taaEnabled ? 'taa' : 'notaa')) + ("-" + (this.isSharpnessEnabled ? 'cas' : 'nocas')) + ("-" + ((_this__debug = this._debug) != null ? _this__debug : '')); if (this._key !== key) { this._key = key; var defines = new Map(); defines.set('TONEMAP', tonemapNames[this.toneMapping]); defines.set('GAMMA', gammaCorrectionName); if (this.bloomTexture) defines.set('BLOOM', true); if (this.cocTexture) defines.set('DOF', true); if (this.blurTextureUpscale) defines.set('DOF_UPSCALE', true); if (this.ssaoTexture) defines.set('SSAO', true); if (this.gradingEnabled) defines.set('GRADING', true); if (this.vignetteEnabled) defines.set('VIGNETTE', true); if (this.fringingEnabled) defines.set('FRINGING', true); if (this.taaEnabled) defines.set('TAA', true); if (this.isSharpnessEnabled) defines.set('CAS', true); if (this._debug) defines.set('DEBUG_COMPOSE', this._debug); var includes = new Map(Object.entries(shaderChunks)); this.shader = this.createQuadShader("ComposeShader-" + key, fragmentShader, { fragmentIncludes: includes, fragmentDefines: defines }); } } } execute() { this.sceneTextureId.setValue(this.sceneTexture); this.sceneTextureInvResValue[0] = 1.0 / this.sceneTexture.width; this.sceneTextureInvResValue[1] = 1.0 / this.sceneTexture.height; this.sceneTextureInvResId.setValue(this.sceneTextureInvResValue); if (this._bloomTexture) { this.bloomTextureId.setValue(this._bloomTexture); this.bloomIntensityId.setValue(this.bloomIntensity); } if (this._cocTexture) { this.cocTextureId.setValue(this._cocTexture); this.blurTextureId.setValue(this.blurTexture); } if (this._ssaoTexture) { this.ssaoTextureId.setValue(this._ssaoTexture); } if (this._gradingEnabled) { this.bcsId.setValue([ this.gradingBrightness, this.gradingContrast, this.gradingSaturation ]); this.tintId.setValue([ this.gradingTint.r, this.gradingTint.g, this.gradingTint.b ]); } if (this._vignetteEnabled) { this.vignetterParamsId.setValue([ this.vignetteInner, this.vignetteOuter, this.vignetteCurvature, this.vignetteIntensity ]); } if (this._fringingEnabled) { // relative to a fixed texture resolution to preserve size regardless of the resolution this.fringingIntensityId.setValue(this.fringingIntensity / 1024); } if (this.isSharpnessEnabled) { this.sharpnessId.setValue(math.lerp(-0.125, -0.2, this.sharpness)); } super.execute(); } constructor(graphicsDevice){ super(graphicsDevice), this.sceneTexture = null, this.bloomIntensity = 0.01, this._bloomTexture = null, this._cocTexture = null, this.blurTexture = null, this.blurTextureUpscale = false, this._ssaoTexture = null, this._toneMapping = TONEMAP_LINEAR, this._gradingEnabled = false, this.gradingSaturation = 1, this.gradingContrast = 1, this.gradingBrightness = 1, this.gradingTint = new Color(1, 1, 1, 1), this._shaderDirty = true, this._vignetteEnabled = false, this.vignetteInner = 0.5, this.vignetteOuter = 1.0, this.vignetteCurvature = 0.5, this.vignetteIntensity = 0.3, this._fringingEnabled = false, this.fringingIntensity = 10, this._taaEnabled = false, this._sharpness = 0.5, this._gammaCorrection = GAMMA_SRGB, this._key = '', this._debug = null; var { scope } = graphicsDevice; this.sceneTextureId = scope.resolve('sceneTexture'); this.bloomTextureId = scope.resolve('bloomTexture'); this.cocTextureId = scope.resolve('cocTexture'); this.ssaoTextureId = scope.resolve('ssaoTexture'); this.blurTextureId = scope.resolve('blurTexture'); this.bloomIntensityId = scope.resolve('bloomIntensity'); this.bcsId = scope.resolve('brightnessContrastSaturation'); this.tintId = scope.resolve('tint'); this.vignetterParamsId = scope.resolve('vignetterParams'); this.fringingIntensityId = scope.resolve('fringingIntensity'); this.sceneTextureInvResId = scope.resolve('sceneTextureInvRes'); this.sceneTextureInvResValue = new Float32Array(2); this.sharpnessId = scope.resolve('sharpness'); } } export { RenderPassCompose };