<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Tiling support for custom WebGL layer views | Sample | ArcGIS Maps SDK for JavaScript</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix.js"></script>
<link rel="stylesheet" href="https://js.arcgis.com/5.0/esri/themes/light/main.css" />
<!-- Load the ArcGIS Maps SDK for JavaScript from CDN -->
<script type="module" src="https://js.arcgis.com/5.0/"></script>
const [Map, MapView, Layer, BaseLayerViewGL2D, TileInfo] = await $arcgis.import([
"@arcgis/core/views/MapView.js",
"@arcgis/core/layers/Layer.js",
"@arcgis/core/views/2d/layers/BaseLayerViewGL2D.js",
"@arcgis/core/layers/support/TileInfo.js",
const CustomLayerView2D = BaseLayerViewGL2D.createSubclass({
constructor: function () {
// Maps from tile id to the texture used by that tile.
this.textures = new window.Map();
// A [x, y, width, height] rectangle that describes the
// position of an unrotated tile in pixels.
this.rect = vec4.create();
// The modelTransform is a rotation around the center
// of the screen; we implement it as a translation, followed
// by a rotation, followed by a translation back to the origin.
// It is used to rotate the 'rect' defined above in the vertex
this.modelTransform = mat3.create();
this.modelTransformPreTranslation = mat3.create();
this.modelTransformRotation = mat3.create();
this.modelTransformPostTranslation = mat3.create();
// Transforms from pixels to normalized device coordinates.
this.ndcTransform = mat3.create();
// We create and keep around a canvas and its associated 2D context.
// We will use it at every frame to paint the textures for newly
const canvas = document.createElement("canvas");
canvas.width = this.layer.tileInfo.size[0];
canvas.height = this.layer.tileInfo.size[1];
const ctx = canvas.getContext("2d");
ctx.shadowColor = "black";
ctx.strokeStyle = "white";
ctx.font = "20px sans-serif";
// Creation of WebGL resources.
const vs = gl.createShader(gl.VERTEX_SHADER);
attribute vec2 a_Position;
uniform mat3 u_ModelTransform;
uniform mat3 u_NDCTransform;
vec3 transformed = u_NDCTransform * u_ModelTransform * vec3(u_Rect.xy + u_Rect.zw * a_Position, 1.0);
transformed /= transformed.z;
gl_Position = vec4(transformed.xy, 0.0, 1.0);
const fs = gl.createShader(gl.FRAGMENT_SHADER);
uniform sampler2D u_Texture;
gl_FragColor = texture2D(u_Texture, v_TexCoord);
this.program = gl.createProgram();
gl.attachShader(this.program, vs);
gl.attachShader(this.program, fs);
gl.bindAttribLocation(this.program, this.a_Position, "a_Position");
gl.linkProgram(this.program);
// Retrieve uniform locations.
this.u_Rect = gl.getUniformLocation(this.program, "u_Rect");
this.u_ModelTransform = gl.getUniformLocation(this.program, "u_ModelTransform");
this.u_NDCTransform = gl.getUniformLocation(this.program, "u_NDCTransform");
this.u_Texture = gl.getUniformLocation(this.program, "u_Texture");
// Create buffers. The tile is represented by a 1x1 square; the vertex shader will
// stretch it and position it on screen using the pixel values contained in `this.rect`.
this.vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array([0, 0, 1, 0, 1, 1, 0, 1]), gl.STATIC_DRAW);
this.indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
new Uint16Array([0, 1, 2, 0, 2, 3]),
// Creates the textures for new tiles that don't have a texture yet, and destroys the textures
// of tiles that are not on screen anymore.
manageTextures: function () {
const tileIdSet = new Set();
// Create new textures as needed.
for (let i = 0; i < this.tiles.length; ++i) {
const tile = this.tiles[i];
if (!this.textures.has(tile.id)) {
// The image is simply a border-to-border square with the tile id
// in the upper-left corner of the screen. When tiled, the squares
// will display as tile boundaries across the entire screen.
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
this.ctx.fillText(tile.id, 10, 24);
this.ctx.strokeRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
const texture = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.ctx.canvas);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
this.textures.set(tile.id, texture);
// Destroys unneeded textures.
this.textures.forEach((_, id) => {
if (!tileIdSet.has(id)) {
gl.deleteTexture(this.textures.get(id));
this.textures.delete(id);
// Example of a render implementation that draws tile boundaries
render: function (renderParameters) {
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.enableVertexAttribArray(this.a_Position);
gl.useProgram(this.program);
// Computes the matrices.
this.ndcTransform[0] = 2.0 / gl.canvas.width;
this.ndcTransform[3] = 0;
this.ndcTransform[6] = -1;
this.ndcTransform[1] = 0;
this.ndcTransform[4] = -2.0 / gl.canvas.height;
this.ndcTransform[7] = 1;
this.ndcTransform[2] = 0;
this.ndcTransform[5] = 0;
this.ndcTransform[8] = 1;
const tileSize = this.layer.tileInfo.size[0];
const state = renderParameters.state;
const pixelRatio = state.pixelRatio;
const width = state.size[0];
const height = state.size[1];
if (state.rotation !== 0) {
this.modelTransformPreTranslation[0] = 1;
this.modelTransformPreTranslation[3] = 0;
this.modelTransformPreTranslation[6] = -width * pixelRatio * 0.5;
this.modelTransformPreTranslation[1] = 0;
this.modelTransformPreTranslation[4] = 1;
this.modelTransformPreTranslation[7] = -height * pixelRatio * 0.5;
this.modelTransformPreTranslation[2] = 0;
this.modelTransformPreTranslation[5] = 0;
this.modelTransformPreTranslation[8] = 1;
const rot = (state.rotation * Math.PI) / 180;
this.modelTransformRotation[0] = Math.cos(rot);
this.modelTransformRotation[3] = -Math.sin(rot);
this.modelTransformRotation[6] = 0;
this.modelTransformRotation[1] = Math.sin(rot);
this.modelTransformRotation[4] = Math.cos(rot);
this.modelTransformRotation[7] = 0;
this.modelTransformRotation[2] = 0;
this.modelTransformRotation[5] = 0;
this.modelTransformRotation[8] = 1;
this.modelTransformPostTranslation[0] = 1;
this.modelTransformPostTranslation[3] = 0;
this.modelTransformPostTranslation[6] = width * pixelRatio * 0.5;
this.modelTransformPostTranslation[1] = 0;
this.modelTransformPostTranslation[4] = 1;
this.modelTransformPostTranslation[7] = height * pixelRatio * 0.5;
this.modelTransformPostTranslation[2] = 0;
this.modelTransformPostTranslation[5] = 0;
this.modelTransformPostTranslation[8] = 1;
this.modelTransformRotation,
this.modelTransformPreTranslation,
this.modelTransformPostTranslation,
this.modelTransform[0] = 1;
this.modelTransform[3] = 0;
this.modelTransform[6] = 0;
this.modelTransform[1] = 0;
this.modelTransform[4] = 1;
this.modelTransform[7] = 0;
this.modelTransform[2] = 0;
this.modelTransform[5] = 0;
this.modelTransform[8] = 1;
// Set per-frame states and uniforms.
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.vertexAttribPointer(this.a_Position, 2, gl.UNSIGNED_BYTE, false, 2, 0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
gl.uniformMatrix3fv(this.u_NDCTransform, false, this.ndcTransform);
gl.uniformMatrix3fv(this.u_ModelTransform, false, this.modelTransform);
// Render each tile separately.
for (let i = 0; i < this.tiles.length; i++) {
// Retrieve the current tile and its associated texture.
const tile = this.tiles[i];
const texture = this.textures.get(tile.id);
// Derive the screen `rect` for this tile.
const screenScale = (tile.resolution / state.resolution) * pixelRatio;
state.toScreenNoRotation(coords, tile.coords);
this.rect[0] = coords[0];
this.rect[1] = coords[1];
this.rect[2] = tileSize * screenScale;
this.rect[3] = tileSize * screenScale;
// Set per-tile uniforms and states.
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform4fv(this.u_Rect, this.rect);
gl.uniform1i(this.u_Texture, 0);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
// Destroy the shader program, the buffers and all the tile textures.
gl.deleteProgram(this.program);
gl.deleteBuffer(this.vertexBuffer);
gl.deleteBuffer(this.indexBuffer);
this.textures.forEach((_, id) => {
gl.deleteTexture(this.textures.get(id));
this.textures.delete(id);
// Required when using tiling; this methods is called every time that `this.tiles`
// changes, to give the derived class a chance to perform per-tile work as needed;
// This is where, for instance, tile data could be fetched from a server.
tilesChanged: function () {},
const CustomLayer = Layer.createSubclass({
// We want to take advantage of the tiling capabilities of BaseLayerViewGL2D.
tileInfo: TileInfo.create({ size: 512, spatialReference: { wkid: 3857 } }),
createLayerView: function (view) {
if (view.type === "2d") {
return new CustomLayerView2D({
const layer = new CustomLayer();
const view = new MapView({