ArcGIS JSAPI 高级教程 - ArcGIS Maps SDK for JavaScript - 自定义范围后处理效果(优化版)
ArcGIS Maps SDK for JavaScript 从 4.29
开始增加 RenderNode
类,可以添加数据以及操作 FBO(ManagedFBO)
;
通过操作 FBO,可以通过后处理实现很多效果,官方提供了几个示例,感兴趣可以看看。
本文介绍一下通过 FBO,实现自定义范围后处理效果(自定义三角形范围)。
本文(优化版)与之前文章的区别在于:优化版可以支持贴地的效果,实用性更强,详情见示例图和在线示例。
另外,本文以三角形范围为例,可以在此基础上扩展为多边形范围。
本文包括核心代码、完整代码以及在线示例。
核心代码
首先介绍一下原理:通过地图构建三角形数据,转为 WebGL 内部坐标,即世界坐标;
片元着色器中,通过深度纹理重建世界坐标。
根据当前 uv 的世界坐标以及三角形顶点数据,判断是否在三角形内,三角形内外显示不同颜色。
c
#version 300 es
precision mediump float;
out mediump vec4 fragColor;
in vec2 uv;
// 颜色纹理
uniform sampler2D colorTex;
// 渲染深度纹理
uniform sampler2D depthTex;
// 相机矩阵
uniform mat4 u_viewMatrix;
uniform mat4 u_projectionMatrix;
uniform mat4 u_inverseProjectionMatrix;
// 相机远近点
uniform vec2 nearFar;
// 三角形顶点
uniform vec3[3] u_triangle_out;
// 线性化深度
float linearizeDepth(float depth) {
float depthNdc = depth * 2.0 - 1.0;
return (2.0 * nearFar[0] * nearFar[1]) / (depthNdc * (nearFar[1] - nearFar[0]) - (nearFar[1] + nearFar[0]));
}
// 获取深度值
float linearDepth(vec2 uv) {
ivec2 iuv = ivec2(uv * vec2(textureSize(depthTex, 0)));
return texelFetch(depthTex, iuv, 0).r;
}
// 深度值获取坐标
vec4 getPositionByDepth(vec2 uv) {
// 获取深度值
float depth = linearDepth(uv);
// 将深度值转换为视图空间中的Z值
// 这通常涉及到将非线性深度值转换为线性深度值
float viewZ = linearizeDepth(depth);
// 计算裁剪空间中的W值
// 这通常用于从NDC(标准化设备坐标)转换为裁剪坐标
float clipW = u_projectionMatrix[2][3] * viewZ + u_projectionMatrix[3][3];
// 将纹理坐标和深度值转换为NDC坐标
// NDC坐标范围是[-1, 1]
vec3 ndcPosition = vec3(uv, depth) * 2.0 - 1.0;
// 将NDC坐标转换为裁剪坐标
// 通过乘以裁剪空间中的W值来实现
vec4 clipPosition = vec4(ndcPosition, 1.0) * clipW;
// 将裁剪坐标变换回视图坐标
// 通过乘以投影矩阵的逆矩阵来实现
vec4 viewPos = u_inverseProjectionMatrix * clipPosition;
// 进行透视除法,将视图坐标转换为齐次坐标
viewPos /= viewPos.w;
// 返回视图空间中的位置
return viewPos;
}
// 判断两个向量是否指向同一方向
bool SameSide(vec3 A, vec3 B, vec3 C, vec3 P)
{
vec3 AB = B - A;
vec3 AC = C - A;
vec3 AP = P - A;
vec3 v1 = cross(AB, AC);
vec3 v2 = cross(AB, AP);
// Normalize the cross products to ensure consistent direction
v1 = normalize(v1);
v2 = normalize(v2);
// V1和v2应该指向同一个方向
return dot(v1, v2) >= 0.0;
}
// 判断点在三角形内
bool isPointInTriangle(vec3 A, vec3 B, vec3 C, vec3 P)
{
return SameSide(A, B, C, P) &&
SameSide(B, C, A, P) &&
SameSide(C, A, B, P);
}
void main() {
vec4 color = texture(colorTex, uv);
// 重建世界坐标
vec4 localPosition = getPositionByDepth(uv);
// 转换三角形顶点数据
vec4 temp1 = u_viewMatrix * vec4(u_triangle_out[0], 1.0);
vec4 temp2 = u_viewMatrix * vec4(u_triangle_out[1], 1.0);
vec4 temp3 = u_viewMatrix * vec4(u_triangle_out[2], 1.0);
if (gl_FrontFacing == true){
// 三角形范围
if (!isPointInTriangle(
temp1.xyz / temp1.w,
temp2.xyz / temp2.w,
temp3.xyz / temp3.w,
localPosition.xyz
)) {
fragColor = color;
} else {
fragColor = color * 2.0;
}
}
}
完整代码
html
<!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 - 自定义范围后处理(优化版) | Sample | ArcGIS Maps SDK for JavaScript 4.29</title>
<link rel="stylesheet" href="https://openlayers.vip/arcgis_api/4.30/esri/themes/light/main.css"/>
<script src="https://openlayers.vip/arcgis_api/4.30/init.js"></script>
<script src="https://openlayers.vip/examples/resources/renderCommon.js"></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"/>
<script>
var _hmt = _hmt || [];
(function () {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?f80a36f14f8a73bb0f82e0fdbcee3058";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
<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/Graphic", "esri/views/3d/webgl",
"esri/geometry/SpatialReference",
"esri/widgets/Home",
"esri/layers/IntegratedMesh3DTilesLayer",
], function (
Map,
SceneView,
RenderNode,
Graphic,
webgl,
SpatialReference,
Home,
IntegratedMesh3DTilesLayer,
) {
const {map, view} = initMap({Map, SceneView, Home});
// 3dtile 数据
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);
// 创建多边形
const polygon = {
type: "polygon", // autocasts as new Polygon()
hasZ: false,
rings: [
[114.17572049692221, 22.29277650792162, 0],
[114.1723157633122, 22.299970323140712, 0],
[114.1658997800559, 22.29321842654308, 0],
[114.17572049692221, 22.29277650792162, 0]
]
};
const fillSymbol = {
type: "simple-fill", // autocasts as new SimpleFillSymbol()
color: [227, 139, 79, 0.8],
outline: {
// autocasts as new SimpleLineSymbol()
color: [255, 255, 255],
width: 1
}
};
// Add the geometry and symbol to a new graphic
const polygonGraphic = new Graphic({
geometry: polygon,
visible: false,
symbol: fillSymbol
});
// Add the graphics to the view's graphics layer
view.graphics.addMany([polygonGraphic]);
// 获取多边形坐标
const points = polygonGraphic.geometry.rings[0].flat();
// 世界坐标
let localOriginRender;
view.when(() => {
// 定位
layer.when(function () {
view.extent = layer.fullExtent;
});
// 经纬度坐标转为世界坐标
localOriginRender = webgl.toRenderCoordinates(
view,
points,
0,
SpatialReference.WGS84,
new Float32Array(points.length - 3),
0,
(points.length - 3) / 3,
);
// 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() {
this.shaderProgram && this.gl?.deleteProgram(this.shaderProgram);
this.positionBuffer && this.gl?.deleteBuffer(this.positionBuffer);
this.vao && this.gl?.deleteVertexArray(this.vao);
},
properties: {
// 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;
const depth = input.getTexture(gl.DEPTH_STENCIL_ATTACHMENT);
// Clear newly acquired framebuffer
gl.clearColor(0, 0, 0, 1);
gl.colorMask(true, true, true, true);
gl.clear(gl.COLOR_BUFFER_BIT);
// 激活透明
activeOpacity(gl);
// 初始化着色器
this.ensureShader(gl);
// 初始化屏幕数据
this.ensureScreenSpacePass(gl);
// 绑定着色器参数
gl.useProgram(this.shaderProgram);
gl.uniform2fv(this.nearFarUniformLocation, [this.camera.near, this.camera.far]);
// 激活一号纹理
gl.activeTexture(gl.TEXTURE0);
// 绑定一号纹理
gl.bindTexture(gl.TEXTURE_2D, color.glName);
// 传入着色器
gl.uniform1i(this.textureUniformLocation, 0);
// 激活三号纹理
// 绑定深度纹理
gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, depth.glName);
gl.uniform1i(this.depthTexUniformLocation, 2);
// 传入三角形顶点
gl.uniform3fv(this.textureUniformTriangleExtent,
new Float32Array(localOriginRender));
// 激活相机矩阵
activeMatrix(this);
// 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));
this.requestRender();
return output;
},
// 着色器程序
shaderProgram: null,
// 纹理
textureUniformLocation: null,
// 顶点位置
positionLocation: null,
// 顶点数组
vao: null,
// 顶点缓冲区
positionBuffer: null,
// 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.shaderProgram != null) {
return;
}
// The vertex shader program
const vshader = `#version 300 es
// 绘制顶点
in vec2 position;
// uv 取样
out vec2 uv;
void main() {
// 绘制顶点
gl_Position = vec4(position, 0.0, 1.0);
// uv 调整中心
uv = position * 0.5 + vec2(0.5);
}
`;
// The fragment shader program applying a greyscsale conversion
const fshader = `#version 300 es
precision mediump float;
out mediump vec4 fragColor;
in vec2 uv;
// 颜色纹理
uniform sampler2D colorTex;
// 渲染深度纹理
uniform sampler2D depthTex;
// 相机矩阵
uniform mat4 u_viewMatrix;
uniform mat4 u_projectionMatrix;
uniform mat4 u_inverseProjectionMatrix;
// 相机远近点
uniform vec2 nearFar;
// 三角形顶点
uniform vec3[3] u_triangle_out;
// 线性化深度
float linearizeDepth(float depth) {
float depthNdc = depth * 2.0 - 1.0;
return (2.0 * nearFar[0] * nearFar[1]) / (depthNdc * (nearFar[1] - nearFar[0]) - (nearFar[1] + nearFar[0]));
}
// 获取深度值
float linearDepth(vec2 uv) {
ivec2 iuv = ivec2(uv * vec2(textureSize(depthTex, 0)));
return texelFetch(depthTex, iuv, 0).r;
}
// 深度值获取坐标
vec4 getPositionByDepth(vec2 uv) {
// 获取深度值
float depth = linearDepth(uv);
// 将深度值转换为视图空间中的Z值
// 这通常涉及到将非线性深度值转换为线性深度值
float viewZ = linearizeDepth(depth);
// 计算裁剪空间中的W值
// 这通常用于从NDC(标准化设备坐标)转换为裁剪坐标
float clipW = u_projectionMatrix[2][3] * viewZ + u_projectionMatrix[3][3];
// 将纹理坐标和深度值转换为NDC坐标
// NDC坐标范围是[-1, 1]
vec3 ndcPosition = vec3(uv, depth) * 2.0 - 1.0;
// 将NDC坐标转换为裁剪坐标
// 通过乘以裁剪空间中的W值来实现
vec4 clipPosition = vec4(ndcPosition, 1.0) * clipW;
// 将裁剪坐标变换回视图坐标
// 通过乘以投影矩阵的逆矩阵来实现
vec4 viewPos = u_inverseProjectionMatrix * clipPosition;
// 进行透视除法,将视图坐标转换为齐次坐标
viewPos /= viewPos.w;
// 返回视图空间中的位置
return viewPos;
}
// 判断两个向量是否指向同一方向
bool SameSide(vec3 A, vec3 B, vec3 C, vec3 P)
{
vec3 AB = B - A;
vec3 AC = C - A;
vec3 AP = P - A;
vec3 v1 = cross(AB, AC);
vec3 v2 = cross(AB, AP);
// Normalize the cross products to ensure consistent direction
v1 = normalize(v1);
v2 = normalize(v2);
// V1和v2应该指向同一个方向
return dot(v1, v2) >= 0.0;
}
// 判断点在三角形内
bool isPointInTriangle(vec3 A, vec3 B, vec3 C, vec3 P)
{
return SameSide(A, B, C, P) &&
SameSide(B, C, A, P) &&
SameSide(C, A, B, P);
}
void main() {
vec4 color = texture(colorTex, uv);
// 重建世界坐标
vec4 localPosition = getPositionByDepth(uv);
// 转换三角形顶点数据
vec4 temp1 = u_viewMatrix * vec4(u_triangle_out[0], 1.0);
vec4 temp2 = u_viewMatrix * vec4(u_triangle_out[1], 1.0);
vec4 temp3 = u_viewMatrix * vec4(u_triangle_out[2], 1.0);
if (gl_FrontFacing == true){
// 三角形范围
if (!isPointInTriangle(
temp1.xyz / temp1.w,
temp2.xyz / temp2.w,
temp3.xyz / temp3.w,
localPosition.xyz
)) {
fragColor = color;
} else {
fragColor = color * 2.0;
}
}
}
`;
this.shaderProgram = initWebgl2Shaders(gl, vshader, fshader);
this.textureUniformLocation = gl.getUniformLocation(this.shaderProgram, "colorTex");
this.depthTexUniformLocation = gl.getUniformLocation(this.shaderProgram, "depthTex");
this.nearFarUniformLocation = gl.getUniformLocation(this.shaderProgram, "nearFar");
// 三角形顶点位置
this.textureUniformTriangleExtent = gl.getUniformLocation(this.shaderProgram, "u_triangle_out");
this.positionLocation = gl.getAttribLocation(this.shaderProgram, "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;
});
});
});
</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>
</body>
</html>
在线示例
ArcGIS Maps SDK for JavaScript 在线示例:自定义范围后处理效果(优化版)