phaser/src/renderer/webgl/pipelines/LightPipeline.js
2020-10-13 18:17:30 +01:00

667 lines
21 KiB
JavaScript

/**
* @author Richard Davey <rich@photonstorm.com>
* @author Felipe Alfonso <@bitnenfer>
* @copyright 2020 Photon Storm Ltd.
* @license {@link https://opensource.org/licenses/MIT|MIT License}
*/
var Class = require('../../../utils/Class');
var GetFastValue = require('../../../utils/object/GetFastValue');
var ModelViewProjection = require('./components/ModelViewProjection');
var PointLightShaderSourceFS = require('../shaders/PointLight-frag.js');
var PointLightShaderSourceVS = require('../shaders/PointLight-vert.js');
var ProjectOrtho = require('../mvp/ProjectOrtho');
var TransformMatrix = require('../../../gameobjects/components/TransformMatrix');
var WebGLPipeline = require('../WebGLPipeline');
var LIGHT_COUNT = 10;
/**
* @classdesc
*
* The Light Pipeline is an extension of the Multi Pipeline and uses a custom shader
* designed to handle forward diffused rendering of 2D lights in a Scene.
*
* The shader works in tandem with Light Game Objects, and optionally texture normal maps,
* to provide an ambient illumination effect.
*
* If you wish to provide your own shader, you can use the `%LIGHT_COUNT%` declaration in the source,
* and it will be automatically replaced at run-time with the total number of configured lights.
*
* The maximum number of lights can be set in the Render Config `maxLights` property and defaults to 10.
*
* Prior to Phaser v3.50 this pipeline was called the `ForwardDiffuseLightPipeline`.
*
* The fragment shader it uses can be found in `shaders/src/Light.frag`.
* The vertex shader it uses can be found in `shaders/src/Multi.vert`.
*
* The default shader attributes for this pipeline are:
*
* `inPosition` (vec2, offset 0)
* `inTexCoord` (vec2, offset 8)
* `inTexId` (float, offset 16)
* `inTintEffect` (float, offset 20)
* `inTint` (vec4, offset 24, normalized)
*
* The default shader uniforms for this pipeline are:
*
* `uProjectionMatrix` (mat4)
* `uViewMatrix` (mat4)
* `uModelMatrix` (mat4)
* `uMainSampler` (sampler2D)
* `uNormSampler` (sampler2D)
* `uCamera` (vec4)
* `uResolution` (vec2)
* `uAmbientLightColor` (vec3)
* `uInverseRotationMatrix` (mat3)
* `uLights` (Light struct)
*
* @class LightPipeline
* @extends Phaser.Renderer.WebGL.Pipelines.MultiPipeline
* @memberof Phaser.Renderer.WebGL.Pipelines
* @constructor
* @since 3.50.0
*
* @param {Phaser.Types.Renderer.WebGL.WebGLPipelineConfig} config - The configuration options for this pipeline.
*/
var LightPipeline = new Class({
Extends: WebGLPipeline,
Mixins: [
ModelViewProjection
],
initialize:
function LightPipeline (config)
{
var gl = config.game.renderer.gl;
LIGHT_COUNT = config.game.renderer.config.maxLights;
// var fragmentShaderSource = GetFastValue(config, 'fragShader', ShaderSourceFS);
// config.fragShader = fragmentShaderSource.replace('%LIGHT_COUNT%', LIGHT_COUNT.toString());
config.fragShader = GetFastValue(config, 'fragShader', PointLightShaderSourceFS);
config.vertShader = GetFastValue(config, 'vertShader', PointLightShaderSourceVS);
config.vertexSize = GetFastValue(config, 'vertexSize', 36);
config.attributes = GetFastValue(config, 'attributes', [
{
name: 'inPosition',
size: 2,
type: gl.FLOAT,
normalized: false,
offset: 0,
enabled: false,
location: -1
},
{
name: 'inLightPosition',
size: 2,
type: gl.FLOAT,
normalized: false,
offset: 8,
enabled: false,
location: -1
},
{
name: 'inLightRadius',
size: 1,
type: gl.FLOAT,
normalized: false,
offset: 16,
enabled: false,
location: -1
},
{
name: 'inLightColor',
size: 4,
type: gl.FLOAT,
normalized: false,
offset: 20,
enabled: false,
location: -1
}
]);
config.uniforms = GetFastValue(config, 'uniforms', [
'uProjectionMatrix',
'uViewMatrix',
'uModelMatrix'
]);
WebGLPipeline.call(this, config);
/**
* Float32 view of the array buffer containing the pipeline's vertices.
*
* @name Phaser.Renderer.WebGL.Pipelines.MultiPipeline#vertexViewF32
* @type {Float32Array}
* @since 3.0.0
*/
this.vertexViewF32 = new Float32Array(this.vertexData);
/**
* A temporary Transform Matrix, re-used internally during batching.
*
* @name Phaser.Renderer.WebGL.Pipelines.MultiPipeline#_tempMatrix1
* @private
* @type {Phaser.GameObjects.Components.TransformMatrix}
* @since 3.11.0
*/
this._tempMatrix1 = new TransformMatrix();
/**
* A temporary Transform Matrix, re-used internally during batching.
*
* @name Phaser.Renderer.WebGL.Pipelines.MultiPipeline#_tempMatrix2
* @private
* @type {Phaser.GameObjects.Components.TransformMatrix}
* @since 3.11.0
*/
this._tempMatrix2 = new TransformMatrix();
/**
* A temporary Transform Matrix, re-used internally during batching.
*
* @name Phaser.Renderer.WebGL.Pipelines.MultiPipeline#_tempMatrix3
* @private
* @type {Phaser.GameObjects.Components.TransformMatrix}
* @since 3.11.0
*/
this._tempMatrix3 = new TransformMatrix();
/**
* Inverse rotation matrix for normal map rotations.
*
* @name Phaser.Renderer.WebGL.Pipelines.LightPipeline#inverseRotationMatrix
* @type {Float32Array}
* @private
* @since 3.16.0
*/
this.inverseRotationMatrix = new Float32Array([
1, 0, 0,
0, 1, 0,
0, 0, 1
]);
/**
* Stores a default normal map, which is an object with a `glTexture` property that
* maps to a 1x1 texture of the color #7f7fff created in the `boot` method.
*
* @name Phaser.Renderer.WebGL.Pipelines.LightPipeline#defaultNormalMap
* @type {object}
* @since 3.50.0
*/
this.defaultNormalMap;
/**
* Stores the previous number of lights rendered.
*
* @name Phaser.Renderer.WebGL.Pipelines.LightPipeline#lightCount
* @type {number}
* @since 3.50.0
*/
this.lightCount = 0;
this.forceZero = true;
this.mvpInit();
},
/**
* Called when the Game has fully booted and the Renderer has finished setting up.
*
* By this stage all Game level systems are now in place and you can perform any final
* tasks that the pipeline may need that relied on game systems such as the Texture Manager.
*
* @method Phaser.Renderer.WebGL.LightPipeline#boot
* @since 3.11.0
*/
boot: function ()
{
WebGLPipeline.prototype.boot.call(this);
var gl = this.gl;
var tempTexture = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, tempTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([ 127, 127, 255, 255 ]));
this.defaultNormalMap = { glTexture: tempTexture };
return this;
},
batchLight: function (light, camera, x0, y0, x1, y1, x2, y2, x3, y3, lightX, lightY)
{
var color = light.color;
var intensity = light.intensity;
var radius = light.radius;
var r = color.r * intensity;
var g = color.g * intensity;
var b = color.b * intensity;
var a = camera.alpha * light.alpha;
if (this.shouldFlush(6))
{
this.flush();
}
var vertexViewF32 = this.vertexViewF32;
var vertexOffset = (this.vertexCount * this.vertexComponentCount) - 1;
vertexViewF32[++vertexOffset] = x0;
vertexViewF32[++vertexOffset] = y0;
vertexViewF32[++vertexOffset] = lightX;
vertexViewF32[++vertexOffset] = lightY;
vertexViewF32[++vertexOffset] = radius;
vertexViewF32[++vertexOffset] = r;
vertexViewF32[++vertexOffset] = g;
vertexViewF32[++vertexOffset] = b;
vertexViewF32[++vertexOffset] = a;
vertexViewF32[++vertexOffset] = x1;
vertexViewF32[++vertexOffset] = y1;
vertexViewF32[++vertexOffset] = lightX;
vertexViewF32[++vertexOffset] = lightY;
vertexViewF32[++vertexOffset] = radius;
vertexViewF32[++vertexOffset] = r;
vertexViewF32[++vertexOffset] = g;
vertexViewF32[++vertexOffset] = b;
vertexViewF32[++vertexOffset] = a;
vertexViewF32[++vertexOffset] = x2;
vertexViewF32[++vertexOffset] = y2;
vertexViewF32[++vertexOffset] = lightX;
vertexViewF32[++vertexOffset] = lightY;
vertexViewF32[++vertexOffset] = radius;
vertexViewF32[++vertexOffset] = r;
vertexViewF32[++vertexOffset] = g;
vertexViewF32[++vertexOffset] = b;
vertexViewF32[++vertexOffset] = a;
vertexViewF32[++vertexOffset] = x0;
vertexViewF32[++vertexOffset] = y0;
vertexViewF32[++vertexOffset] = lightX;
vertexViewF32[++vertexOffset] = lightY;
vertexViewF32[++vertexOffset] = radius;
vertexViewF32[++vertexOffset] = r;
vertexViewF32[++vertexOffset] = g;
vertexViewF32[++vertexOffset] = b;
vertexViewF32[++vertexOffset] = a;
vertexViewF32[++vertexOffset] = x2;
vertexViewF32[++vertexOffset] = y2;
vertexViewF32[++vertexOffset] = lightX;
vertexViewF32[++vertexOffset] = lightY;
vertexViewF32[++vertexOffset] = radius;
vertexViewF32[++vertexOffset] = r;
vertexViewF32[++vertexOffset] = g;
vertexViewF32[++vertexOffset] = b;
vertexViewF32[++vertexOffset] = a;
vertexViewF32[++vertexOffset] = x3;
vertexViewF32[++vertexOffset] = y3;
vertexViewF32[++vertexOffset] = lightX;
vertexViewF32[++vertexOffset] = lightY;
vertexViewF32[++vertexOffset] = radius;
vertexViewF32[++vertexOffset] = r;
vertexViewF32[++vertexOffset] = g;
vertexViewF32[++vertexOffset] = b;
vertexViewF32[++vertexOffset] = a;
this.vertexCount += 6;
},
/**
* Called every time a Game Object needs to use this pipeline.
*
* @method Phaser.Renderer.WebGL.Pipelines.MultiPipeline#onBind
* @since 3.0.0
*
* @param {Phaser.GameObjects.GameObject} [gameObject] - The Game Object that invoked this pipeline, if any.
*
* @return {this} This WebGLPipeline instance.
*/
onBind: function ()
{
this.mvpUpdate();
return this;
},
/**
* Uploads the vertex data and emits a draw call for the current batch of vertices.
*
* @method Phaser.Renderer.WebGL.Pipelines.MultiPipeline#flush
* @since 3.0.0
*
* @return {this} This WebGLPipeline instance.
*/
flush: function ()
{
var gl = this.gl;
var vertexCount = this.vertexCount;
var vertexSize = this.vertexSize;
if (vertexCount > 0)
{
if (vertexCount === this.vertexCapacity)
{
gl.bufferData(gl.ARRAY_BUFFER, this.vertexData, gl.DYNAMIC_DRAW);
}
else
{
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.bytes.subarray(0, vertexCount * vertexSize));
}
gl.drawArrays(this.topology, 0, vertexCount);
this.vertexCount = 0;
}
return this;
},
/**
* Called every time the pipeline is bound by the renderer.
* Sets the shader program, vertex buffer and other resources.
* Should only be called when changing pipeline.
*
* @method Phaser.Renderer.WebGL.Pipelines.LightPipeline#bind
* @since 3.50.0
*
* @param {boolean} [reset=false] - Should the pipeline be fully re-bound after a renderer pipeline clear?
*
* @return {this} This WebGLPipeline instance.
bind: function (reset)
{
if (reset === undefined) { reset = false; }
WebGLPipeline.prototype.bind.call(this, reset);
var renderer = this.renderer;
var program = this.program;
renderer.setInt1(program, 'uMainSampler', 0);
renderer.setInt1(program, 'uNormSampler', 1);
renderer.setFloat2(program, 'uResolution', this.width, this.height);
return this;
},
*/
/**
* Resizes this pipeline and updates the projection.
*
* @method Phaser.Renderer.WebGL.Pipelines.MultiPipeline#resize
* @since 3.0.0
*
* @param {number} width - The new width.
* @param {number} height - The new height.
*
* @return {this} This WebGLPipeline instance.
*/
resize: function (width, height)
{
WebGLPipeline.prototype.resize.call(this, width, height);
ProjectOrtho(this, 0, this.width, this.height, 0, -1000, 1000);
return this;
},
/**
* This function sets all the needed resources for each camera pass.
*
* @method Phaser.Renderer.WebGL.Pipelines.LightPipeline#onRender
* @since 3.0.0
*
* @param {Phaser.Scene} scene - The Scene being rendered.
* @param {Phaser.Cameras.Scene2D.Camera} camera - The Scene Camera being rendered with.
*
* @return {this} This WebGLPipeline instance.
onRender: function (scene, camera)
{
this.active = false;
var lightManager = scene.sys.lights;
if (!lightManager || lightManager.lights.length <= 0 || !lightManager.active)
{
// Passthru
return this;
}
var lights = lightManager.cull(camera);
var lightCount = Math.min(lights.length, LIGHT_COUNT);
if (lightCount === 0)
{
return this;
}
this.active = true;
var renderer = this.renderer;
var program = this.program;
var cameraMatrix = camera.matrix;
var point = {x: 0, y: 0};
var height = renderer.height;
var i;
if (lightCount !== this.lightCount)
{
for (i = 0; i < LIGHT_COUNT; i++)
{
// Reset lights
renderer.setFloat1(program, 'uLights[' + i + '].radius', 0);
}
this.lightCount = lightCount;
}
if (camera.dirty)
{
renderer.setFloat4(program, 'uCamera', camera.x, camera.y, camera.rotation, camera.zoom);
}
// TODO - Only if dirty! and cache the location
renderer.setFloat3(program, 'uAmbientLightColor', lightManager.ambientColor.r, lightManager.ambientColor.g, lightManager.ambientColor.b);
for (i = 0; i < lightCount; i++)
{
var light = lights[i];
var lightName = 'uLights[' + i + '].';
cameraMatrix.transformPoint(light.x, light.y, point);
// TODO - Cache the uniform locations!!!
renderer.setFloat2(program, lightName + 'position', point.x - (camera.scrollX * light.scrollFactorX * camera.zoom), height - (point.y - (camera.scrollY * light.scrollFactorY) * camera.zoom));
if (light.dirty)
{
renderer.setFloat3(program, lightName + 'color', light.r, light.g, light.b);
renderer.setFloat1(program, lightName + 'intensity', light.intensity);
renderer.setFloat1(program, lightName + 'radius', light.radius);
light.dirty = false;
}
}
this.currentNormalMapRotation = null;
return this;
},
*/
/**
* Rotates the normal map vectors inversely by the given angle.
* Only works in 2D space.
*
* @method Phaser.Renderer.WebGL.Pipelines.LightPipeline#setNormalMapRotation
* @since 3.16.0
*
* @param {number} rotation - The angle of rotation in radians.
*/
setNormalMapRotation: function (rotation)
{
if (rotation !== this.currentNormalMapRotation || this.vertexCount === 0)
{
if (this.vertexCount > 0)
{
this.flush();
}
var inverseRotationMatrix = this.inverseRotationMatrix;
if (rotation)
{
var rot = -rotation;
var c = Math.cos(rot);
var s = Math.sin(rot);
inverseRotationMatrix[1] = s;
inverseRotationMatrix[3] = -s;
inverseRotationMatrix[0] = inverseRotationMatrix[4] = c;
}
else
{
inverseRotationMatrix[0] = inverseRotationMatrix[4] = 1;
inverseRotationMatrix[1] = inverseRotationMatrix[3] = 0;
}
this.renderer.setMatrix3(this.program, 'uInverseRotationMatrix', false, inverseRotationMatrix);
this.currentNormalMapRotation = rotation;
}
},
/**
* Assigns a texture to the current batch. If a different texture is already set it creates a new batch object.
*
* @method Phaser.Renderer.WebGL.Pipelines.LightPipeline#setTexture2D
* @since 3.50.0
*
* @param {WebGLTexture} [texture] - WebGLTexture that will be assigned to the current batch. If not given uses blankTexture.
* @param {Phaser.GameObjects.GameObject} [gameObject] - The Game Object being rendered or added to the batch.
setTexture2D: function (texture, gameObject)
{
var renderer = this.renderer;
if (texture === undefined) { texture = renderer.tempTextures[0]; }
var normalTexture = this.getNormalMap(gameObject);
if (renderer.isNewNormalMap())
{
this.flush();
renderer.setTextureZero(texture);
renderer.setNormalMap(normalTexture);
}
var rotation = (gameObject) ? gameObject.rotation : 0;
this.setNormalMapRotation(rotation);
this.currentUnit = 0;
return 0;
},
*/
/**
* Custom pipelines can use this method in order to perform any required pre-batch tasks
* for the given Game Object. It must return the texture unit the Game Object was assigned.
*
* @method Phaser.Renderer.WebGL.Pipelines.LightPipeline#setGameObject
* @since 3.50.0
*
* @param {Phaser.GameObjects.GameObject} gameObject - The Game Object being rendered or added to the batch.
* @param {Phaser.Textures.Frame} [frame] - Optional frame to use. Can override that of the Game Object.
*
* @return {number} The texture unit the Game Object has been assigned.
setGameObject: function (gameObject, frame)
{
if (frame === undefined) { frame = gameObject.frame; }
var renderer = this.renderer;
var texture = frame.glTexture;
var normalTexture = this.getNormalMap(gameObject);
if (renderer.isNewNormalMap())
{
this.flush();
renderer.setTextureZero(texture);
renderer.setNormalMap(normalTexture);
}
this.setNormalMapRotation(gameObject.rotation);
this.currentUnit = 0;
return 0;
},
*/
/**
* Returns the normal map WebGLTexture from the given Game Object.
* If the Game Object doesn't have one, it returns the default normal map from this pipeline instead.
*
* @method Phaser.Renderer.WebGL.Pipelines.LightPipeline#getNormalMap
* @since 3.50.0
*
* @param {Phaser.GameObjects.GameObject} [gameObject] - The Game Object to get the normal map from.
*
* @return {WebGLTexture} The normal map texture.
*/
getNormalMap: function (gameObject)
{
var normalTexture;
if (!gameObject)
{
normalTexture = this.defaultNormalMap;
}
else if (gameObject.displayTexture)
{
normalTexture = gameObject.displayTexture.dataSource[gameObject.displayFrame.sourceIndex];
}
else if (gameObject.texture)
{
normalTexture = gameObject.texture.dataSource[gameObject.frame.sourceIndex];
}
else if (gameObject.tileset)
{
if (Array.isArray(gameObject.tileset))
{
normalTexture = gameObject.tileset[0].image.dataSource[0];
}
else
{
normalTexture = gameObject.tileset.image.dataSource[0];
}
}
if (!normalTexture)
{
normalTexture = this.defaultNormalMap;
}
return normalTexture.glTexture;
}
});
LightPipeline.LIGHT_COUNT = LIGHT_COUNT;
module.exports = LightPipeline;