phaser
Version:
A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.
647 lines (589 loc) • 21.6 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 TransformMatrix = require('../../../../gameobjects/components/TransformMatrix');
var Vector2 = require('../../../../math/Vector2');
var Class = require('../../../../utils/Class');
var Merge = require('../../../../utils/object/Merge');
var ProgramManager = require('../../ProgramManager');
var MakeApplyLighting = require('../../shaders/additionMakers/MakeApplyLighting');
var MakeApplyTint = require('../../shaders/additionMakers/MakeApplyTint');
var MakeDefineLights = require('../../shaders/additionMakers/MakeDefineLights');
var MakeDefineTexCount = require('../../shaders/additionMakers/MakeDefineTexCount');
var MakeGetNormalFromMap = require('../../shaders/additionMakers/MakeGetNormalFromMap');
var MakeGetTexCoordOut = require('../../shaders/additionMakers/MakeGetTexCoordOut');
var MakeGetTexRes = require('../../shaders/additionMakers/MakeGetTexRes');
var MakeGetTexture = require('../../shaders/additionMakers/MakeGetTexture');
var MakeOutInverseRotation = require('../../shaders/additionMakers/MakeOutInverseRotation');
var MakeSmoothPixelArt = require('../../shaders/additionMakers/MakeSmoothPixelArt');
var Utils = require('../../Utils');
var WebGLVertexBufferLayoutWrapper = require('../../wrappers/WebGLVertexBufferLayoutWrapper');
var RenderNode = require('../RenderNode');
var ShaderSourceFS = require('../../shaders/SpriteGPULayer-frag');
var ShaderSourceVS = require('../../shaders/SpriteGPULayer-vert');
/**
* @classdesc
* This RenderNode handles rendering of a single SpriteGPULayer object.
* A new instance of the RenderNode should be created for each SpriteGPULayer object,
* as it stores the shader program and vertex buffer data for the object.
*
* It is a Stand-Alone Render Node, meaning that it does not batch with other objects.
* It is best suited to rendering highly complex GPU-driven sprite layers.
*
* @class SubmitterSpriteGPULayer
* @extends Phaser.Renderer.WebGL.RenderNodes.RenderNode
* @memberof Phaser.Renderer.WebGL.RenderNodes
* @constructor
* @since 4.0.0
* @param {Phaser.Renderer.WebGL.RenderNodes.RenderNodeManager} manager - The manager that owns this RenderNode.
* @param {Phaser.Types.Renderer.WebGL.RenderNodes.SubmitterSpriteGPULayerConfig} config - The configuration object for this handler.
* @param {Phaser.GameObjects.SpriteGPULayer} gameObject - The SpriteGPULayer object to render.
*/
var SubmitterSpriteGPULayer = new Class({
Extends: RenderNode,
initialize: function SubmitterSpriteGPULayer (manager, config, gameObject)
{
var renderer = manager.renderer;
var finalConfig = Merge(config || {}, this.defaultConfig);
var name = finalConfig.name;
this._completeLayout(finalConfig);
RenderNode.call(this, name, manager);
/**
* The completed configuration object for this RenderNode.
* This is defined by the default configuration and the user-defined configuration object.
*
* @name Phaser.Renderer.WebGL.RenderNodes.SubmitterSpriteGPULayer#config
* @type {object}
* @since 4.0.0
*/
this.config = finalConfig;
/**
* The SpriteGPULayer GameObject this RenderNode is rendering.
*
* @name Phaser.Renderer.WebGL.RenderNodes.SubmitterSpriteGPULayer#gameObject
* @type {Phaser.GameObjects.SpriteGPULayer}
* @since 4.0.0
* @readonly
*/
this.gameObject = gameObject;
/**
* The instance buffer layout for this RenderNode.
*
* @name Phaser.Renderer.WebGL.RenderNodes.SubmitterSpriteGPULayer#instanceBufferLayout
* @type {Phaser.Renderer.WebGL.Wrappers.WebGLVertexBufferLayoutWrapper}
* @since 4.0.0
* @readonly
*/
this.instanceBufferLayout = new WebGLVertexBufferLayoutWrapper(
renderer,
finalConfig.instanceBufferLayout,
null
);
/**
* The vertex buffer layout for this RenderNode.
*
* This consists of 4 bytes, 0-3, forming corners of a quad instance.
*
* @name Phaser.Renderer.WebGL.RenderNodes.SubmitterSpriteGPULayer#vertexBufferLayout
* @type {Phaser.Renderer.WebGL.Wrappers.WebGLVertexBufferLayoutWrapper}
* @since 4.0.0
* @readonly
*/
this.vertexBufferLayout = new WebGLVertexBufferLayoutWrapper(
renderer,
finalConfig.vertexBufferLayout,
null
);
// Set up vertex buffer.
var vertexBuffer = this.vertexBufferLayout.buffer;
var vertexBufferViewU8 = vertexBuffer.viewU8;
vertexBufferViewU8[0] = 0;
vertexBufferViewU8[1] = 1;
vertexBufferViewU8[2] = 2;
vertexBufferViewU8[3] = 3;
vertexBuffer.update();
/**
* The program manager used to create and manage shader programs.
* This contains shader variants.
*
* @name Phaser.Renderer.WebGL.RenderNodes.SubmitterSpriteGPULayer#programManager
* @type {Phaser.Renderer.WebGL.ProgramManager}
* @since 4.0.0
*/
this.programManager = new ProgramManager(
renderer,
[ this.vertexBufferLayout, this.instanceBufferLayout ],
this.indexBuffer
);
// Fill in program configuration from config.
this.programManager.setBaseShader(
finalConfig.shaderName,
finalConfig.vertexSource,
finalConfig.fragmentSource
);
if (finalConfig.shaderAdditions)
{
for (var i = 0; i < finalConfig.shaderAdditions.length; i++)
{
var addition = finalConfig.shaderAdditions[i];
this.programManager.addAddition(addition);
}
}
if (finalConfig.shaderFeatures)
{
for (i = 0; i < finalConfig.shaderFeatures.length; i++)
{
this.programManager.addFeature(finalConfig.shaderFeatures[i]);
}
}
/**
* A matrix used for temporary calculations.
*
* @name Phaser.Renderer.WebGL.RenderNodes.SubmitterSpriteGPULayer#_calcMatrix
* @type {Phaser.GameObjects.Components.TransformMatrix}
* @since 4.0.0
*/
this._calcMatrix = new TransformMatrix();
/**
* A vector used for temporary calculations.
*
* @name Phaser.Renderer.WebGL.RenderNodes.SubmitterSpriteGPULayer#_lightVector
* @type {Phaser.Math.Vector2}
* @since 4.0.0
* @private
*/
this._lightVector = new Vector2();
},
/**
* Default configuration of this RenderNode.
*
* @name Phaser.Renderer.WebGL.RenderNodes.SubmitterSpriteGPULayer#defaultConfig
* @type {object}
* @since 4.0.0
* @readonly
* @property {string} name - The name of this RenderNode.
* @property {string} vertexSource - The vertex shader source.
* @property {string} fragmentSource - The fragment shader source.
*/
defaultConfig: {
name: 'SubmitterSpriteGPULayer',
count: 0,
shaderName: 'SpriteGPULayer',
vertexSource: ShaderSourceVS,
fragmentSource: ShaderSourceFS,
shaderAdditions: [
MakeGetTexCoordOut(),
MakeGetTexRes(),
MakeSmoothPixelArt(true),
MakeDefineTexCount(1),
MakeGetTexture(),
MakeApplyTint(),
MakeDefineLights(true),
MakeOutInverseRotation(true),
MakeGetNormalFromMap(true),
MakeApplyLighting(true)
],
instanceBufferLayout: {
usage: 'STATIC_DRAW',
instanceDivisor: 1,
layout: [
{
name: 'inPositionX',
size: 4
},
{
name: 'inPositionY',
size: 4
},
{
name: 'inRotation',
size: 4
},
{
name: 'inScaleX',
size: 4
},
{
name: 'inScaleY',
size: 4
},
{
name: 'inAlpha',
size: 4
},
{
name: 'inFrame',
size: 4
},
{
name: 'inTintBlend',
size: 4
},
{
name: 'inTintBL',
size: 4,
type: 'UNSIGNED_BYTE',
normalized: true
},
{
name: 'inTintTL',
size: 4,
type: 'UNSIGNED_BYTE',
normalized: true
},
{
name: 'inTintBR',
size: 4,
type: 'UNSIGNED_BYTE',
normalized: true
},
{
name: 'inTintTR',
size: 4,
type: 'UNSIGNED_BYTE',
normalized: true
},
{
name: 'inOriginAndTintModeAndCreationTime',
size: 4
},
{
name: 'inScrollFactor',
size: 2
}
]
},
vertexBufferLayout: {
usage: 'STATIC_DRAW',
count: 4,
layout: [
{
// The vertex index, 0-3.
name: 'inVertex',
type: 'UNSIGNED_BYTE'
}
]
}
},
/**
* Fill out the configuration object with default values where needed.
*
* @method Phaser.Renderer.WebGL.RenderNodes.SubmitterSpriteGPULayer#_completeLayout
* @since 4.0.0
* @param {object} config - The configuration object to complete.
*/
_completeLayout: function (config)
{
// Set up vertex buffer layout.
var layoutSource = config.vertexBufferLayout;
config.vertexBufferLayout = {};
config.vertexBufferLayout.usage = layoutSource.usage;
config.vertexBufferLayout.count = layoutSource.count;
config.vertexBufferLayout.layout = [];
var remove = config.vertexBufferLayoutRemove || [];
for (var i = 0; i < layoutSource.layout.length; i++)
{
var sourceAttr = layoutSource.layout[i];
if (remove.indexOf(sourceAttr.name) !== -1)
{
continue;
}
config.vertexBufferLayout.layout[i] = {
name: sourceAttr.name,
size: sourceAttr.size || 1,
type: sourceAttr.type || 'FLOAT',
normalized: sourceAttr.normalized || false
};
}
if (config.vertexBufferLayoutAdd)
{
var add = config.vertexBufferLayoutAdd || [];
for (i = 0; i < add.length; i++)
{
var addAttr = add[i];
config.vertexBufferLayout.layout.push({
name: addAttr.name,
size: addAttr.size || 1,
type: addAttr.type || 'FLOAT',
normalized: addAttr.normalized || false
});
}
}
// Set up instance buffer layout.
layoutSource = config.instanceBufferLayout;
config.instanceBufferLayout = {};
config.instanceBufferLayout.usage = layoutSource.usage;
config.instanceBufferLayout.instanceDivisor = layoutSource.instanceDivisor;
config.instanceBufferLayout.layout = [];
remove = config.instanceBufferLayoutRemove || [];
for (i = 0; i < layoutSource.layout.length; i++)
{
sourceAttr = layoutSource.layout[i];
if (remove.indexOf(sourceAttr.name) !== -1)
{
continue;
}
config.instanceBufferLayout.layout[i] = {
name: sourceAttr.name,
size: sourceAttr.size || 1,
type: sourceAttr.type || 'FLOAT',
normalized: sourceAttr.normalized || false
};
}
if (config.instanceBufferLayoutAdd)
{
add = config.instanceBufferLayoutAdd || [];
for (i = 0; i < add.length; i++)
{
addAttr = add[i];
config.instanceBufferLayout.layout.push({
name: addAttr.name,
size: addAttr.size || 1,
type: addAttr.type || 'FLOAT',
normalized: addAttr.normalized || false
});
}
}
},
/**
* Uploads all shader uniforms required for the current render pass.
* This includes camera projection and view matrices, render resolution,
* pixel rounding, scroll offsets, elapsed time, diffuse and frame data
* texture resolutions, gravity, texture sampler bindings, and lighting
* uniforms if lighting is enabled on the layer.
*
* @method Phaser.Renderer.WebGL.RenderNodes.SubmitterSpriteGPULayer#setupUniforms
* @since 4.0.0
* @param {Phaser.Renderer.WebGL.DrawingContext} drawingContext - The current drawing context.
*/
setupUniforms: function (drawingContext)
{
var camera = drawingContext.camera;
var programManager = this.programManager;
var layer = this.gameObject;
programManager.setUniform(
'uRoundPixels',
camera.roundPixels
);
programManager.setUniform(
'uResolution',
[ drawingContext.width, drawingContext.height ]
);
drawingContext.renderer.setProjectionMatrixFromDrawingContext(drawingContext);
programManager.setUniform(
'uProjectionMatrix',
drawingContext.renderer.projectionMatrix.val
);
var cm = camera.matrixCombined;
programManager.setUniform(
'uViewMatrix',
[
cm.a, cm.b, 0,
cm.c, cm.d, 0,
cm.tx, cm.ty, 1
]
);
programManager.setUniform(
'uCameraScrollAndAlpha',
[
camera.scrollX,
camera.scrollY,
layer.alpha
]
);
programManager.setUniform(
'uTime',
layer.timeElapsed
);
programManager.setUniform(
'uDiffuseResolution',
[
layer.frame.source.width,
layer.frame.source.height
]
);
programManager.setUniform(
'uFrameDataResolution',
[
layer.frameDataTexture.width,
layer.frameDataTexture.height
]
);
programManager.setUniform(
'uGravity',
layer.gravity
);
// Set texture sampler uniforms.
programManager.setUniform('uMainSampler[0]', 0);
programManager.setUniform('uFrameDataTexture', 1);
var glTexture = layer.texture.source[0].glTexture;
programManager.setUniform('uMainResolution[0]', [ glTexture.width, glTexture.height ]);
// Lighting uniforms.
Utils.updateLightingUniforms(
layer.lighting,
drawingContext,
programManager,
2,
this._lightVector,
layer.selfShadow.enabled,
layer.selfShadow.diffuseFlatThreshold,
layer.selfShadow.penumbra
);
},
/**
* Updates the shader program options based on the current state of the
* SpriteGPULayer game object. Enables or disables lighting shader additions,
* sets the active light count define, configures smooth pixel art rendering,
* and applies any shader features reported by the game object, including
* self-shadowing support.
*
* @method Phaser.Renderer.WebGL.RenderNodes.SubmitterSpriteGPULayer#updateRenderOptions
* @since 4.0.0
*/
updateRenderOptions: function ()
{
var programManager = this.programManager;
// Set lighting options.
var lighting = this.gameObject.lighting;
var lightingAdditions = programManager.getAdditionsByTag('LIGHTING');
for (var i = 0; i < lightingAdditions.length; i++)
{
lightingAdditions[i].disable = !lighting;
}
if (lighting)
{
var defineLightsAddition = programManager.getAddition('DefineLights');
if (defineLightsAddition)
{
defineLightsAddition.additions.fragmentDefine = '#define LIGHT_COUNT ' + this.manager.renderer.config.maxLights;
}
}
// Set smooth pixel art options.
var smoothAddition = programManager.getAddition('SmoothPixelArt');
if (smoothAddition)
{
var smoothPixelArt = this.gameObject.texture.smoothPixelArt;
if (smoothPixelArt === null)
{
smoothPixelArt = this.gameObject.scene.game.config.smoothPixelArt;
}
smoothAddition.disable = !smoothPixelArt;
}
// Set features.
programManager.clearFeatures();
var shaderFeatures = this.gameObject.getShaderFeatures();
for (i = 0; i < shaderFeatures.length; i++)
{
programManager.addFeature(shaderFeatures[i]);
}
if (this.gameObject.selfShadow.enabled)
{
programManager.addFeature('SELFSHADOW');
}
},
/**
* Render a SpriteGPULayer object.
*
* @method Phaser.Renderer.WebGL.RenderNodes.SubmitterSpriteGPULayer#run
* @since 4.0.0
* @param {Phaser.Renderer.WebGL.DrawingContext} drawingContext - The current drawing context.
*/
run: function (drawingContext)
{
var i;
var layer = this.gameObject;
if (layer.memberCount === 0)
{
return;
}
this.manager.startStandAloneRender();
this.onRunBegin(drawingContext);
// Update instance buffer if needed.
var segments = layer.bufferUpdateSegments;
if (segments > 0)
{
var buffer = this.instanceBufferLayout.buffer;
var memberCount = layer.memberCount;
var lastSegment = Math.floor(memberCount / layer.bufferUpdateSegmentSize);
var occupiedSegmentsAllUpdated = true;
for (i = 0; i <= lastSegment; i++)
{
if (1 << i & segments)
{
continue;
}
occupiedSegmentsAllUpdated = false;
break;
}
if (
segments === layer.MAX_BUFFER_UPDATE_SEGMENTS_FULL ||
memberCount <= layer.bufferUpdateSegmentSize ||
occupiedSegmentsAllUpdated
)
{
// Everything needs updating.
buffer.update();
}
else
{
// Only some segments need updating.
var segmentSize = layer.bufferUpdateSegmentSize;
var segmentByteSize = segmentSize * this.instanceBufferLayout.layout.stride;
for (
i = 0;
i < 32 && i * segmentSize < memberCount;
i++
)
{
if (segments & (1 << i))
{
buffer.update(segmentByteSize, i * segmentByteSize);
}
}
}
layer.clearAllSegmentsNeedUpdate();
}
// Assemble textures.
var textures = [
layer.frame.source.glTexture,
layer.frameDataTexture
];
if (layer.lighting)
{
var normalMap = layer.texture.dataSource[layer.frame.sourceIndex];
if (!normalMap)
{
normalMap = this.manager.renderer.normalTexture;
}
else
{
normalMap = normalMap.glTexture;
}
textures[2] = normalMap;
}
this.updateRenderOptions();
var programManager = this.programManager;
var programSuite = programManager.getCurrentProgramSuite();
if (programSuite)
{
var program = programSuite.program;
var vao = programSuite.vao;
this.setupUniforms(drawingContext);
programManager.applyUniforms(program);
// Render instances.
this.manager.renderer.drawInstancedArrays(
drawingContext,
textures,
program,
vao,
0,
4,
layer.memberCount
);
}
this.onRunEnd(drawingContext);
}
});
module.exports = SubmitterSpriteGPULayer;