ArcGIS JSAPI 高级教程 - ArcGIS Maps SDK for JavaScript - 探测效果(地图探测、地图窥探)
- 核心代码
- 完整代码:
- 在线示例
ArcGIS Maps SDK for JavaScript 从 4.29
开始增加 RenderNode
类,可以添加数据以及操作 FBO(ManagedFBO)
;
通过操作 FBO,可以通过后处理实现很多效果,官方提供了几个示例,感兴趣可以看看。
本文介绍一下通过 FBO,实现鼠标探测效果。
本文包括核心代码、完整代码以及在线示例。
核心代码
原理也比较容易,即获取鼠标位置,转为 WebGL 位置,计算圆形范围,
经过判断,范围内外显示不同颜色。
需要注意的地方:鼠标位置归一化,y 轴位置翻转以及纠正圆形。
具体介绍详见代码注释。
// 监听鼠标离开图形事件
view.on("pointer-move", function (event) {
// 获取视口尺寸
const viewWidth = view.width;
const viewHeight = view.height;
// 计算中心点,这里归一化位置
const centerX = event.x / viewWidth;
// 翻转 y 轴
const centerY = 1 - event.y / viewHeight;
luminanceRenderNode.center = [centerX,centerY];
});
// The fragment shader program applying a greyscsale conversion
const fshader = `#version 300 es
precision highp float;
out lowp vec4 fragColor;
in vec2 uv;
uniform sampler2D colorTex;
// 圆参数
uniform vec3 u_center;
// 计算宽高比,纠正圆形
vec2 calculateAspectRatio(vec2 size) {
return vec2(size[1] / size[0], 1.0);
}
// 是否在圆内
bool isInsideCircle(vec2 uv_) {
// 纹理尺寸
vec2 size = vec2(textureSize(colorTex, 0));
// 纠正范围
uv_ = (uv_ - u_center.rg)/calculateAspectRatio(size);
// 计算UV坐标到圆心的距离
float distance = length(uv_);
// 判断距离是否小于圆的半径
// 这里给一个最小圆形
return distance < (u_center.b >= 0.3 ? 0.3: u_center.b);
}
void main() {
vec4 color = texture(colorTex, uv);
if(isInsideCircle(uv)){
// 圆内高亮
fragColor = vec4(color.rgb * 1.2, color.a);
}else{
// 灰度化
fragColor = vec4(vec3(dot(color.rgb, vec3(0.2126, 0.7152, 0.0722)))*0.7, color.a);
}
}`;
完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no"/>
<title>Custom RenderNode - Color Spy | Sample | ArcGIS Maps SDK for JavaScript 4.29</title>
<link rel="stylesheet" href="https://js.arcgis.com/4.29/esri/themes/light/main.css"/>
<script src="https://js.arcgis.com/4.29/"></script>
<script type="module" src="https://js.arcgis.com/calcite-components/2.5.1/calcite.esm.js"></script>
<link rel="stylesheet" type="text/css" href="https://js.arcgis.com/calcite-components/2.5.1/calcite.css"/>
<style>
html,
body,
#viewDiv {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
}
</style>
<script>
require(["esri/Map", "esri/views/SceneView", "esri/views/3d/webgl/RenderNode",
"esri/widgets/Slider","esri/geometry/Point",
"esri/layers/IntegratedMesh3DTilesLayer",
], function (
Map,
SceneView,
RenderNode,
Slider,
Point,
IntegratedMesh3DTilesLayer,
) {
const view = new SceneView({
container: "viewDiv",
map: new Map({basemap: "satellite"})
});
const layer = new IntegratedMesh3DTilesLayer({
url: "http://openlayers.vip/cesium/3dtile/xianggang_1.1/tileset.json",
title: "Utrecht Integrated Mesh 3D Tiles"
});
view.map.add(layer);
view.when(() => {
layer.when(function () {
view.extent = layer.fullExtent;
});
});
// Create and compile WebGL shader objects
function createShader(gl, src, type) {
const shader = gl.createShader(type);
gl.shaderSource(shader, src);
gl.compileShader(shader);
return shader;
}
// Create and link WebGL program object
function createProgram(gl, vsSource, fsSource) {
const program = gl.createProgram();
if (!program) {
console.error("Failed to create program");
}
const vertexShader = createShader(gl, vsSource, gl.VERTEX_SHADER);
const fragmentShader = createShader(gl, fsSource, gl.FRAGMENT_SHADER);
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!success) {
// covenience console output to help debugging shader code
console.error(`Failed to link program:
error ${gl.getError()},
info log: ${gl.getProgramInfoLog(program)},
vertex: ${gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)},
fragment: ${gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)}
vertex info log: ${gl.getShaderInfoLog(vertexShader)},
fragment info log: ${gl.getShaderInfoLog(fragmentShader)}`);
}
return program;
}
// Derive a new subclass from RenderNode called LuminanceRenderNode
const LuminanceRenderNode = RenderNode.createSubclass({
constructor: function () {
// consumes and produces define the location of the the render node in the render pipeline
this.consumes = {required: ["composite-color"]};
this.produces = "composite-color";
},
// Ensure resources are cleaned up when render node is removed
destroy() {
if (this.program) {
this.gl?.deleteProgram(this.program);
}
if (this.positionBuffer) {
this.gl?.deleteBuffer(this.positionBuffer);
}
if (this.vao) {
this.gl?.deleteVertexArray(this.vao);
}
},
properties: {
// 修改中心点
center: {
set: function (value) {
// Setting produces to null disables the render node
this.viewCenter[0] = value[0];
this.viewCenter[1] = value[1];
this.requestRender();
}
},
// 修改半径
radius: {
set: function (value = 0.1) {
// Setting produces to null disables the render node
this.viewCenter[2] = value;
this.requestRender();
}
},
// Define getter and setter for class member enabled
enabled: {
get: function () {
return this.produces != null;
},
set: function (value) {
// Setting produces to null disables the render node
this.produces = value ? "composite-color" : null;
this.requestRender();
}
}
},
render(inputs) {
// The field input contains all available framebuffer objects
// We need color texture from the composite render target
const input = inputs.find(({name}) => name === "composite-color");
const color = input.getTexture();
// Acquire the composite framebuffer object, and bind framebuffer as current target
const output = this.acquireOutputFramebuffer();
const gl = this.gl;
// Clear newly acquired framebuffer
gl.clearColor(0, 0, 0, 1);
gl.colorMask(true, true, true, true);
gl.clear(gl.COLOR_BUFFER_BIT);
// Prepare custom shaders and geometry for screenspace rendering
this.ensureShader(this.gl);
this.ensureScreenSpacePass(gl);
// Bind custom program
gl.useProgram(this.program);
// console.log(color.glName)
// Use composite-color render target to be modified in the shader
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, color.glName);
gl.uniform1i(this.textureUniformLocation, 0);
gl.uniform3fv(this.textureUniformCenter, this.viewCenter);
// Issue the render call for a screen space render pass
gl.bindVertexArray(this.vao);
gl.drawArrays(gl.TRIANGLES, 0, 3);
// use depth from input on output framebuffer
output.attachDepth(input.getAttachment(gl.DEPTH_STENCIL_ATTACHMENT));
return output;
},
program: null,
textureUniformLocation: null,
positionLocation: null,
vao: null,
positionBuffer: null,
// 默认圆参数
viewCenter: new Float32Array([0,0,0.1]),
// Setup screen space filling triangle
ensureScreenSpacePass(gl) {
if (this.vao) {
return;
}
this.vao = gl.createVertexArray();
gl.bindVertexArray(this.vao);
this.positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
const vertices = new Float32Array([-1.0, -1.0, 3.0, -1.0, -1.0, 3.0]);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(this.positionLocation);
gl.bindVertexArray(null);
},
// Setup custom shader programs
ensureShader(gl) {
if (this.program != null) {
return;
}
// The vertex shader program
// Sets position from 0..1 for fragment shader
// Forwards texture coordinates to fragment shader
const vshader = `#version 300 es
in vec2 position;
out vec2 uv;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
uv = position * 0.5 + vec2(0.5);
}`;
// The fragment shader program applying a greyscsale conversion
const fshader = `#version 300 es
precision highp float;
out lowp vec4 fragColor;
in vec2 uv;
uniform sampler2D colorTex;
// 圆参数
uniform vec3 u_center;
// 计算宽高比,纠正圆形
vec2 calculateAspectRatio(vec2 size) {
return vec2(size[1] / size[0], 1.0);
}
// 是否在圆内
bool isInsideCircle(vec2 uv_) {
// 纹理尺寸
vec2 size = vec2(textureSize(colorTex, 0));
// 纠正范围
uv_ = (uv_ - u_center.rg)/calculateAspectRatio(size);
// 计算UV坐标到圆心的距离
float distance = length(uv_);
// 判断距离是否小于圆的半径
return distance < (u_center.b >= 0.3 ? 0.3: u_center.b);
}
void main() {
vec4 color = texture(colorTex, uv);
if(isInsideCircle(uv)){
// 圆内高亮
fragColor = vec4(color.rgb * 1.2, color.a);
}else{
fragColor = vec4(vec3(dot(color.rgb, vec3(0.2126, 0.7152, 0.0722)))*0.7, color.a);
}
}`;
this.program = createProgram(gl, vshader, fshader);
this.textureUniformLocation = gl.getUniformLocation(this.program, "colorTex");
this.textureUniformCenter = gl.getUniformLocation(this.program, "u_center");
this.positionLocation = gl.getAttribLocation(this.program, "position");
}
});
// Initializes the new custom render node and connects to SceneView
const luminanceRenderNode = new LuminanceRenderNode({view});
// Toggle button to enable/disable the custom render node
const renderNodeToggle = document.getElementById("renderNodeToggle");
renderNodeToggle.addEventListener("calciteSwitchChange", () => {
luminanceRenderNode.enabled = !luminanceRenderNode.enabled;
});
// 监听鼠标离开图形事件
view.on("pointer-move", function (event) {
// 获取视口尺寸
const viewWidth = view.width;
const viewHeight = view.height;
// 计算中心点
const centerX = event.x / viewWidth;
const centerY = 1 - event.y / viewHeight;
luminanceRenderNode.center = [centerX,centerY];
});
const slider = new Slider({
container: "sliderDiv",
min: 0,
max: 0.3,
values: [ 0.1 ],
snapOnClickEnabled: false,
visibleElements: {
labels: false,
rangeLabels: true
}
});
slider.on("thumb-drag", (event) => {
// 修改半径
luminanceRenderNode.radius = slider.values[0];
});
view.ui.add("renderNodeUI", "top-right");
view.ui.add(slider, {
position: "top-right"
});
});
</script>
</head>
<body>
<calcite-block open heading="Toggle Render Node" id="renderNodeUI">
<calcite-label layout="inline">
Color
<calcite-switch id="renderNodeToggle" checked></calcite-switch>
Grayscale
</calcite-label>
</calcite-block>
<div id="viewDiv">
</div>
<div id="sliderDiv"></div>
</body>
</html>
在线示例
ArcGIS Maps SDK for JavaScript 在线示例:探测效果(地图探测)