mcpField
A
arcgis-custom-rendering

arcgis-custom-rendering

github.com/saschabrunnerch開発2361 回インストール29 週間前に更新
リポジトリ

arcgis-custom-rendering について

ArcGIS Maps SDK for JavaScriptでカスタム描画を行う開発者向けのスキルです。2D・3Dレイヤー、WebGL描画、カスタムタイルソース、SceneViewの後処理を扱います。

主な機能

  1. 01カスタム2D・3Dレイヤーの作成
  2. 02WebGL描画ビューの構築
  3. 03カスタムタイルソースの実装
  4. 04SceneViewの後処理の追加
  5. 05ダイナミック・標高レイヤーの利用

活用シーン

  1. 012Dマップでの独自コンテンツの描画
  2. 02WebGLベースの可視化の構築
  3. 03カスタムタイルデータの処理
  4. 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",
);

カスタムレンダリングの概要

クラス2D3D典型的な用途
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 を設定)
glWebGL 2 コンテキスト(読み取り専用)
viewSceneView への参照
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;
    });
  },
});

よくある落とし穴

  1. CORS:外部のタイルサーバーは、キャンバスベースの処理を行うために CORS をサポートしている必要があります。

  2. キャンバスサイズ:タイルサイズを tileInfo.size[0] と正確に一致させてください。一致しないと、レンダリングに不具合が生じます。

  3. メモリ管理:detach() または destroy() で、WebGL リソース(プログラム、バッファー、VAO、テクスチャ)を必ずクリーンアップしてください。

  4. 中止シグナル:適切にキャンセルできるよう、必ず options?.signal を esriRequest に渡してください。

  5. 座標系:ピクセルに揃えたレンダリングには displayViewMatrix3 を、マップ座標でのレンダリングには viewMatrix3 を使用してください。

  6. RenderNode の WebGL バージョン:SceneView は WebGL 2 を使用するため、シェーダーでは #version 300 es を使用する必要があります。

  7. 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、VoxelLayer
  • arcgis-scene-environment — ライティング、天候、大気
  • arcgis-layers — 標準的なレイヤータイプ
  • arcgis-performance — レンダリングの最適化

コメント