phaser
Version:
A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.
579 lines (545 loc) • 21 kB
JavaScript
/**
* @author Benjamin D. Richards <benjamindrichards@gmail.com>
* @copyright 2013-2026 Phaser Studio Inc.
* @license {@link https://opensource.org/licenses/MIT|MIT License}
*/
var Class = require('../../../utils/Class');
var Shader = require('../../shader/Shader');
var Color = require('../../../display/color/Color');
var NoiseWorley4DFrag = require('../../../renderer/webgl/shaders/NoiseWorley4D-frag');
/**
* @classdesc
* A NoiseCell4D Game Object.
*
* This game object is a quad which displays cellular noise.
* You can manipulate this object like any other, make it interactive,
* and use it in filters and masks to create visually stunning effects.
*
* Behind the scenes, a NoiseCell4D is a {@link Phaser.GameObjects.Shader}
* using a specific shader program.
*
* Cellular noise, also called Worley Noise or Voronoi Noise,
* consists of a pattern of cells. This is good for modeling natural phenomena
* like waves, clouds, or scales.
*
* You can set the color and transparency, cell count, variation,
* and seed value of the noise.
* You can change the detail level by increasing `noiseIterations`.
* You can change the noise mode to output sharp edges, soft edges,
* or flat colors for the cells.
*
* You can scroll the noise by animating the `noiseOffset` property.
*
* You can set `noiseNormalMap` to output a normal map.
* This is a quick way to add texture for lighting.
*
* The 4D version of NoiseCell has two extra dimensions: Z and W.
* The shader only renders the XY slice through the noise field.
* Because the centers of cells typically lie elsewhere in the hypervolume,
* cells appear with variation in brightness.
* You can scroll on the Z axis to shift the slice, smoothly changing the cell pattern.
* In 4D, you can instead move the ZW offset in a circle,
* creating a constantly changing pattern which repeats without reversing
* or resetting.
* This ZW circling technique is advised for long-term effects,
* because it avoids large offsets which can cause floating-point precision issues.
*
* @class NoiseCell4D
* @extends Phaser.GameObjects.Shader
* @memberof Phaser.GameObjects
* @since 4.0.0
* @constructor
*
* @param {Phaser.Scene} scene - The Scene to which this Game Object belongs.
* @param {Phaser.Types.GameObjects.NoiseCell4D.NoiseCell4DQuadConfig} [config] - The configuration for this Game Object.
* @param {number} [x=0] - The horizontal position of this Game Object in the world.
* @param {number} [y=0] - The vertical position of this Game Object in the world.
* @param {number} [width=128] - The width of the Game Object.
* @param {number} [height=128] - The height of the Game Object.
*/
var NoiseCell4D = new Class({
Extends: Shader,
initialize: function NoiseCell4D (scene, config, x, y, width, height)
{
if (!config) { config = {}; }
var shaderConfig = {
name: 'noiseCell4D',
fragmentSource: NoiseWorley4DFrag,
shaderAdditions: [
{
name: 'MODE_DISTANCE',
tags: [ 'MODE' ],
additions: {
fragmentMode: '#define MODE_DISTANCE'
}
},
{
name: 'ITERATION_COUNT_1',
tags: [ 'ITERATION_COUNT' ],
additions: {
fragmentIterations: '#define ITERATION_COUNT 1.0'
}
},
{
name: 'NORMALMAP',
tags: [ 'NORMALMAP' ],
additions: {
fragmentNormalMap: '#define NORMAL_MAP\n#extension GL_OES_standard_derivatives : enable'
},
disable: !config.noiseNormalMap
}
],
setupUniforms: this._setupUniforms,
updateShaderConfig: this._updateShaderConfig
};
Shader.call(this, scene, shaderConfig, x, y, width, height);
this.type = 'NoiseCell4D';
/**
* The number of cells in each dimension.
*
* This must be an array of 4 numbers.
*
* Try to keep the cell count between 2
* and about an eighth of the resolution of the texture.
* A cell count of 1 has no room to vary.
* A cell count greater than the resolution of the texture
* will essentially be expensive white noise.
*
* @name Phaser.GameObjects.NoiseCell4D#noiseCells
* @type {number[]}
* @default [ 32, 32, 32, 32 ]
* @since 4.0.0
*/
this.noiseCells = [ 32, 32, 32, 32 ];
if (config.noiseCells)
{
this.noiseCells = config.noiseCells;
}
/**
* How many cells wide the pattern is.
*
* By default, this is set to the same dimensions as `noiseCells`.
* This causes the output to wrap seamlessly at the edges.
* To restore wrapping if you changed settings, call `this.wrapNoise()`.
*
* A lower value causes the output to repeat.
*
* A higher value breaks visible wrapping.
* The cell pattern still repeats off-camera.
* Try to keep this value as low as possible,
* as it helps avoid floating-point precision errors.
*
* This must be an array of 4 numbers.
*
* @name Phaser.GameObjects.NoiseCell4D#noiseWrap
* @type {number[]}
* @default [ 32, 32, 32, 32 ]
* @since 4.0.0
*/
this.noiseWrap = [
this.noiseCells[0],
this.noiseCells[1],
this.noiseCells[2],
this.noiseCells[3]
];
if (config.noiseWrap)
{
this.noiseWrap = config.noiseWrap;
}
/**
* The offset of the noise in each dimension: [ x, y, z, w ].
* Animate x and y to scroll the noise pattern.
* Animate z and w to smoothly change the noise pattern.
*
* This must be an array of 4 numbers.
*
* Moving too far from 0 will introduce floating-point precision issues.
* This can cause the noise to appear blocky.
* We start to see obvious blockiness at offsets of a few thousand,
* so stay below that.
*
* You can evolve the noise pattern by scrolling the Z axis.
* However, this will eventually meet those floating-point precision issues.
*
* In four dimensions, you can instead evolve the noise pattern
* by moving along a circle in the ZW plane.
* This changes the pattern smoothly, without leaving the safe region,
* and without needing to stop and reverse or reset.
* 4D noise is very useful for continuously scrolling patterns.
*
* @example
* // Scroll the noise pattern without changing the pattern.
* noise.noiseOffset[0] = Math.sin(scene.time.now / 10000);
* noise.noiseOffset[1] = Math.cos(scene.time.now / 10000);
*
* // Evolve the noise pattern along a circle in the ZW plane.
* noise.noiseOffset[2] = Math.sin(scene.time.now / 1000) / 32;
* noise.noiseOffset[3] = Math.cos(scene.time.now / 1000) / 32;
*
* @name Phaser.GameObjects.NoiseCell4D#noiseOffset
* @type {number[]}
* @default [ 0, 0, 0, 0 ]
* @since 4.0.0
*/
this.noiseOffset = [ 0, 0, 0, 0 ];
if (config.noiseOffset)
{
this.noiseOffset = config.noiseOffset;
}
/**
* How much each cell can vary from its grid position.
* High values break further from the grid.
*
* At 0, cells are perfectly square.
* At 1, cells are fully chaotic.
* Never go higher than 1, as this can distort the cell matrix so much
* that the nearest cell is outside the sampling range,
* causing seams in the noise.
*
* @name Phaser.GameObjects.NoiseCell4D#noiseVariation
* @type {number[]}
* @default [ 1, 1, 1, 1 ]
* @since 4.0.0
*/
this.noiseVariation = [ 1, 1, 1, 1 ];
if (config.noiseVariation)
{
this.noiseVariation = config.noiseVariation;
}
/**
* How many octaves of noise to apply.
* This adds fine detail to the noise, at the cost of performance.
*
* Each octave of noise has twice the resolution,
* and contributes half as much to the output.
*
* This value should be an integer of 1 or higher.
* Values above 5 or so have increasingly little effect.
* Each iteration has a cost, so only use as much as you need!
*
* @name Phaser.GameObjects.NoiseCell4D#noiseIterations
* @type {number}
* @default 1
* @since 4.0.0
*/
this.noiseIterations = 1;
if (config.noiseIterations)
{
this.noiseIterations = config.noiseIterations;
}
/**
* What mode to output the noise in.
*
* - 0: Sharp boundaries between cells.
* - 1: Index mode. Cells have a single flat color.
* It is random and may not be unique.
* - 2: Smooth boundaries between cells.
* Use `noiseSmoothing` to control smoothness.
*
* @name Phaser.GameObjects.NoiseCell4D#noiseMode
* @type {number}
* @default 0
* @since 4.0.0
*/
this.noiseMode = 0;
if (config.noiseMode)
{
this.noiseMode = config.noiseMode;
}
/**
* How much smoothing to apply in smoothing mode.
*
* The default value is 1, which applies moderate smoothing between cells.
* The value is a factor.
* Values from 0-1 reduce the smoothing.
* Values above 1 intensify the smoothing.
* Intensification slows above 4 or so.
*
* @name Phaser.GameObjects.NoiseCell4D#noiseSmoothing
* @type {number}
* @default 1
* @since 4.0.0
*/
this.noiseSmoothing = 1;
if (config.noiseSmoothing)
{
this.noiseSmoothing = config.noiseSmoothing;
}
/**
* Whether to convert the noise output to a normal map.
*
* This works properly with noise modes 0 and 2, which form curves.
*
* Control the curvature strength with `noiseNormalScale`.
*
* @name Phaser.GameObjects.NoiseCell4D#noiseNormalMap
* @type {boolean}
* @default false
* @since 4.0.0
*/
this.noiseNormalMap = !!config.noiseNormalMap;
/**
* Curvature strength of normal map output.
* This is used when `noiseNormalMap` is enabled.
*
* The default is 1. Higher values produce more curvature;
* lower values are flatter.
*
* Surface angle is determined by the rate of change of the noise.
* Noise with more iterations tends to change more rapidly,
* thus have more pronounced normals.
*
* @name Phaser.GameObjects.NoiseCell4D#noiseNormalScale
* @type {number}
* @default 1
* @since 4.0.0
*/
this.noiseNormalScale = 1;
if (config.noiseNormalScale !== undefined)
{
this.noiseNormalScale = config.noiseNormalScale;
}
/**
* The color of the middle of the cells.
*
* The default is black. You can set any color, and change the alpha.
*
* @name Phaser.GameObjects.NoiseCell4D#noiseColorStart
* @type {Phaser.Display.Color}
* @since 4.0.0
*/
this.noiseColorStart = new Color(0, 0, 0);
/**
* The color of the edge of the cells.
*
* The default is white. You can set any color, and change the alpha.
*
* @name Phaser.GameObjects.NoiseCell4D#noiseColorEnd
* @type {Phaser.Display.Color}
* @since 4.0.0
*/
this.noiseColorEnd = new Color(255, 255, 255);
if (config.noiseColorStart !== undefined || config.noiseColorEnd !== undefined)
{
this.setNoiseColor(config.noiseColorStart, config.noiseColorEnd);
}
/**
* Seed values for the noise.
* Vary these to change the shape of cells in the pattern.
* A different seed creates a completely different pattern.
*
* This must be an array of 16 numbers.
*
* Noise seed values should be fairly small.
* Numbers between 0 and 1, or 0 and 16, are recommended.
* Very large seed values may lose floating-point precision
* and cause the noise to appear blocky.
*
* @name Phaser.GameObjects.NoiseCell4D#noiseSeed
* @type {number[]}
* @default [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ]
* @since 4.0.0
*/
this.noiseSeed = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ];
if (config.noiseSeed) { this.noiseSeed = config.noiseSeed; }
if (config.randomizeNoiseSeed)
{
this.randomizeNoiseSeed();
}
/**
* Whether to jitter shader inputs to force continuous high precision.
* This is an advanced setting.
*
* Chromium browsers seem to switch between WebGL rendering modes,
* which changes the precision available to the noise shader.
* This can change the noise calculation, causing parts of the output
* to flicker unpredictably.
* The `keepAwake` setting adds an imperceptible amount to the offset
* during rendering, which seems to force Chromium to be consistent
* and eliminate the flickering.
*
* Don't disable this unless you know what you're doing.
* It prevents an unpredictable problem that might not appear in your
* browser or device, but will appear for other users.
*
* @name Phaser.GameObjects.NoiseCell4D#keepAwake
* @type {boolean}
* @default true
* @since 4.0.0
*/
this.keepAwake = true;
},
/**
* Set the colors of the noise, from a variety of color formats.
*
* - A number is expected to be a 24 or 32 bit RGB or ARGB value.
* - A string is expected to be a hex code.
* - An array of numbers is expected to be RGB or RGBA in the range 0-1.
* - A Color object can be used.
*
* @method Phaser.GameObjects.NoiseCell4D#setNoiseColor
* @since 4.0.0
* @param {number | string | number[] | Phaser.Display.Color} [start=0x000000] - The color in the middle of the cells.
* @param {number | string | number[] | Phaser.Display.Color} [end=0xffffff] - The color at the edge of the cells.
* @return {this} This game object.
*/
setNoiseColor: function (start, end)
{
var alpha;
if (start === undefined)
{
start = 0x000000;
}
if (end === undefined)
{
end = 0xffffff;
}
if (typeof start === 'number')
{
Color.IntegerToColor(start, this.noiseColorStart);
}
else if (typeof start === 'string')
{
Color.HexStringToColor(start, this.noiseColorStart);
}
else if (Array.isArray(start))
{
alpha = (start[3] === undefined) ? 1 : start[3];
this.noiseColorStart.setGLTo(start[0], start[1], start[2], alpha);
}
else if (start instanceof Color)
{
this.noiseColorStart.setTo(start.red, start.green, start.blue, start.alpha);
}
if (typeof end === 'number')
{
Color.IntegerToColor(end, this.noiseColorEnd);
}
else if (typeof end === 'string')
{
Color.HexStringToColor(end, this.noiseColorEnd);
}
else if (Array.isArray(end))
{
alpha = (end[3] === undefined) ? 1 : end[3];
this.noiseColorEnd.setGLTo(end[0], end[1], end[2], alpha);
}
else if (end instanceof Color)
{
this.noiseColorEnd.setTo(end.red, end.green, end.blue, end.alpha);
}
return this;
},
/**
* Randomize the noise seed, creating a unique pattern.
*
* @method Phaser.GameObjects.NoiseCell4D#randomizeNoiseSeed
* @since 4.0.0
* @return {this} This game object.
*/
randomizeNoiseSeed: function ()
{
var len = this.noiseSeed.length;
for (var i = 0; i < len; i++)
{
this.noiseSeed[i] = Math.random();
}
return this;
},
/**
* Set the noise texture to wrap seamlessly.
*
* This sets `noiseWrap` to equal `noiseCells` in all dimensions.
*
* @method Phaser.GameObjects.NoiseCell4D#wrapNoise
* @since 4.0.0
* @return {this} This game object.
*/
wrapNoise: function ()
{
var len = this.noiseWrap.length;
for (var i = 0; i < len; i++)
{
this.noiseWrap[i] = this.noiseCells[i];
}
return this;
},
/**
* The function which sets uniforms for the shader.
* This is provided to the Shader base class as `setupUniforms`.
* You should not override `setupUniforms` on this object.
*
* @method Phaser.GameObjects.NoiseCell4D#_setupUniforms
* @private
* @since 4.0.0
* @param {function} setUniform - The function which sets uniforms. `(name: string, value: any) => void`.
* @param {Phaser.Renderer.WebGL.DrawingContext} drawingContext - A reference to the current drawing context.
*/
_setupUniforms: function (setUniform)
{
if (this.keepAwake)
{
var wakeValue = Math.sin(this.scene.time.now) / 4096 / 256;
setUniform('uCellOffset', [
this.noiseOffset[0] + wakeValue,
this.noiseOffset[1] + wakeValue,
this.noiseOffset[2] + wakeValue,
this.noiseOffset[3] + wakeValue
]);
}
else
{
setUniform('uCellOffset', this.noiseOffset);
}
setUniform('uSeedX', this.noiseSeed.slice(0, 4));
setUniform('uSeedY', this.noiseSeed.slice(4, 8));
setUniform('uSeedZ', this.noiseSeed.slice(8, 12));
setUniform('uSeedW', this.noiseSeed.slice(12, 16));
setUniform('uCells', this.noiseCells);
setUniform('uVariation', this.noiseVariation);
setUniform('uWrap', this.noiseWrap);
setUniform('uSmoothing', this.noiseSmoothing);
setUniform('uNormalScale', this.noiseNormalScale);
setUniform('uColorStart', this.noiseColorStart.gl);
setUniform('uColorEnd', this.noiseColorEnd.gl);
},
/**
* The function which updates shader configuration.
* This is provided to the Shader base class as `updateShaderConfig`.
* You should not override `updateShaderConfig` on this object.
*
* @method Phaser.GameObjects.NoiseCell4D#_updateShaderConfig
* @private
* @since 4.0.0
* @param {Phaser.Renderer.WebGL.DrawingContext} drawingContext - A reference to the current drawing context.
* @param {Phaser.GameObjects.Gradient} gameObject - The game object which is rendering.
* @param {Phaser.Renderer.WebGL.RenderNodes.ShaderQuad} renderNode - The render node currently rendering.
*/
_updateShaderConfig: function (drawingContext, gameObject, renderNode)
{
var iterations = Math.max(1, Math.floor(gameObject.noiseIterations));
var iterationAdd = renderNode.programManager.getAdditionsByTag('ITERATION_COUNT')[0];
iterationAdd.name = 'ITERATION_COUNT_' + iterations;
iterationAdd.additions.fragmentIterations = '#define ITERATION_COUNT ' + iterations + '.0';
var mode = 'MODE_DISTANCE';
switch (gameObject.noiseMode)
{
case 1:
{
mode = 'MODE_INDEX';
break;
}
case 2:
{
mode = 'MODE_DISTANCE_SMOOTH';
break;
}
}
var modeAdd = renderNode.programManager.getAdditionsByTag('MODE')[0];
modeAdd.name = mode;
modeAdd.additions.fragmentMode = '#define ' + mode;
var normalAdd = renderNode.programManager.getAdditionsByTag('NORMALMAP')[0];
normalAdd.disable = !gameObject.noiseNormalMap;
}
});
module.exports = NoiseCell4D;