gl-tiled
Version:
A Tiled editor renderer for WebGL.
732 lines (588 loc) • 22.5 kB
text/typescript
// @if DEBUG
import { ASSERT } from './debug';
// @endif
import { ILayer } from './tiled/layers';
import { ITilemap } from './tiled/Tilemap';
import { assertNever } from './utils/assertNever';
import { GLProgram } from './utils/GLProgram';
import { hasOwnKey } from './utils/hasOwnKey';
import { parseColorStr } from './utils/parseColorStr';
import { ELayerType } from './ELayerType';
import { GLTileset } from './GLTileset';
// import { GLObjectgroup } from './GLObjectgroup';
import { GLTilelayer } from './GLTilelayer';
import { GLImagelayer } from './GLImagelayer';
import { IAssetCache } from './IAssetCache';
import backgroundVS from './shaders/background.vert';
import backgroundFS from './shaders/background.frag';
import tilelayerVS from './shaders/tilelayer.vert';
import tilelayerFS from './shaders/tilelayer.frag';
import imagelayerVS from './shaders/imagelayer.vert';
import imagelayerFS from './shaders/imagelayer.frag';
export type TGLLayer = GLTilelayer | GLImagelayer /*| GLObjectgroup*/;
export interface IBlendMode
{
/**
* A 2 or 4 element array specifying which blend function to use.
*
* Default: [gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA]
*/
func: number[];
/** What blend equation to use. Default: gl.FUNC_ADD */
equation: number;
}
export interface ITilemapOptions
{
/** The WebGL context to use to render. */
gl?: WebGLRenderingContext;
/** A cache of preloaded assets. Keyed by URL as it appears in the tilemap data. */
assetCache?: IAssetCache;
/** What blend function/equation should we draw with? */
blendMode?: IBlendMode;
/** Should we render the background color of the map? Default: true */
renderBackgroundColor?: boolean;
/** Should we automatically create each imagelayer? Default: true */
createAllImagelayers?: boolean;
/** Should we automatically create each tilelayer? Default: true */
createAllTilelayers?: boolean;
/** Should we automatically create each objectgroup? Default: false */
// createAllObjectgroups?: boolean;
}
interface IShaderCache
{
background: GLProgram;
tilelayer: GLProgram;
imagelayer: GLProgram;
}
export class GLTilemap
{
private static _attribIndices = {
aPosition: 0,
aTexture: 1,
};
gl: WebGLRenderingContext | null = null;
shaders: IShaderCache | null = null;
renderBackgroundColor: boolean;
blendMode: IBlendMode;
readonly assetCache: IAssetCache | undefined = undefined;
private _layers: TGLLayer[] = [];
private _tilesets: GLTileset[] = [];
private _viewportSize = new Float32Array(2);
private _scaledViewportSize = new Float32Array(2);
private _inverseLayerTileSize = new Float32Array(2);
private _quadVerts = new Float32Array([
//x y u v
-1, -1, 0, 1,
1, -1, 1, 1,
1, 1, 1, 0,
-1, -1, 0, 1,
1, 1, 1, 0,
-1, 1, 0, 0,
]);
private _quadVertBuffer: WebGLBuffer | null = null;
private _firstTilelayerUniformUpload = true;
private _tileScale = 1;
private _totalTilesetImages = 0;
private _backgroundColor: Float32Array;
private _tilesetIndices: Int32Array;
private _tilesetTileSizeBuffer: Float32Array;
private _tilesetTileOffsetBuffer: Float32Array;
private _inverseTilesetTextureSizeBuffer: Float32Array;
constructor(public readonly desc: ITilemap, options: ITilemapOptions = {})
{
// @if DEBUG
ASSERT(desc.version >= 1.2, `Unsupported JSON format version ${desc.version}, please update your JSON to v1.2`);
// @endif
if (options.assetCache)
this.assetCache = options.assetCache;
this.renderBackgroundColor = typeof options.renderBackgroundColor === 'boolean' ? options.renderBackgroundColor : true;
this.blendMode = {
func: options.blendMode && options.blendMode.func || [WebGLRenderingContext.SRC_ALPHA, WebGLRenderingContext.ONE_MINUS_SRC_ALPHA],
equation: options.blendMode && options.blendMode.equation || WebGLRenderingContext.FUNC_ADD,
};
this._inverseLayerTileSize[0] = 1 / desc.tilewidth;
this._inverseLayerTileSize[1] = 1 / desc.tileheight;
for (let i = 0; i < desc.tilesets.length; ++i)
{
const tileset = new GLTileset(desc.tilesets[i], this.assetCache);
this._totalTilesetImages += tileset.images.length;
this._tilesets.push(tileset);
}
this._createInitialLayers(desc.layers, options);
// parse the background color
this._backgroundColor = new Float32Array(4);
if (desc.backgroundcolor)
parseColorStr(desc.backgroundcolor, this._backgroundColor);
// setup the different buffers
this._tilesetIndices = new Int32Array(this._totalTilesetImages);
this._tilesetTileSizeBuffer = new Float32Array(this._totalTilesetImages * 2);
this._tilesetTileOffsetBuffer = new Float32Array(this._totalTilesetImages * 2);
this._inverseTilesetTextureSizeBuffer = new Float32Array(this._totalTilesetImages * 2);
this._buildBufferData();
if (options.gl)
{
this.glInitialize(options.gl);
}
}
get layers(): ReadonlyArray<TGLLayer>
{
return this._layers;
}
get tilesets(): ReadonlyArray<GLTileset>
{
return this._tilesets;
}
get viewportWidth(): number
{
return this._viewportSize[0];
}
get viewportHeight(): number
{
return this._viewportSize[1];
}
get scaledViewportWidth(): number
{
return this._scaledViewportSize[0];
}
get scaledViewportHeight(): number
{
return this._scaledViewportSize[1];
}
set repeatTiles(v: boolean)
{
for (let i = 0; i < this._layers.length; ++i)
{
const layer = this._layers[i];
if (layer.type === ELayerType.Tilelayer)
{
layer.repeatTiles = false;
}
}
}
get tileScale(): number
{
return this._tileScale;
}
set tileScale(scale: number)
{
if (this._tileScale != scale)
{
this._tileScale = scale;
this._updateViewportSize();
}
}
resizeViewport(width: number, height: number): void
{
if (this._viewportSize[0] != width || this._viewportSize[1] != height)
{
this._viewportSize[0] = width;
this._viewportSize[1] = height;
this._updateViewportSize();
}
}
glInitialize(gl: WebGLRenderingContext): void
{
this.glTerminate();
this.gl = gl;
this._firstTilelayerUniformUpload = true;
// initialize layers
for (let i = 0; i < this._layers.length; ++i)
{
this._layers[i].glInitialize(gl);
}
// initialize tilesets
for (let i = 0; i < this._tilesets.length; ++i)
{
this._tilesets[i].glInitialize(gl);
}
// create buffers
this._quadVertBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this._quadVertBuffer);
gl.bufferData(gl.ARRAY_BUFFER, this._quadVerts, gl.STATIC_DRAW);
// create shaders
this._createShaders();
// update viewport uniforms
this._updateViewportSize();
}
glTerminate(): void
{
if (!this.gl)
return;
const gl = this.gl;
// destroy layers
for (let i = 0; i < this._layers.length; ++i)
{
this._layers[i].glTerminate();
}
// destroy tilesets
for (let i = 0; i < this._tilesets.length; ++i)
{
this._tilesets[i].glTerminate();
}
// destroy buffers
if (this._quadVertBuffer)
{
gl.deleteBuffer(this._quadVertBuffer);
this._quadVertBuffer = null;
}
// destroy shaders
for (const k in this.shaders)
{
if (!hasOwnKey(this.shaders, k))
continue;
const shader = this.shaders[k];
gl.deleteProgram(shader.program);
}
this.shaders = null;
this.gl = null;
}
/**
* Updates each layer's animations by the given delta time.
*
* @param dt Delta time in milliseconds to perform an update for.
*/
update(dt: number): void
{
for (let i = 0; i < this.layers.length; ++i)
{
const layer = this._layers[i];
if (layer.type === ELayerType.Tilelayer)
layer.update(dt);
}
}
/**
* Draws the tilemap.
*
* @param x The x offset at which to draw the map
* @param y The y offset at which to draw the map
*/
draw(x: number = 0, y: number = 0): void
{
if (!this.gl || !this.shaders)
return;
var gl = this.gl;
gl.enable(gl.BLEND);
gl.blendEquation(this.blendMode.equation);
if (this.blendMode.func.length > 2)
{
gl.blendFuncSeparate(
this.blendMode.func[0],
this.blendMode.func[1],
this.blendMode.func[2],
this.blendMode.func[3]);
}
else
{
gl.blendFunc(this.blendMode.func[0], this.blendMode.func[1]);
}
// Enable attributes, these are the same for all shaders.
gl.bindBuffer(gl.ARRAY_BUFFER, this._quadVertBuffer);
gl.enableVertexAttribArray(GLTilemap._attribIndices.aPosition);
gl.enableVertexAttribArray(GLTilemap._attribIndices.aTexture);
gl.vertexAttribPointer(GLTilemap._attribIndices.aPosition, 2, gl.FLOAT, false, 16, 0);
gl.vertexAttribPointer(GLTilemap._attribIndices.aTexture, 2, gl.FLOAT, false, 16, 8);
// Draw background
if (this.renderBackgroundColor && this._backgroundColor[3] > 0)
{
const bgShader = this.shaders.background;
// @if DEBUG
ASSERT(!!(bgShader.uniforms.uColor), 'Invalid uniforms for background shader.');
// @endif
gl.useProgram(bgShader.program);
gl.uniform4fv(bgShader.uniforms.uColor!, this._backgroundColor);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
// Bind tileset textures
let imgIndex = 0;
for (let i = 0; i < this._tilesets.length; ++i)
{
const tileset = this._tilesets[i];
for (let t = 0; t < tileset.textures.length; ++t)
{
this.gl.activeTexture(gl.TEXTURE1 + imgIndex);
this.gl.bindTexture(this.gl.TEXTURE_2D, tileset.textures[t]);
imgIndex++;
}
}
// Draw each layer of the map
gl.activeTexture(gl.TEXTURE0);
let lastShader = ELayerType.UNKNOWN;
let activeShader: GLProgram | null = null;
for (let i = 0; i < this._layers.length; ++i)
{
const layer = this._layers[i];
const offsetx = layer.desc.offsetx || 0;
const offsety = layer.desc.offsety || 0;
if (!layer.desc.visible)
continue;
if (lastShader != layer.type)
{
activeShader = this._bindShader(layer);
lastShader = layer.type;
}
if (!activeShader)
continue;
// @if DEBUG
ASSERT(!!(activeShader.uniforms.uOffset), 'Invalid uniforms for layer shader.');
// @endif
switch (layer.type)
{
case ELayerType.Tilelayer:
layer.uploadUniforms(activeShader);
gl.uniform2f(
activeShader.uniforms.uOffset!,
-offsetx + (x * layer.scrollScaleX),
-offsety + (y * layer.scrollScaleY)
);
break;
case ELayerType.Imagelayer:
layer.uploadUniforms(activeShader);
gl.uniform2f(
activeShader.uniforms.uOffset!,
offsetx + (-x * layer.scrollScaleX),
-offsety + (y * layer.scrollScaleY)
);
break;
// case ELayerType.Objectgroup:
// break;
default:
assertNever(layer);
}
// if (layer.type !== ELayerType.Objectgroup)
{
gl.bindTexture(gl.TEXTURE_2D, layer.texture);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
}
}
findLayerDesc(...name: string[]): ILayer | null
{
return this._doFindLayerDesc(this.desc.layers, name, 0);
}
createLayer(...name: string[]): boolean
{
if (name.length === 0)
return false;
const layerDesc = this._doFindLayerDesc(this.desc.layers, name, 0);
if (!layerDesc)
return false;
this.createLayerFromDesc(layerDesc);
return true;
}
destroyLayer(...name: string[]): boolean
{
if (name.length === 0)
return false;
const layerDesc = this._doFindLayerDesc(this.desc.layers, name, 0);
if (!layerDesc)
return false;
return this.destroyLayerFromDesc(layerDesc);
}
createLayerFromDesc(layer: ILayer): void
{
let newLayer: TGLLayer | null = null;
switch (layer.type)
{
case 'tilelayer':
newLayer = new GLTilelayer(layer, this.tilesets);
break;
case 'objectgroup':
// newLayer = new GLObjectgroup(layer);
break;
case 'imagelayer':
newLayer = new GLImagelayer(layer, this.assetCache);
break;
case 'group':
for (let i = 0; i < layer.layers.length; ++i)
{
this.createLayerFromDesc(layer.layers[i]);
}
break;
default:
return assertNever(layer);
}
if (newLayer)
{
this._layers.push(newLayer);
if (this.gl)
newLayer.glInitialize(this.gl);
}
}
destroyLayerFromDesc(layerDesc: ILayer): boolean
{
for (let i = 0; i < this._layers.length; ++i)
{
const layer = this._layers[i];
if (layer.desc === layerDesc)
{
layer.glTerminate();
this._layers.splice(1, 1);
return true;
}
}
return false;
}
private _doFindLayerDesc(layers: ILayer[], names: string[], nameIndex: number): ILayer | null
{
for (let i = 0; i < layers.length; ++i)
{
const layer = layers[i];
if (layer.name === names[nameIndex])
{
if (layer.type === 'group')
{
// more names, so try something in this group
if (names.length < nameIndex + 1)
{
return this._doFindLayerDesc(layer.layers, names, ++nameIndex);
}
// No more names, return the group.
else
{
return layer;
}
}
else
{
return layer;
}
}
}
return null;
}
private _bindShader(layer: TGLLayer): GLProgram
{
// @if DEBUG
ASSERT(!!(this.gl && this.shaders), 'Cannot call `_bindShader` before `glInitialize`.');
// @endif
const gl = this.gl!;
switch (layer.type)
{
case ELayerType.Tilelayer:
{
const tileShader = this.shaders!.tilelayer;
gl.useProgram(tileShader.program);
// these are static, and only need to be uploaded once.
if (this._firstTilelayerUniformUpload)
{
this._firstTilelayerUniformUpload = false;
// @if DEBUG
ASSERT(!!(tileShader.uniforms.uLayer
&& tileShader.uniforms.uInverseLayerTileSize
&& tileShader.uniforms.uTilesets
&& tileShader.uniforms.uTilesetTileSize
&& tileShader.uniforms.uTilesetTileOffset
&& tileShader.uniforms.uInverseTilesetTextureSize),
'Invalid uniforms for tile layer shader.');
// @endif
gl.uniform1i(tileShader.uniforms.uLayer!, 0);
gl.uniform2fv(tileShader.uniforms.uInverseLayerTileSize!, this._inverseLayerTileSize);
gl.uniform1iv(tileShader.uniforms.uTilesets!, this._tilesetIndices);
gl.uniform2fv(tileShader.uniforms.uTilesetTileSize!, this._tilesetTileSizeBuffer);
gl.uniform2fv(tileShader.uniforms.uTilesetTileOffset!, this._tilesetTileOffsetBuffer);
gl.uniform2fv(tileShader.uniforms.uInverseTilesetTextureSize!, this._inverseTilesetTextureSizeBuffer);
}
return tileShader;
}
case ELayerType.Imagelayer:
{
const imageShader = this.shaders!.imagelayer;
gl.useProgram(imageShader.program);
return imageShader;
}
// case ELayerType.Objectgroup:
// return ;
default:
return assertNever(layer);
}
}
private _updateViewportSize(): void
{
if (!this.gl || !this.shaders)
return;
this._scaledViewportSize[0] = this._viewportSize[0] / this._tileScale;
this._scaledViewportSize[1] = this._viewportSize[1] / this._tileScale;
const gl = this.gl!;
const tileShader = this.shaders!.tilelayer;
// @if DEBUG
ASSERT(!!(tileShader.uniforms.uViewportSize
&& tileShader.uniforms.uInverseTileScale),
'Invalid uniforms for tile layer shader.');
// @endif
gl.useProgram(tileShader.program);
gl.uniform2fv(tileShader.uniforms.uViewportSize!, this._scaledViewportSize);
gl.uniform1f(tileShader.uniforms.uInverseTileScale!, 1.0 / this._tileScale);
const imageShader = this.shaders!.imagelayer;
// @if DEBUG
ASSERT(!!(imageShader.uniforms.uViewportSize
&& imageShader.uniforms.uInverseTileScale),
'Invalid uniforms for image shader.');
// @endif
gl.useProgram(imageShader.program);
gl.uniform2fv(imageShader.uniforms.uViewportSize!, this._scaledViewportSize);
gl.uniform1f(imageShader.uniforms.uInverseTileScale!, 1.0 / this._tileScale);
}
private _buildBufferData(): void
{
// Index buffer
for (let i = 0; i < this._tilesetIndices.length; ++i)
this._tilesetIndices[i] = i + 1;
// tileset size buffers
let imgIndex = 0;
for (let i = 0; i < this._tilesets.length; ++i)
{
const tileset = this._tilesets[i];
for (let s = 0; s < tileset.images.length; ++s)
{
this._tilesetTileSizeBuffer[(imgIndex * 2)] = tileset.desc.tilewidth;
this._tilesetTileSizeBuffer[(imgIndex * 2) + 1] = tileset.desc.tileheight;
this._tilesetTileOffsetBuffer[(imgIndex * 2)] = tileset.desc.spacing;
this._tilesetTileOffsetBuffer[(imgIndex * 2) + 1] = tileset.desc.margin;
const imgDesc = tileset.desc.tiles && tileset.desc.tiles[s];
const imgWidth = imgDesc && imgDesc.imagewidth ? imgDesc.imagewidth : tileset.desc.imagewidth;
const imgHeight = imgDesc && imgDesc.imageheight ? imgDesc.imageheight : tileset.desc.imageheight;
this._inverseTilesetTextureSizeBuffer[(imgIndex * 2)] = 1 / imgWidth;
this._inverseTilesetTextureSizeBuffer[(imgIndex * 2) + 1] = 1 / imgHeight;
imgIndex++;
}
}
}
private _createInitialLayers(layers: ILayer[], options: ITilemapOptions): void
{
const createTilelayers = typeof options.createAllTilelayers === 'boolean' ? options.createAllTilelayers : true;
const createImagelayers = typeof options.createAllImagelayers === 'boolean' ? options.createAllImagelayers : true;
// const createObjectgroups = typeof options.createAllObjectgroups === 'boolean' ? options.createAllObjectgroups : false;
// We don't create anything, early out.
if (!createTilelayers && !createImagelayers /*&& !createObjectgroups*/)
return;
for (let i = 0; i < layers.length; ++i)
{
const layer = layers[i];
if ((layer.type === 'tilelayer' && createTilelayers)
// || (layer.type === 'objectgroup' && createObjectgroups)
|| (layer.type === 'imagelayer' && createImagelayers))
{
this.createLayerFromDesc(layer);
}
else if (layer.type === 'group')
{
this._createInitialLayers(layer.layers, options);
}
}
}
private _createShaders(): void
{
// @if DEBUG
ASSERT(!!(this.gl), 'Cannot call `_createShaders` before `glInitialize`.');
// @endif
const tilelayerFragShader = tilelayerFS
.replace('#pragma define(NUM_TILESETS)', `#define NUM_TILESETS ${this._tilesets.length}`)
.replace('#pragma define(NUM_TILESET_IMAGES)', `#define NUM_TILESET_IMAGES ${this._totalTilesetImages}`);
const gl = this.gl!;
this.shaders = {
background: new GLProgram(gl, backgroundVS, backgroundFS, GLTilemap._attribIndices),
tilelayer: new GLProgram(gl, tilelayerVS, tilelayerFragShader, GLTilemap._attribIndices),
imagelayer: new GLProgram(gl, imagelayerVS, imagelayerFS, GLTilemap._attribIndices),
};
}
}