phaser
Version:
A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.
603 lines (534 loc) • 20.8 kB
JavaScript
/**
* @author Benjamin D. Richards <benjamindrichards@gmail.com>
* @copyright 2013-2026 Phaser Studio Inc.
* @license None
*/
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 MakeAnimLength = require('../../shaders/additionMakers/MakeAnimLength');
var MakeApplyLighting = require('../../shaders/additionMakers/MakeApplyLighting');
var MakeDefineLights = require('../../shaders/additionMakers/MakeDefineLights');
var MakeSampleNormal = require('../../shaders/additionMakers/MakeSampleNormal');
var MakeSmoothPixelArt = require('../../shaders/additionMakers/MakeSmoothPixelArt');
var ShaderSourceFS = require('../../shaders/TilemapGPULayer-frag');
var ShaderSourceVS = require('../../shaders/TilemapGPULayer-vert');
var WebGLVertexBufferLayoutWrapper = require('../../wrappers/WebGLVertexBufferLayoutWrapper');
var RenderNode = require('../RenderNode');
var Utils = require('../../Utils');
/**
* @classdesc
* The SubmitterTilemapGPULayer RenderNode handles GPU-accelerated rendering
* of TilemapGPULayer objects in the WebGL renderer.
*
* Unlike batch-based submitters, this is a Stand Alone Render, meaning each
* layer is drawn in its own draw call. It manages its own shader program
* via a ProgramManager, which supports several optional shader features
* including smooth pixel art upscaling, normal-mapped lighting, animated
* tiles, self-shadowing, and bilinear border filtering. The appropriate
* shader variant is selected each frame based on the layer's configuration.
*
* @class SubmitterTilemapGPULayer
* @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.BatchHandlerConfig} [config] - The configuration object for this handler.
*/
var SubmitterTilemapGPULayer = new Class({
Extends: RenderNode,
initialize: function SubmitterTilemapGPULayer (manager, config)
{
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.SubmitterTilemapGPULayer#config
* @type {object}
* @since 4.0.0
*/
this.config = finalConfig;
/**
* The index buffer defining vertex order.
*
* @name Phaser.Renderer.WebGL.RenderNodes.SubmitterTilemapGPULayer#indexBuffer
* @type {Phaser.Renderer.WebGL.Wrappers.WebGLBufferWrapper}
* @since 4.0.0
*/
this.indexBuffer = renderer.genericQuadIndexBuffer;
/**
* The vertex buffer layout for this RenderNode.
*
* This consists of 4 vertices (indexed 0-3), one per corner of the quad.
*
* @name Phaser.Renderer.WebGL.RenderNodes.SubmitterTilemapGPULayer#vertexBufferLayout
* @type {Phaser.Renderer.WebGL.Wrappers.WebGLVertexBufferLayoutWrapper}
* @since 4.0.0
* @readonly
*/
this.vertexBufferLayout = new WebGLVertexBufferLayoutWrapper(
renderer,
finalConfig.vertexBufferLayout,
null
);
/**
* The program manager used to create and manage shader programs.
* This contains shader variants.
*
* @name Phaser.Renderer.WebGL.RenderNodes.SubmitterTilemapGPULayer#programManager
* @type {Phaser.Renderer.WebGL.ProgramManager}
* @since 4.0.0
*/
this.programManager = new ProgramManager(
renderer,
[ this.vertexBufferLayout ],
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]);
}
}
/**
* The matrix used internally to compute sprite transforms.
*
* @name Phaser.Renderer.WebGL.RenderNodes.SubmitterTilemapGPULayer#_spriteMatrix
* @type {Phaser.GameObjects.Components.TransformMatrix}
* @since 4.0.0
* @private
*/
this._spriteMatrix = new TransformMatrix();
/**
* The matrix used internally to compute the final transform.
*
* @name Phaser.Renderer.WebGL.RenderNodes.SubmitterTilemapGPULayer#_calcMatrix
* @type {Phaser.GameObjects.Components.TransformMatrix}
* @since 4.0.0
* @private
*/
this._calcMatrix = new TransformMatrix();
/**
* A vector used for temporary calculations.
*
* @name Phaser.Renderer.WebGL.RenderNodes.SubmitterTilemapGPULayer#_lightVector
* @type {Phaser.Math.Vector2}
* @since 4.0.0
* @private
*/
this._lightVector = new Vector2();
/**
* The matrix used to store the final quad data for rendering.
*
* @name Phaser.Renderer.WebGL.RenderNodes.SubmitterTilemapGPULayer#_quad
* @type {Float32Array}
* @since 4.0.0
* @private
*/
this._quad = new Float32Array(8);
},
/**
* Default configuration of this RenderNode.
*
* @name Phaser.Renderer.WebGL.RenderNodes.SubmitterTilemapGPULayer#defaultConfig
* @type {Phaser.Types.Renderer.WebGL.RenderNodes.BatchHandlerConfig}
* @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: 'SubmitterTilemapGPULayer',
shaderName: 'TilemapGPULayer',
vertexSource: ShaderSourceVS,
fragmentSource: ShaderSourceFS,
shaderAdditions: [
MakeSmoothPixelArt(true),
MakeSampleNormal(true),
MakeDefineLights(true),
MakeApplyLighting(true)
],
vertexBufferLayout: {
usage: 'DYNAMIC_DRAW',
count: 4,
layout: [
{
name: 'inPosition',
size: 2
},
{
name: 'inTexCoord',
size: 2
}
]
}
},
/**
* Fill out the configuration object with default values where needed.
*
* @method Phaser.Renderer.WebGL.RenderNodes.SubmitterTilemapGPULayer#_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 || 4;
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
});
}
}
},
/**
* Populates all shader uniforms required to render the given TilemapGPULayer.
*
* This includes the viewport resolution and projection matrix, the tileset
* image texture, the layer data texture, the animation data texture, tile
* dimensions (width, height, margin, and spacing), the layer's alpha and
* elapsed time for animation, and any lighting uniforms when the layer has
* lighting enabled.
*
* @method Phaser.Renderer.WebGL.RenderNodes.SubmitterTilemapGPULayer#setupUniforms
* @since 4.0.0
* @param {Phaser.Renderer.WebGL.DrawingContext} drawingContext - The current drawing context.
* @param {Phaser.Tilemaps.TilemapGPULayer} tilemapLayer - The TilemapGPULayer being rendered.
*/
setupUniforms: function (drawingContext, tilemapLayer)
{
var programManager = this.programManager;
// Standard uniforms.
programManager.setUniform(
'uResolution',
[ drawingContext.width, drawingContext.height ]
);
drawingContext.renderer.setProjectionMatrixFromDrawingContext(drawingContext);
programManager.setUniform(
'uProjectionMatrix',
drawingContext.renderer.projectionMatrix.val
);
// TilemapGPULayer uniforms.
var tileset = tilemapLayer.tileset;
var mainTexture = tileset.glTexture;
var layerTexture = tilemapLayer.layerDataTexture;
var animTexture = tileset.getAnimationDataTexture(drawingContext.renderer);
programManager.setUniform('uMainSampler', 0);
programManager.setUniform('uLayerSampler', 1);
programManager.setUniform('uAnimSampler', 2);
programManager.setUniform(
'uMainResolution',
[ mainTexture.width, mainTexture.height ]
);
programManager.setUniform(
'uLayerResolution',
[ layerTexture.width, layerTexture.height ]
);
programManager.setUniform(
'uAnimResolution',
[ animTexture.width, animTexture.height ]
);
programManager.setUniform(
'uTileColumns',
tileset.columns
);
programManager.setUniform(
'uTileWidthHeightMarginSpacing',
[
tileset.tileWidth,
tileset.tileHeight,
tileset.tileMargin,
tileset.tileSpacing
]
);
programManager.setUniform(
'uAlpha',
tilemapLayer.alpha
);
programManager.setUniform(
'uTime',
tilemapLayer.timeElapsed
);
// Lighting uniforms.
Utils.updateLightingUniforms(
tilemapLayer.lighting,
drawingContext,
programManager,
3,
this._lightVector,
tilemapLayer.selfShadow.enabled,
tilemapLayer.selfShadow.diffuseFlatThreshold,
tilemapLayer.selfShadow.penumbra
);
},
/**
* Evaluates the current state of the TilemapGPULayer and updates the
* shader program configuration accordingly. This is called before each
* render and may result in a different shader variant being selected.
*
* Specifically, it configures: the animation length addition (based on the
* tileset's maximum animation frame count), lighting additions (enabled or
* disabled based on the layer's lighting setting, with the light count
* define updated when active), the self-shadow feature flag, the smooth
* pixel art addition, and the border filter feature flag (based on whether
* the tileset texture uses linear magnification filtering).
*
* @method Phaser.Renderer.WebGL.RenderNodes.SubmitterTilemapGPULayer#updateRenderOptions
* @since 4.0.0
* @param {Phaser.Tilemaps.TilemapGPULayer} gameObject - The TilemapGPULayer being rendered.
*/
updateRenderOptions: function (gameObject)
{
var programManager = this.programManager;
var texture = gameObject.tileset.image;
// We do not track whether the shader program has changed.
// This is because this is not a batch renderer,
// and the program is set every time this is called.
// Set animation options.
var animAddition = programManager.getAdditionsByTag('MAXANIMS')[0];
if (animAddition)
{
programManager.removeAddition(animAddition.name);
}
if (gameObject.tileset.maxAnimationLength > 0)
{
programManager.addAddition(
MakeAnimLength(gameObject.tileset.maxAnimationLength)
);
}
// Set lighting options.
var lighting = gameObject.lighting;
var lightingAdditions = programManager.getAdditionsByTag('LIGHTING');
for (var i = 0; i < lightingAdditions.length; i++)
{
var addition = lightingAdditions[i];
addition.disable = !lighting;
}
if (lighting)
{
var defineLightsAddition = programManager.getAddition('DefineLights');
if (defineLightsAddition)
{
defineLightsAddition.additions.fragmentDefine =
'#define LIGHT_COUNT ' + this.manager.renderer.config.maxLights;
}
}
// Set self-shadow options.
var selfShadow = gameObject.selfShadow.enabled;
if (selfShadow === null)
{
selfShadow = gameObject.scene.sys.game.config.selfShadow;
}
if (selfShadow)
{
programManager.addFeature('SELFSHADOW');
}
else
{
programManager.removeFeature('SELFSHADOW');
}
// Set smooth pixel art options.
var smoothPixelArt = texture.smoothPixelArt;
if (smoothPixelArt === null)
{
smoothPixelArt = gameObject.scene.sys.game.config.smoothPixelArt;
}
var smoothPixelArtAddition = programManager.getAddition('SmoothPixelArt');
if (smoothPixelArtAddition)
{
smoothPixelArtAddition.disable = !smoothPixelArt;
}
// Set up border filtering options.
var borderFilter = texture.source[0].glTexture.magFilter === this.manager.renderer.gl.LINEAR;
if (borderFilter)
{
programManager.addFeature('BORDERFILTER');
}
else
{
programManager.removeFeature('BORDERFILTER');
}
},
/**
* The main render entry point for a TilemapGPULayer. Transforms the layer
* quad using the camera view matrix and any parent transform, writes the
* four corner positions and texture coordinates into the vertex buffer,
* assembles the texture array (tileset image, layer data, optional
* animation data, and optional normal map for lighting), selects the
* correct shader variant via `updateRenderOptions`, populates all uniforms
* via `setupUniforms`, and issues a single draw call for the layer.
*
* @method Phaser.Renderer.WebGL.RenderNodes.SubmitterTilemapGPULayer#run
* @since 4.0.0
* @param {Phaser.Renderer.WebGL.DrawingContext} drawingContext - The current drawing context.
* @param {Phaser.Tilemaps.TilemapGPULayer} tilemapLayer - The TilemapGPULayer being rendered.
* @param {Phaser.GameObjects.Components.TransformMatrix} [parentMatrix] - The parent matrix describing the game object's context.
*/
run: function (
drawingContext,
tilemapLayer,
parentMatrix
)
{
var manager = this.manager;
var renderer = manager.renderer;
manager.startStandAloneRender();
this.onRunBegin(drawingContext);
// Transform layer quad.
var camera = drawingContext.camera;
var spriteMatrix = this._spriteMatrix;
var calcMatrix = this._calcMatrix;
var quad = this._quad;
var x = tilemapLayer.x;
var y = tilemapLayer.y;
var width = tilemapLayer.width;
var height = tilemapLayer.height;
calcMatrix.copyWithScrollFactorFrom(
camera.getViewMatrix(!drawingContext.useCanvas),
camera.scrollX, camera.scrollY,
tilemapLayer.scrollFactorX, tilemapLayer.scrollFactorY
);
if (parentMatrix)
{
calcMatrix.multiply(parentMatrix);
}
spriteMatrix.applyITRS(x, y, 0, tilemapLayer.scaleX, tilemapLayer.scaleY);
// Multiply by the Sprite matrix
calcMatrix.multiply(spriteMatrix);
// Compute output quad.
calcMatrix.setQuad(
x,
y,
x + width,
y + height,
quad
);
// Populate vertex buffer.
var stride = this.vertexBufferLayout.layout.stride;
var vertexBuffer = this.vertexBufferLayout.buffer;
var vertexF32 = vertexBuffer.viewF32;
var offset32 = 0;
// Bottom Left.
vertexF32[offset32++] = quad[2];
vertexF32[offset32++] = quad[3];
vertexF32[offset32++] = 0;
vertexF32[offset32++] = 0;
// Top Left.
vertexF32[offset32++] = quad[0];
vertexF32[offset32++] = quad[1];
vertexF32[offset32++] = 0;
vertexF32[offset32++] = 1;
// Bottom Right.
vertexF32[offset32++] = quad[4];
vertexF32[offset32++] = quad[5];
vertexF32[offset32++] = 1;
vertexF32[offset32++] = 0;
// Top Right.
vertexF32[offset32++] = quad[6];
vertexF32[offset32++] = quad[7];
vertexF32[offset32++] = 1;
vertexF32[offset32++] = 1;
// Update vertex buffer.
// Because we frequently aren't filling the entire buffer,
// we need to update the buffer with the correct size.
vertexBuffer.update(stride * 4);
// Assemble textures.
var tileset = tilemapLayer.tileset;
var mainGlTexture = tileset.glTexture;
var animated = tileset.getAnimationDataIndexMap(renderer).size > 0;
var textures = [
mainGlTexture,
tilemapLayer.layerDataTexture
];
if (animated)
{
textures[2] = tileset.getAnimationDataTexture(renderer);
}
if (tilemapLayer.lighting)
{
var texture = tileset.image;
var normalMap = texture.dataSource[0];
if (!normalMap)
{
normalMap = this.manager.renderer.normalTexture;
}
else
{
normalMap = normalMap.glTexture;
}
textures[3] = normalMap;
}
this.updateRenderOptions(tilemapLayer);
var programManager = this.programManager;
var programSuite = programManager.getCurrentProgramSuite();
if (programSuite)
{
var program = programSuite.program;
var vao = programSuite.vao;
this.setupUniforms(drawingContext, tilemapLayer);
programManager.applyUniforms(program);
// Render layer.
renderer.drawElements(
drawingContext,
textures,
program,
vao,
4,
0
);
}
this.onRunEnd(drawingContext);
}
});
module.exports = SubmitterTilemapGPULayer;