关于 arcgis-custom-rendering
这是一项面向 ArcGIS Maps SDK for JavaScript 开发者的自定义渲染技能,涵盖 2D 和 3D 图层、WebGL 渲染、自定义瓦片源及 SceneView 后期处理。
主要功能
- 01创建自定义 2D 和 3D 图层
- 02构建 WebGL 渲染视图
- 03实现自定义瓦片源
- 04添加 SceneView 后期处理
- 05使用动态与高程图层
适用场景
- 01在 2D 地图中渲染自定义内容
- 02构建基于 WebGL 的可视化
- 03处理自定义瓦片数据
- 04为 SceneView 添加后期效果
安装
复制下面的命令,在终端中运行。
$
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");
},
});
自定义高程图层
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({...}),不要使用 ES6class 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— 渲染优化
评论
支持 Markdown