ArcGIS JSAPI 高级教程 - ArcGIS Maps SDK for JavaScript - 自定义范围后处理效果(优化版)

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 在线示例:自定义范围后处理效果(优化版)

相关推荐
GIS开发特训营1 天前
ArcGIS API for Javascript学习
javascript·学习·arcgis·gis开发·webgis·三维gis
扛着仪器去爬山1 天前
如何利用ArcGIS探究环境和生态因子对水体、土壤和大气污染物的影响?
arcgis·生态学·遥感·土地利用·论文写作·数据处理·生态环境
杨超越luckly1 天前
ArcGIS应用指南:ArcGIS制作局部放大地图
大数据·arcgis·信息可视化·数据挖掘·数据分析
她说—技术支持1 天前
ArcGIS 10.2软件安装包下载及安装教程!
arcgis
Z_W_H_1 天前
【ArcGISPro】使用AI提取要素-土地分类(sentinel2)
arcgis·arcgispro
疯狂学习GIS1 天前
ArcGIS遥感影像重采样操作及不同算法对比
arcgis·rs·遥感数据
WangYan20223 天前
泥石流灾害风险评估与模拟丨AI与R语言、ArcGIS、HECRAS融合,提升泥石流灾害风险预测的精度和准确性
arcgis·chatgpt·遥感技术·地质灾害·泥石流·hecras·风险评价
疯狂学习GIS4 天前
ArcGIS填补面图层的细小空白并删除主体部分外的零散部分
arcgis·gis·学术工作效率·gis数据
sinat_384241094 天前
在有网络连接的机器上打包 electron 及其依赖项,在没有网络连接的机器上安装这些离线包
javascript·arcgis·electron
GIS 数据栈4 天前
每日一书 《基于ArcGIS的Python编程秘笈》
开发语言·python·arcgis