From arcgis-maps-sdk-js-ai-context
Create custom ArcGIS layers with WebGL rendering using RenderNode, BaseLayerViewGL2D, BaseTileLayer, and BaseDynamicLayer for 2D/3D visualizations, custom data sources, and post-processing effects.
How this skill is triggered — by the user, by Claude, or both
Slash command
/arcgis-maps-sdk-js-ai-context:arcgis-custom-renderingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use this skill for creating custom layers, WebGL rendering in 2D/3D, custom tile sources, and post-processing effects in SceneView.
Use this skill for creating custom layers, WebGL rendering in 2D/3D, custom tile sources, and post-processing effects in SceneView.
// 3D custom rendering
import RenderNode from "@arcgis/core/views/3d/webgl/RenderNode.js";
// 2D custom rendering
import BaseLayerView2D from "@arcgis/core/views/2d/layers/BaseLayerView2D.js";
import BaseLayerViewGL2D from "@arcgis/core/views/2d/layers/BaseLayerViewGL2D.js";
// Custom layer bases
import BaseTileLayer from "@arcgis/core/layers/BaseTileLayer.js";
import BaseDynamicLayer from "@arcgis/core/layers/BaseDynamicLayer.js";
import BaseElevationLayer from "@arcgis/core/layers/BaseElevationLayer.js";
import Layer from "@arcgis/core/layers/Layer.js";
// Utilities
import esriRequest from "@arcgis/core/request.js";
import Color from "@arcgis/core/Color.js";
const RenderNode = await $arcgis.import(
"@arcgis/core/views/3d/webgl/RenderNode.js",
);
const BaseTileLayer = await $arcgis.import(
"@arcgis/core/layers/BaseTileLayer.js",
);
const BaseLayerViewGL2D = await $arcgis.import(
"@arcgis/core/views/2d/layers/BaseLayerViewGL2D.js",
);
| Class | 2D | 3D | Typical Use |
|---|---|---|---|
RenderNode | — | Yes | Post-processing effects in SceneView |
BaseLayerView2D | Yes | — | Custom 2D canvas rendering |
BaseLayerViewGL2D | Yes | — | WebGL-accelerated 2D rendering |
BaseTileLayer | Yes | Yes | Custom tile sources and processing |
BaseDynamicLayer | Yes | Yes | On-demand dynamic rendering |
BaseElevationLayer | — | Yes | Custom elevation/terrain data |
RenderNode creates custom rendering passes in SceneView's WebGL 2 pipeline.
const LuminanceRenderNode = RenderNode.createSubclass({
constructor() {
this.consumes = { required: ["composite-color"] };
this.produces = "composite-color";
},
render(inputs) {
const input = inputs.find(({ name }) => name === "composite-color");
const colorTexture = input.getTexture();
const output = this.acquireOutputFramebuffer();
const gl = this.gl;
gl.useProgram(this.program);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, colorTexture);
gl.uniform1i(this.texLocation, 0);
gl.bindVertexArray(this.vao);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// Preserve depth from input
output.attachDepth(input.getAttachment(gl.DEPTH_STENCIL_ATTACHMENT));
return output;
},
destroy() {
if (this.program) this.gl?.deleteProgram(this.program);
if (this.vao) this.gl?.deleteVertexArray(this.vao);
},
});
// Add to SceneView
const view = new SceneView({ container: "viewDiv", map });
view.when(() => {
new LuminanceRenderNode({ view });
});
| Property/Method | Description |
|---|---|
consumes | Render targets this node needs as input (e.g., { required: ["composite-color"] }) |
produces | Name of the render target produced (set null to disable) |
gl | WebGL 2 context (read-only) |
view | Reference to SceneView |
render(inputs) | Override to implement rendering logic |
acquireOutputFramebuffer() | Get framebuffer to render into |
requestRender() | Request a new frame |
const MyRenderNode = RenderNode.createSubclass({
properties: {
enabled: {
get() {
return this.produces != null;
},
set(value) {
this.produces = value ? "composite-color" : null;
this.requestRender();
},
},
},
});
#version 300 es
precision highp float;
out lowp vec4 fragColor;
in vec2 uv;
uniform sampler2D colorTex;
void main() {
vec4 color = texture(colorTex, uv);
// Luminance (grayscale) conversion
fragColor = vec4(vec3(dot(color.rgb, vec3(0.2126, 0.7152, 0.0722))), color.a);
}
const TintLayer = BaseTileLayer.createSubclass({
properties: {
urlTemplate: null,
tint: {
value: null,
type: Color,
},
},
getTileUrl(level, row, col) {
return this.urlTemplate
.replace("{z}", level)
.replace("{x}", col)
.replace("{y}", row);
},
fetchTile(level, row, col, options) {
const url = this.getTileUrl(level, row, col);
return esriRequest(url, {
responseType: "image",
signal: options?.signal,
}).then((response) => {
const image = response.data;
const width = this.tileInfo.size[0];
const height = this.tileInfo.size[0];
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
if (this.tint) {
context.fillStyle = this.tint.toCss();
context.fillRect(0, 0, width, height);
context.globalCompositeOperation = "difference";
}
context.drawImage(image, 0, 0, width, height);
return canvas;
});
},
});
const customLayer = new TintLayer({
urlTemplate: "https://tile.opentopomap.org/{z}/{x}/{y}.png",
tint: new Color("#132178"),
title: "Custom Tinted Layer",
});
map.add(customLayer);
| Method | Description |
|---|---|
getTileUrl(level, row, col) | Return URL string for a tile |
fetchTile(level, row, col, options) | Fetch and optionally process tile; return canvas or image |
load() | Called when layer loads |
refresh() | Reload all tiles |
const CustomLayer = BaseTileLayer.createSubclass({
properties: {
customProperty: null,
},
initialize() {
reactiveUtils.watch(
() => this.customProperty,
() => this.refresh(),
);
},
});
const CustomDynamicLayer = BaseDynamicLayer.createSubclass({
properties: {
data: null,
},
getImageUrl(extent, width, height) {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
// Convert geo coords to pixel coords
const xScale = width / (extent.xmax - extent.xmin);
const yScale = height / (extent.ymax - extent.ymin);
this.data.forEach((point) => {
const x = (point.x - extent.xmin) * xScale;
const y = height - (point.y - extent.ymin) * yScale;
context.beginPath();
context.arc(x, y, 5, 0, Math.PI * 2);
context.fillStyle = "red";
context.fill();
});
return canvas.toDataURL("image/png");
},
});
const ExaggeratedElevationLayer = BaseElevationLayer.createSubclass({
properties: {
exaggeration: 2,
},
load() {
this._elevation = new ElevationLayer({
url: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer",
});
this.addResolvingPromise(this._elevation.load());
},
fetchTile(level, row, col, options) {
return this._elevation.fetchTile(level, row, col, options).then((data) => {
const exaggeration = this.exaggeration;
for (let i = 0; i < data.values.length; i++) {
data.values[i] *= exaggeration;
}
return data;
});
},
});
const CustomGLLayerView = BaseLayerViewGL2D.createSubclass({
attach() {
const gl = this.context;
this.program = this.createShaderProgram(gl);
this.buffer = gl.createBuffer();
this.vao = gl.createVertexArray();
},
render(renderParameters) {
const gl = renderParameters.context;
const { displayViewMatrix3, size } = renderParameters.state;
gl.useProgram(this.program);
gl.uniformMatrix3fv(
gl.getUniformLocation(this.program, "u_matrix"),
false,
displayViewMatrix3,
);
gl.bindVertexArray(this.vao);
gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
},
detach() {
const gl = this.context;
gl.deleteProgram(this.program);
gl.deleteBuffer(this.buffer);
gl.deleteVertexArray(this.vao);
},
});
const CustomLayer = Layer.createSubclass({
createLayerView(view) {
if (view.type === "2d") {
return new CustomGLLayerView({ view, layer: this });
}
},
});
const CustomLayerView2D = BaseLayerView2D.createSubclass({
attach() {
// Initialize resources
},
render(renderParameters) {
const { context, state } = renderParameters;
const ctx = context; // CanvasRenderingContext2D
// state.size — viewport size [width, height]
// state.resolution — map units per pixel
// state.extent — current visible extent
const screenPoint = state.toScreen(state.center);
ctx.fillStyle = "red";
ctx.fillRect(screenPoint[0] - 5, screenPoint[1] - 5, 10, 10);
},
detach() {
// Clean up resources
},
});
| Method | Description |
|---|---|
attach() | Called when LayerView is added to view — init resources |
render(renderParameters) | Called every frame to draw content |
detach() | Called when LayerView is removed — clean up resources |
requestRender() | Request a re-render on next frame |
import * as Lerc from "https://cdn.jsdelivr.net/npm/[email protected]/+esm";
const LercLayer = BaseTileLayer.createSubclass({
properties: {
urlTemplate: null,
minValue: 0,
maxValue: 1000,
},
fetchTile(level, row, col, options) {
const url = this.urlTemplate
.replace("{z}", level)
.replace("{x}", col)
.replace("{y}", row);
return esriRequest(url, {
responseType: "array-buffer",
signal: options?.signal,
}).then((response) => {
const decodedPixels = Lerc.decode(response.data);
const { width, height, pixels } = decodedPixels;
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(width, height);
for (let i = 0; i < pixels[0].length; i++) {
const value = pixels[0][i];
const normalized =
(value - this.minValue) / (this.maxValue - this.minValue);
imageData.data[i * 4] = Math.round(normalized * 255);
imageData.data[i * 4 + 1] = 0;
imageData.data[i * 4 + 2] = Math.round((1 - normalized) * 255);
imageData.data[i * 4 + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
return canvas;
});
},
});
const BlendLayer = BaseTileLayer.createSubclass({
properties: {
multiplyLayers: null,
},
load() {
this.multiplyLayers.forEach((layer) => {
this.addResolvingPromise(layer.load());
});
},
fetchTile(level, row, col, options) {
const tilePromises = this.multiplyLayers.map((layer) =>
layer.fetchTile(level, row, col, options),
);
return Promise.all(tilePromises).then((images) => {
const width = this.tileInfo.size[0];
const height = this.tileInfo.size[1];
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.drawImage(images[0], 0, 0, width, height);
ctx.globalCompositeOperation = "multiply";
for (let i = 1; i < images.length; i++) {
ctx.drawImage(images[i], 0, 0, width, height);
}
return canvas;
});
},
});
CORS: External tile servers must support CORS for canvas-based processing.
Canvas size: Match tile size exactly with tileInfo.size[0] — mismatches cause rendering artifacts.
Memory management: Always clean up WebGL resources (programs, buffers, VAOs, textures) in detach() or destroy().
Abort signals: Always pass options?.signal to esriRequest for proper cancellation.
Coordinate systems: Use displayViewMatrix3 for pixel-aligned rendering, viewMatrix3 for map-coordinate rendering.
RenderNode WebGL version: SceneView uses WebGL 2 — shaders must use #version 300 es.
createSubclass pattern: Use BaseTileLayer.createSubclass({...}) — do not use ES6 class extends.
layers-custom-tilelayer — Custom tinted tile layerlayers-custom-blendlayer — Multi-layer blendinglayers-custom-lerc-2d — LERC data decodinglayers-custom-elevation-exaggerated — Elevation exaggerationlayers-custom-dynamiclayer — Dynamic on-demand renderingcustom-gl-tiles — WebGL tile renderingcustom-gl-visuals — Complex WebGL visualizationscustom-gl-animated-lines — Animated line renderingcustom-render-node-color — RenderNode color post-processingcustom-render-node-dof — Depth of field effectcustom-render-node-windmills — 3D geometry renderingcustom-lv-deckgl — deck.gl integrationarcgis-3d-layers — SceneLayer, PointCloudLayer, VoxelLayerarcgis-scene-environment — Lighting, weather, atmospherearcgis-layers — Standard layer typesarcgis-performance — Rendering optimizationnpx claudepluginhub saschabrunnerch/arcgis-maps-sdk-js-ai-context --plugin arcgis-maps-sdk-js-ai-contextCreate 2D and 3D maps and scenes using ArcGIS Maps SDK for JavaScript. Initialize MapViews, SceneViews, layers, navigation; supports ESM imports, CDN dynamic imports, autocasting, explicit classes.
Loads 3D Tiles tilesets and Mapbox Vector Tiles as runtime 3D Tiles in CesiumJS. Covers async factory methods, LOD options, styling, metadata, voxels, point clouds, clipping planes/polygons, and feature picking.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.