Testing out a possible FX Pipeline

This commit is contained in:
Richard Davey 2023-02-13 01:30:22 +00:00
parent 98ced9a38b
commit 963ff647e6
12 changed files with 497 additions and 122 deletions

View file

@ -4,8 +4,6 @@
* @license {@link https://opensource.org/licenses/MIT|MIT License}
*/
var Bloom = require('../../renderer/webgl/pipelines/fx/Bloom.js');
/**
* Provides methods used for setting the FX values of a Game Object.
* Should be applied as a mixin and not used directly.
@ -17,6 +15,8 @@ var Bloom = require('../../renderer/webgl/pipelines/fx/Bloom.js');
var FX = {
fx: null,
/**
* The amount of extra padding to be applied to this Game Object
* when it is being rendered by a PreFX or SpriteFX Pipeline.
@ -93,18 +93,50 @@ var FX = {
{
},
enableFX: function ()
{
this.fx = {
glow: {
quality: 0.1,
distance: 10,
outerStrength: 4,
innerStrength: 0,
knockout: false,
color: [ 1, 1, 1, 1 ]
},
shadow: {
x: 0,
y: 0,
decay: 0.1,
power: 1.0,
shadowColor: [ 0, 0, 0, 1 ],
samples: 12,
intensity: 1
}
};
var renderer = this.scene.sys.renderer;
if (!renderer)
{
return this;
}
var pipeline;
var pipelines = renderer.pipelines;
if (pipelines)
{
pipeline = pipelines.FX_PIPELINE;
}
this.pipeline = pipeline;
return this;
},
addBloom: function (r, g, b)
{
var instance = new Bloom(this.scene.sys.game);
instance.gameObject = this;
instance.setColor(r, g, b);
this.postPipelines.push(instance);
this.hasPostPipeline = true;
return instance;
}
};

View file

@ -14,6 +14,7 @@ var SnapCeil = require('../../math/snap/SnapCeil');
// Default Phaser 3 Pipelines
var BitmapMaskPipeline = require('./pipelines/BitmapMaskPipeline');
var FXPipeline = require('./pipelines/FXPipeline');
var LightPipeline = require('./pipelines/LightPipeline');
var MobilePipeline = require('./pipelines/MobilePipeline');
var MultiPipeline = require('./pipelines/MultiPipeline');
@ -30,7 +31,7 @@ var UtilityPipeline = require('./pipelines/UtilityPipeline');
* The `WebGLRenderer` owns a single instance of the Pipeline Manager, which you can access
* via the `WebGLRenderer.pipelines` property.
*
* By default, there are 7 pipelines installed into the Pipeline Manager when Phaser boots:
* By default, there are 9 pipelines installed into the Pipeline Manager when Phaser boots:
*
* 1. The Multi Pipeline. Responsible for all multi-texture rendering, i.e. Sprites and Tilemaps.
* 2. The Rope Pipeline. Responsible for rendering the Rope Game Object.
@ -40,6 +41,7 @@ var UtilityPipeline = require('./pipelines/UtilityPipeline');
* 6. The Bitmap Mask Pipeline. Responsible for Bitmap Mask rendering.
* 7. The Utility Pipeline. Responsible for providing lots of handy texture manipulation functions.
* 8. The Mobile Pipeline. Responsible for rendering on mobile with single-bound textures.
* 9. The FX Pipeline. Responsible for rendering Game Objects with special FX applied to them.
*
* You can add your own custom pipeline via the `PipelineManager.add` method. Pipelines are
* identified by unique string-based keys.
@ -92,7 +94,8 @@ var PipelineManager = new Class({
[ CONST.ROPE_PIPELINE, RopePipeline ],
[ CONST.LIGHT_PIPELINE, LightPipeline ],
[ CONST.POINTLIGHT_PIPELINE, PointLightPipeline ],
[ CONST.MOBILE_PIPELINE, MobilePipeline ]
[ CONST.MOBILE_PIPELINE, MobilePipeline ],
[ CONST.FX_PIPELINE, FXPipeline ]
]);
/**
@ -186,7 +189,7 @@ var PipelineManager = new Class({
/**
* A constant-style reference to the Mobile Pipeline Instance.
*
* This is the default Phaser 3 pipeline and is used by the WebGL Renderer to manage
* This is the default Phaser 3 mobile pipeline and is used by the WebGL Renderer to manage
* camera effects and more on mobile devices. This property is set during the `boot` method.
*
* @name Phaser.Renderer.WebGL.PipelineManager#MOBILE_PIPELINE
@ -196,6 +199,19 @@ var PipelineManager = new Class({
*/
this.MOBILE_PIPELINE = null;
/**
* A constant-style reference to the FX Pipeline Instance.
*
* This is the default Phaser 3 FX pipeline and is used by the WebGL Renderer to manage
* Game Objects with special effects enabled. This property is set during the `boot` method.
*
* @name Phaser.Renderer.WebGL.PipelineManager#FX_PIPELINE
* @type {Phaser.Renderer.WebGL.Pipelines.FXPipeline}
* @default null
* @since 3.60.0
*/
this.FX_PIPELINE = null;
/**
* A reference to the Full Frame 1 Render Target that belongs to the
* Utility Pipeline. This property is set during the `boot` method.
@ -378,6 +394,7 @@ var PipelineManager = new Class({
this.MULTI_PIPELINE = this.get(CONST.MULTI_PIPELINE);
this.BITMAPMASK_PIPELINE = this.get(CONST.BITMAPMASK_PIPELINE);
this.MOBILE_PIPELINE = this.get(CONST.MOBILE_PIPELINE);
this.FX_PIPELINE = this.get(CONST.FX_PIPELINE);
// And now the ones in the config, if any
@ -1146,6 +1163,19 @@ var PipelineManager = new Class({
return this.UTILITY_PIPELINE.bind(currentShader);
},
/**
* Sets the FX Pipeline to be the currently bound pipeline.
*
* @method Phaser.Renderer.WebGL.PipelineManager#setFX
* @since 3.60.0
*
* @return {Phaser.Renderer.WebGL.Pipelines.FXPipeline} The FX Pipeline instance.
*/
setFX: function ()
{
return this.set(this.FX_PIPELINE);
},
/**
* Use this to reset the gl context to the state that Phaser requires to continue rendering.
*

View file

@ -0,0 +1,145 @@
/**
* @author Richard Davey <rich@photonstorm.com>
* @copyright 2013-2023 Photon Storm Ltd.
* @license {@link https://opensource.org/licenses/MIT|MIT License}
*/
var Class = require('../../../utils/Class');
var PreFXPipeline = require('./PreFXPipeline');
var BloomFrag = require('../shaders/FXBloom-frag.js');
var GlowFrag = require('../shaders/FXGlow-frag.js');
var ShadowFrag = require('../shaders/FXShadow-frag.js');
var SingleQuadVS = require('../shaders/Single-vert.js');
/**
* @classdesc
*
* @class FX
* @extends Phaser.Renderer.WebGL.Pipelines.PreFXPipeline
* @memberof Phaser.Renderer.WebGL.Pipelines
* @constructor
* @since 3.60.0
*
* @param {Phaser.Game} game - A reference to the Phaser game instance.
*/
var FXPipeline = new Class({
Extends: PreFXPipeline,
initialize:
function FXPipeline (config)
{
var shaders = [];
var fragShader = GlowFrag;
var quality = 0.1;
var distance = 10;
fragShader = fragShader.replace(/__SIZE__/gi, `${(1 / quality / distance).toFixed(7)}`);
fragShader = fragShader.replace(/__DIST__/gi, `${distance.toFixed(0)}.0`);
shaders.push({
name: 'Glow',
fragShader: fragShader,
vertShader: SingleQuadVS
});
fragShader = ShadowFrag;
var samples = 12;
fragShader = fragShader.replace(/__SAMPLES__/gi, samples.toFixed(0));
shaders.push({
name: 'Shadow',
fragShader: fragShader,
vertShader: SingleQuadVS
});
config.shaders = shaders;
PreFXPipeline.call(this, config);
this.bloom = {
steps: 4,
offsetX: 2,
offsetY: 3,
blurStrength: 1,
strength: 1.5,
color: [ 1, 1, 1 ]
};
this.glow = {
quality: 0.1,
distance: 10,
outerStrength: 4,
innerStrength: 0,
knockout: false,
color: [ 1, 1, 1, 1 ]
};
},
boot: function ()
{
PreFXPipeline.prototype.boot.call(this);
console.log(this.shaders);
},
onDraw: function (target1, target2, target3)
{
// tempSprite = current sprite being drawn
this.setShader(this.shaders[4]);
var glow = this.tempSprite.fx.glow;
this.set1f('outerStrength', glow.outerStrength);
this.set1f('innerStrength', glow.innerStrength);
this.set4fv('glowColor', glow.color);
this.setBoolean('knockout', glow.knockout);
this.set2f('resolution', target1.width, target1.height);
this.copy(target1, target2);
this.setShader(this.shaders[5]);
var shadow = this.tempSprite.fx.shadow;
this.set1f('intensity', shadow.intensity);
this.set1f('decay', shadow.decay);
this.set1f('power', shadow.power / shadow.samples);
this.set2f('lightPosition', shadow.x, shadow.y);
this.set4fv('shadowColor', shadow.shadowColor);
this.copy(target2, target1);
this.drawToGame(target1);
},
onBloomDraw: function (target1, target2, target3)
{
this.manager.copyFrame(target1, target3);
var x = (2 / target1.width) * this.offsetX;
var y = (2 / target1.height) * this.offsetY;
for (var i = 0; i < this.steps; i++)
{
this.set2f('uOffset', x, 0);
this.copySprite(target1, target2);
this.set2f('uOffset', 0, y);
this.copySprite(target2, target1);
}
this.blendFrames(target3, target1, target2, this.strength);
this.copyToGame(target2);
}
});
module.exports = FXPipeline;

View file

@ -566,6 +566,51 @@ var PreFXPipeline = new Class({
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
},
// cls: function (target1, target2, target3)
// {
// var gl = this.gl;
// gl.clearColor(0, 0, 0, 0);
// gl.viewport(0, 0, target1.width, target1.height);
// gl.bindFramebuffer(gl.FRAMEBUFFER, target1.framebuffer);
// gl.clear(gl.COLOR_BUFFER_BIT);
// gl.bindFramebuffer(gl.FRAMEBUFFER, target2.framebuffer);
// gl.clear(gl.COLOR_BUFFER_BIT);
// gl.bindFramebuffer(gl.FRAMEBUFFER, target3.framebuffer);
// gl.clear(gl.COLOR_BUFFER_BIT);
// gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// },
copy: function (source, target)
{
var gl = this.gl;
this.set1i('uMainSampler', 0);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, source.texture);
// source and target must always be the same size
gl.viewport(0, 0, source.width, source.height);
this.setUVs(0, 0, 0, 1, 1, 1, 1, 0);
gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, target.texture, 0);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.bufferData(gl.ARRAY_BUFFER, this.quadVertexData, gl.STATIC_DRAW);
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
},
/**
* Draws the `source1` and `source2` Render Targets to the `target` Render Target
* using a linear blend effect, which is controlled by the `strength` parameter.
@ -713,15 +758,23 @@ var PreFXPipeline = new Class({
// var y2 = matrix.getY(xw, y);
// var y3 = matrix.getY(xw, yh);
this.batchVert(x0, y0, 0, 0, 0, 0, 0xffffff);
this.batchVert(x1, y1, 0, 1, 0, 0, 0xffffff);
this.batchVert(x2, y2, 1, 1, 0, 0, 0xffffff);
this.batchVert(x0, y0, 0, 0, 0, 0, 0xffffff);
this.batchVert(x2, y2, 1, 1, 0, 0, 0xffffff);
this.batchVert(x3, y3, 1, 0, 0, 0, 0xffffff);
var white = 0xffffff;
this.batchVert(x0, y0, 0, 0, 0, 0, white);
this.batchVert(x1, y1, 0, 1, 0, 0, white);
this.batchVert(x2, y2, 1, 1, 0, 0, white);
this.batchVert(x0, y0, 0, 0, 0, 0, white);
this.batchVert(x2, y2, 1, 1, 0, 0, white);
this.batchVert(x3, y3, 1, 0, 0, 0, white);
this.flush();
// Clear the source framebuffer out, ready for the next pass
gl.clearColor(0, 0, 0, 0);
gl.bindFramebuffer(gl.FRAMEBUFFER, source.framebuffer);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// No hanging references
this.tempSprite = null;
},

View file

@ -104,7 +104,17 @@ var PIPELINE_CONST = {
* @const
* @since 3.60.0
*/
MOBILE_PIPELINE: 'MobilePipeline'
MOBILE_PIPELINE: 'MobilePipeline',
/**
* The Special FX Pipeline.
*
* @name Phaser.Renderer.WebGL.Pipelines.FX_PIPELINE
* @type {string}
* @const
* @since 3.60.0
*/
FX_PIPELINE: 'FxPipeline'
};

View file

@ -1,83 +0,0 @@
/**
* @author Richard Davey <rich@photonstorm.com>
* @copyright 2013-2023 Photon Storm Ltd.
* @license {@link https://opensource.org/licenses/MIT|MIT License}
*/
var Class = require('../../../../utils/Class');
var SpriteFXPipeline = require('../SpriteFXPipeline');
var BloomFrag = require('../../shaders/FXBloom-frag.js');
/**
* @classdesc
*
* @class Bloom
* @extends Phaser.Renderer.WebGL.Pipelines.SpriteFXPipeline
* @memberof Phaser.Renderer.WebGL.Pipelines.FX
* @constructor
* @since 3.60.0
*
* @param {Phaser.Game} game - A reference to the Phaser game instance.
*/
var Bloom = new Class({
Extends: SpriteFXPipeline,
initialize:
function Bloom (game)
{
console.log('bloom', game);
SpriteFXPipeline.call(this, {
game: game,
fragShader: BloomFrag
});
this.steps = 4;
this.offsetX = 0;
this.offsetY = 0;
this.blurStrength = 1;
this.strength = 1;
this.color = [ 1, 1, 1 ];
},
onPreRender: function ()
{
this.set1f('uStrength', this.blurStrength);
this.set3fv('uColor', this.color);
},
onDraw: function (target1, target2, target3)
{
this.manager.copyFrame(target1, target3);
var x = (2 / target1.width) * this.offsetX;
var y = (2 / target1.height) * this.offsetY;
for (var i = 0; i < this.steps; i++)
{
this.set2f('uOffset', x, 0);
this.copySprite(target1, target2);
this.set2f('uOffset', 0, y);
this.copySprite(target2, target1);
}
this.blendFrames(target3, target1, target2, this.strength);
this.copyToGame(target2);
},
setColor: function (r, g, b)
{
this.color[0] = r;
this.color[1] = g;
this.color[2] = b;
return this;
}
});
module.exports = Bloom;

View file

@ -1,17 +0,0 @@
/**
* @author Richard Davey <rich@photonstorm.com>
* @copyright 2013-2023 Photon Storm Ltd.
* @license {@link https://opensource.org/licenses/MIT|MIT License}
*/
/**
* @namespace Phaser.Renderer.WebGL.Pipelines.FX
*/
var FX = {
Bloom: require('./Bloom')
};
module.exports = FX;

View file

@ -15,6 +15,7 @@ var Pipelines = {
BitmapMaskPipeline: require('./BitmapMaskPipeline'),
Events: require('./events'),
FXPipeline: require('./FXPipeline'),
LightPipeline: require('./LightPipeline'),
MobilePipeline: require('./MobilePipeline'),
MultiPipeline: require('./MultiPipeline'),

View file

@ -0,0 +1,70 @@
module.exports = [
'#define SHADER_NAME GLOW_FS',
'',
'precision mediump float;',
'',
'uniform sampler2D uMainSampler;',
'',
'varying vec2 outTexCoord;',
'',
'uniform float outerStrength;',
'uniform float innerStrength;',
'uniform vec2 resolution;',
'uniform vec4 glowColor;',
'uniform bool knockout;',
'',
'const float PI = 3.14159265358979323846264;',
'',
'const float DIST = __DIST__;',
'const float SIZE = min(__SIZE__, PI * 2.0);',
'const float NUM = ceil(PI * 2.0 / SIZE);',
'',
'const float MAX_TOTAL_ALPHA = NUM * DIST * (DIST + 1.0) / 2.0;',
'',
'void main ()',
'{',
' vec2 px = vec2(1.0 / resolution.x, 1.0 / resolution.y);',
'',
' float totalAlpha = 0.0;',
'',
' vec2 direction;',
' vec2 displaced;',
'',
' for (float angle = 0.0; angle < PI * 2.0; angle += SIZE)',
' {',
' direction = vec2(cos(angle), sin(angle)) * px;',
'',
' for (float curDistance = 0.0; curDistance < DIST; curDistance++)',
' {',
' displaced = outTexCoord + direction * (curDistance + 1.0);',
'',
' totalAlpha += (DIST - curDistance) * texture2D(uMainSampler, displaced).a;',
' }',
' }',
'',
' vec4 curColor = texture2D(uMainSampler, outTexCoord);',
'',
' float alphaRatio = (totalAlpha / MAX_TOTAL_ALPHA);',
'',
' float innerGlowAlpha = (1.0 - alphaRatio) * innerStrength * curColor.a;',
' float innerGlowStrength = min(1.0, innerGlowAlpha);',
'',
' vec4 innerColor = mix(curColor, glowColor, innerGlowStrength);',
'',
' float outerGlowAlpha = alphaRatio * outerStrength * (1. - curColor.a);',
' float outerGlowStrength = min(1.0 - innerColor.a, outerGlowAlpha);',
'',
' vec4 outerGlowColor = outerGlowStrength * glowColor.rgba;',
'',
' if (knockout)',
' {',
' float resultAlpha = outerGlowAlpha + innerGlowAlpha;',
' gl_FragColor = vec4(glowColor.rgb * resultAlpha, resultAlpha);',
' }',
' else',
' {',
' gl_FragColor = innerColor + outerGlowColor;',
' }',
'}',
''
].join('\n');

View file

@ -0,0 +1,35 @@
module.exports = [
'#define SHADER_NAME SHADOW_FS',
'',
'precision mediump float;',
'',
'uniform sampler2D uMainSampler;',
'uniform vec2 lightPosition;',
'uniform vec4 shadowColor;',
'uniform float decay;',
'uniform float power;',
'uniform float intensity;',
'',
'varying vec2 outTexCoord;',
'',
'const int SAMPLES = __SAMPLES__;',
'',
'void main ()',
'{',
' vec4 texture = texture2D(uMainSampler, outTexCoord);',
'',
' vec2 pc = (lightPosition - outTexCoord) * intensity;',
'',
' float shadow = 0.0;',
'',
' for (int i = 0; i < SAMPLES; ++i)',
' {',
' shadow += texture2D(uMainSampler, outTexCoord + float(i) * decay / float(SAMPLES) * pc).a * power;',
' }',
'',
' float mask = 1.0 - texture.a;',
'',
' gl_FragColor = mix(texture, shadowColor, shadow * mask);',
'}',
''
].join('\n');

View file

@ -0,0 +1,67 @@
#define SHADER_NAME GLOW_FS
precision mediump float;
uniform sampler2D uMainSampler;
varying vec2 outTexCoord;
uniform float outerStrength;
uniform float innerStrength;
uniform vec2 resolution;
uniform vec4 glowColor;
uniform bool knockout;
const float PI = 3.14159265358979323846264;
const float DIST = __DIST__;
const float SIZE = min(__SIZE__, PI * 2.0);
const float NUM = ceil(PI * 2.0 / SIZE);
const float MAX_TOTAL_ALPHA = NUM * DIST * (DIST + 1.0) / 2.0;
void main ()
{
vec2 px = vec2(1.0 / resolution.x, 1.0 / resolution.y);
float totalAlpha = 0.0;
vec2 direction;
vec2 displaced;
for (float angle = 0.0; angle < PI * 2.0; angle += SIZE)
{
direction = vec2(cos(angle), sin(angle)) * px;
for (float curDistance = 0.0; curDistance < DIST; curDistance++)
{
displaced = outTexCoord + direction * (curDistance + 1.0);
totalAlpha += (DIST - curDistance) * texture2D(uMainSampler, displaced).a;
}
}
vec4 curColor = texture2D(uMainSampler, outTexCoord);
float alphaRatio = (totalAlpha / MAX_TOTAL_ALPHA);
float innerGlowAlpha = (1.0 - alphaRatio) * innerStrength * curColor.a;
float innerGlowStrength = min(1.0, innerGlowAlpha);
vec4 innerColor = mix(curColor, glowColor, innerGlowStrength);
float outerGlowAlpha = alphaRatio * outerStrength * (1. - curColor.a);
float outerGlowStrength = min(1.0 - innerColor.a, outerGlowAlpha);
vec4 outerGlowColor = outerGlowStrength * glowColor.rgba;
if (knockout)
{
float resultAlpha = outerGlowAlpha + innerGlowAlpha;
gl_FragColor = vec4(glowColor.rgb * resultAlpha, resultAlpha);
}
else
{
gl_FragColor = innerColor + outerGlowColor;
}
}

View file

@ -0,0 +1,32 @@
#define SHADER_NAME SHADOW_FS
precision mediump float;
uniform sampler2D uMainSampler;
uniform vec2 lightPosition;
uniform vec4 shadowColor;
uniform float decay;
uniform float power;
uniform float intensity;
varying vec2 outTexCoord;
const int SAMPLES = __SAMPLES__;
void main ()
{
vec4 texture = texture2D(uMainSampler, outTexCoord);
vec2 pc = (lightPosition - outTexCoord) * intensity;
float shadow = 0.0;
for (int i = 0; i < SAMPLES; ++i)
{
shadow += texture2D(uMainSampler, outTexCoord + float(i) * decay / float(SAMPLES) * pc).a * power;
}
float mask = 1.0 - texture.a;
gl_FragColor = mix(texture, shadowColor, shadow * mask);
}