arcgis-custom-rendering について
ArcGIS Maps SDK for JavaScriptでカスタム描画を行う開発者向けのスキルです。2D・3Dレイヤー、WebGL描画、カスタムタイルソース、SceneViewの後処理を扱います。
主な機能
- 01カスタム2D・3Dレイヤーの作成
- 02WebGL描画ビューの構築
- 03カスタムタイルソースの実装
- 04SceneViewの後処理の追加
- 05ダイナミック・標高レイヤーの利用
活用シーン
- 012Dマップでの独自コンテンツの描画
- 02WebGLベースの可視化の構築
- 03カスタムタイルデータの処理
- 04SceneViewへの後処理効果の追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ saschabrunnerch/ arcgis-maps-sdk-js-ai-context --skill arcgis-custom-renderingインストール後、Claude Code で/arcgis-custom-renderingを入力して読み込まれたか確認します。
詳細
ArcGIS カスタムレンダリング
カスタムレイヤー、2D/3D の WebGL レンダリング、カスタムタイルソース、SceneView のポストプロセスエフェクトを作成する際に、このスキルを使用します。
インポートパターン
ESM(npm)
// 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";
CDN(動的インポート)
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",
);
カスタムレンダリングの概要
| クラス | 2D | 3D | 典型的な用途 |
|---|---|---|---|
RenderNode | — | はい | SceneView のポストプロセスエフェクト |
BaseLayerView2D | はい | — | カスタム 2D キャンバスレンダリング |
BaseLayerViewGL2D | はい | — | WebGL アクセラレーションによる 2D レンダリング |
BaseTileLayer | はい | はい | カスタムタイルソースと処理 |
BaseDynamicLayer | はい | はい | オンデマンドの動的レンダリング |
BaseElevationLayer | — | はい | カスタム標高/地形データ |
RenderNode(3D ポストプロセス)
RenderNode は、SceneView の WebGL 2 パイプラインにカスタムレンダリングパスを作成します。
基本的な RenderNode
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 });
});
RenderNode の主要な概念
| プロパティ/メソッド | 説明 |
|---|---|
consumes | このノードが入力として必要とするレンダーターゲット(例:{ required: ["composite-color"] }) |
produces | 生成するレンダーターゲットの名前(無効にするには null を設定) |
gl | WebGL 2 コンテキスト(読み取り専用) |
view | SceneView への参照 |
render(inputs) | レンダリングロジックを実装するためにオーバーライド |
acquireOutputFramebuffer() | 描画先のフレームバッファーを取得 |
requestRender() | 新しいフレームを要求 |
RenderNode の有効化/無効化
const MyRenderNode = RenderNode.createSubclass({
properties: {
enabled: {
get() {
return this.produces != null;
},
set(value) {
this.produces = value ? "composite-color" : null;
this.requestRender();
},
},
},
});
GLSL ES 3.0 フラグメントシェーダーの例
#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);
}
カスタム TileLayer
基本的なカスタム TileLayer
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);
BaseTileLayer の主要なメソッド
| メソッド | 説明 |
|---|---|
getTileUrl(level, row, col) | タイルの URL 文字列を返す |
fetchTile(level, row, col, options) | タイルを取得し、必要に応じて処理する。キャンバスまたは画像を返す |
load() | レイヤーの読み込み時に呼び出される |
refresh() | すべてのタイルを再読み込みする |
プロパティの変更を監視する
const CustomLayer = BaseTileLayer.createSubclass({
properties: {
customProperty: null,
},
initialize() {
reactiveUtils.watch(
() => this.customProperty,
() => this.refresh(),
);
},
});
カスタム DynamicLayer
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");
},
});
カスタム Elevation Layer
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;
});
},
});
WebGL カスタム 2D レンダリング(BaseLayerViewGL2D)
WebGL を使用したカスタム LayerView
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);
},
});
createLayerView を使用したカスタムレイヤー
const CustomLayer = Layer.createSubclass({
createLayerView(view) {
if (view.type === "2d") {
return new CustomGLLayerView({ view, layer: this });
}
},
});
BaseLayerView2D(キャンバスベース)
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
},
});
LayerView のライフサイクル
| メソッド | 説明 |
|---|---|
attach() | LayerView がビューに追加されたときに呼び出される — リソースを初期化 |
render(renderParameters) | コンテンツを描画するために各フレームで呼び出される |
detach() | LayerView がビューから削除されたときに呼び出される — リソースをクリーンアップ |
requestRender() | 次のフレームで再レンダリングを要求する |
LERC のデコード
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:外部のタイルサーバーは、キャンバスベースの処理を行うために CORS をサポートしている必要があります。
-
キャンバスサイズ:タイルサイズを
tileInfo.size[0]と正確に一致させてください。一致しないと、レンダリングに不具合が生じます。 -
メモリ管理:
detach()またはdestroy()で、WebGL リソース(プログラム、バッファー、VAO、テクスチャ)を必ずクリーンアップしてください。 -
中止シグナル:適切にキャンセルできるよう、必ず
options?.signalをesriRequestに渡してください。 -
座標系:ピクセルに揃えたレンダリングには
displayViewMatrix3を、マップ座標でのレンダリングにはviewMatrix3を使用してください。 -
RenderNode の WebGL バージョン:SceneView は WebGL 2 を使用するため、シェーダーでは
#version 300 esを使用する必要があります。 -
createSubclass パターン:
BaseTileLayer.createSubclass({...})を使用してください。ES6 のclass extendsは使用しないでください。
リファレンスサンプル
layers-custom-tilelayer— カスタム色調のタイルレイヤーlayers-custom-blendlayer— 複数レイヤーのブレンドlayers-custom-lerc-2d— LERC データのデコードlayers-custom-elevation-exaggerated— 標高の強調layers-custom-dynamiclayer— オンデマンドの動的レンダリングcustom-gl-tiles— WebGL タイルのレンダリングcustom-gl-visuals— 複雑な WebGL ビジュアライゼーションcustom-gl-animated-lines— アニメーションする線のレンダリングcustom-render-node-color— RenderNode のカラー後処理custom-render-node-dof— 被写界深度エフェクトcustom-render-node-windmills— 3D ジオメトリのレンダリングcustom-lv-deckgl— deck.gl との統合
関連スキル
arcgis-3d-layers— SceneLayer、PointCloudLayer、VoxelLayerarcgis-scene-environment— ライティング、天候、大気arcgis-layers— 標準的なレイヤータイプarcgis-performance— レンダリングの最適化