ArcGIS Maps SDK for JavaScript 从 4.29
开始增加 RenderNode
类,可以添加数据以及操作 FBO(ManagedFBO)
;
通过操作 FBO,可以通过后处理实现很多效果,官方提供了几个示例,感兴趣可以看看。
本文介绍一下通过 FBO,实现自定义范围后处理效果(自定义三角形范围)。
本文(优化版)与之前文章的区别在于:优化版可以支持贴地的效果,实用性更强,详情见示例图和在线示例。
另外,本文以图层范围为例,判断 extent 范围,即矩形范围。
本文包括核心代码、完整代码以及在线示例。
核心代码
首先介绍一下原理:通过获取图层范围数据,获取顶点数据,转为 WebGL 内部坐标,即世界坐标;
片元着色器中,通过深度纹理重建世界坐标。
根据当前 uv 的世界坐标以及图层范围(矩形)顶点数据,判断是否在矩形内,矩形内外显示不同颜色。
#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[4] 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 isPointInRectangle(vec3 A, vec3 B, vec3 C, vec3 D, vec3 P)
{
return SameSide(A, B, C, P) &&
SameSide(B, C, D, P) &&
SameSide(C, D, A, P) &&
SameSide(D, A, B, P);
}
void main() {
vec4 color = texture(colorTex, uv);
// 重建世界坐标
vec4 localPosition = getPositionByDepth(uv);
// 转换三角形顶点数据
vec4 triangle1 = u_viewMatrix * vec4(u_triangle_out[0],1.0);
vec4 triangle2 = u_viewMatrix * vec4(u_triangle_out[1],1.0);
vec4 triangle3 = u_viewMatrix * vec4(u_triangle_out[2],1.0);
vec4 triangle4 = u_viewMatrix * vec4(u_triangle_out[3],1.0);
if (gl_FrontFacing == true){
// 三角形范围
if (!isPointInRectangle(
triangle1.xyz / triangle1.w,
triangle2.xyz / triangle2.w,
triangle3.xyz / triangle3.w,
triangle4.xyz / triangle4.w,
localPosition.xyz
)) {
fragColor = color *= 0.8;
} else {
fragColor = color * 1.5;
fragColor.b *= 2.0;
}
}
}
完整代码
<!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 type="module" src="https://openlayers.vip/arcgis_api/calcite-components/2.8.1/calcite.esm.js"></script>
<link rel="stylesheet" type="text/css" href="https://openlayers.vip/arcgis_api/calcite-components/2.8.1/calcite.css" />
<script src="https://openlayers.vip/examples/resources/renderCommon.js"></script>
<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/geometry/Mesh",
"esri/geometry/Polygon",
"esri/widgets/Home",
"esri/layers/IntegratedMesh3DTilesLayer",
], function (
Map,
SceneView,
RenderNode,
Graphic,
webgl,
SpatialReference,
Mesh,
Polygon,
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);
// 世界坐标
let localOriginRender;
view.when(() => {
// 定位
// 获取图层范围
layer.when(function () {
view.extent = layer.fullExtent;
// 获取图层范围,转为多边形对象
const layerExtent = Mesh.createFromPolygon(Polygon.fromExtent(layer.fullExtent))
// 获取顶点数据
const position = layerExtent.vertexAttributes.position;
// 经纬度坐标转为世界坐标
localOriginRender = webgl.toRenderCoordinates(
view,
position,
0,
SpatialReference.WGS84,
new Float32Array(position.length),
0,
position.length / 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);
// 初始化着色器
this.ensureShader(gl);
// 初始化屏幕数据
this.ensureScreenSpacePass(gl);
// 绑定着色器参数
gl.useProgram(this.shaderProgram);
gl.uniform2fv(this.nearFarUniformLocation, [this.camera.near, this.camera.far]);
// 传入范围顶点数据
gl.uniform3fv(this.textureUniformTriangleExtent, new Float32Array(localOriginRender));
// 激活一号纹理
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);
// 激活相机矩阵
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[4] 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 isPointInRectangle(vec3 A, vec3 B, vec3 C, vec3 D, vec3 P)
{
return SameSide(A, B, C, P) &&
SameSide(B, C, D, P) &&
SameSide(C, D, A, P) &&
SameSide(D, A, B, P);
}
void main() {
vec4 color = texture(colorTex, uv);
// 重建世界坐标
vec4 localPosition = getPositionByDepth(uv);
// 转换三角形顶点数据
vec4 triangle1 = u_viewMatrix * vec4(u_triangle_out[0],1.0);
vec4 triangle2 = u_viewMatrix * vec4(u_triangle_out[1],1.0);
vec4 triangle3 = u_viewMatrix * vec4(u_triangle_out[2],1.0);
vec4 triangle4 = u_viewMatrix * vec4(u_triangle_out[3],1.0);
if (gl_FrontFacing == true){
// 三角形范围
if (!isPointInRectangle(
triangle1.xyz / triangle1.w,
triangle2.xyz / triangle2.w,
triangle3.xyz / triangle3.w,
triangle4.xyz / triangle4.w,
localPosition.xyz
)) {
fragColor = color *= 0.8;
} else {
fragColor = color * 1.5;
fragColor.b *= 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 在线示例:图层范围(Extent)后处理效果