05-ArcGIS For JavaScript-RenderNode后处理效果

综述

ArcGIS For JavaScript 4.9版本提供了很多优秀的功能,其中提供了RenderNode类,既可以支持第三方渲染引擎的植入,例如webgl、threejs等三维引擎,同时也支持对当前场景中的渲染进行后处理操作。

ArcGIS官网中描述支持可实现的几种后处理操作:混合颜色、深度渲染、高亮及法线操作等后处理。

在这里插入图片描述
今天这里会简单描述下混合颜色和高亮的处理。

代码解析

const LuminanceRenderNode = RenderNode.createSubclass({
  consumes: { required: ["composite-color"] }
  produces: ["composite-color"]
  render(inputs) {
     // custom render code
  }
});

要实现后处理功能,需要通过RenderNode.createSubclass去创建渲染对象。其中主要包括了:

  • consumes: 声明渲染需要引擎的哪些输入。
  • produces: 定义呈现函数产生的输出。

例如,要请求composite-color和法线,函数consume()被指定如下:consume: {required: [“composite-color”, “normals”], optional: [“highlights”]}。

输出总是作为渲染函数的输入之一给出。例如,后处理渲染函数可以声明生成复合色输出: produces: “composite-color”。

代码实现

颜色混合

颜色混合需要做两点改动:

  • 初始化参数设置
	  this.consumes = { required: ["composite-color"] };
      this.produces = "composite-color";
  • shader代码修改,主要是对fragColor颜色值的修改。
            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;

                    void main() {
                        vec4 color = texture(colorTex, uv);
                        fragColor = vec4(vec3(dot(color.rgb, vec3(0.2126, 0.7152, 0.0722))), color.a);
                    }`;

完整代码

<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 modification | Sample | ArcGIS Maps SDK for JavaScript 4.30</title>

    <link rel="stylesheet" href="https://js.arcgis.com/4.30/esri/themes/light/main.css" />
    <script src="https://js.arcgis.com/4.30/"></script>
    <script type="module" src="https://js.arcgis.com/calcite-components/2.8.5/calcite.esm.js"></script>
    <link rel="stylesheet" type="text/css" href="https://js.arcgis.com/calcite-components/2.8.5/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"], function (
        Map,
        SceneView,
        RenderNode
      ) {
        const view = new SceneView({
          container: "viewDiv",
          map: new Map({ basemap: "satellite" })
        });

        // 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: {
            // 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);

            // 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);

            // 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,

          // 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;

                    void main() {
                        vec4 color = texture(colorTex, uv);
                        fragColor = vec4(vec3(dot(color.rgb, vec3(0.2126, 0.7152, 0.0722))), color.a);
                    }`;

            this.program = createProgram(gl, vshader, fshader);
            this.textureUniformLocation = gl.getUniformLocation(this.program, "colorTex");
            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.ui.add("renderNodeUI", "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>
  </body>
</html>

结果

在这里插入图片描述

高亮处理

高亮是设置其实和颜色混合差不多,只是需要在声明consumes的时候,设置optional: [“highlights”] 。

this.consumes = { required: ["composite-color"], optional: ["highlights"] };

完整代码

<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 modification | Sample | ArcGIS Maps SDK for JavaScript 4.30</title>

    <link rel="stylesheet" href="https://js.arcgis.com/4.30/esri/themes/light/main.css" />
    <script src="https://js.arcgis.com/4.30/"></script>
    <script type="module" src="https://js.arcgis.com/calcite-components/2.8.5/calcite.esm.js"></script>
    <link rel="stylesheet" type="text/css" href="https://js.arcgis.com/calcite-components/2.8.5/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/layers/SceneLayer"], function (
                Map,
                SceneView,
                RenderNode,
                SceneLayer
            ) {

            const layer = new SceneLayer({
                url:'https://tiles.arcgis.com/tiles/V6ZHFr6zdgNZuVG0/arcgis/rest/services/campus_buildings/SceneServer'
                // url: "https://tiles.arcgis.com/tiles/z2tnIkrLQ2BRzr6P/arcgis/rest/services/SanFrancisco_Bldgs/SceneServer",
                // outFields:["NAME",""]
            });

            const view = new SceneView({
                container: "viewDiv",
                map: new Map({ basemap: "satellite", 
                // ground: "world-elevation" 
            }),
                highlightOptions: {
                    haloColor: [255, 38, 150],
                    color: [255, 255, 255],
                    fillOpacity: 0.3
                }
            });

            view.map.add(layer);
            view.popupEnabled = false;
            let layerView = null;
            let highlight = null;

            view.when(function () {
                view.extent = layer.fullExtent;

                view.whenLayerView(layer).then((_layerView) => {
                    layerView = _layerView;
                })
            })



            view.on('click', function (event) {
                view.hitTest(event).then(function (response) {
                    let result = response.results[0];
                    if(result == undefined){
                        if(highlight){
                            highlight.remove();
                            highlight = null;
                        }
                        return;
                    }
                    // let objectId = result.graphic.attributes.OBJECTID;
                    let objectId = result.graphic.attributes.OID;

                    if (highlight) {
                        highlight.remove();
                        highlight = null;
                    }
                    // highlight the feature with the returned objectId
                    highlight = layerView.highlight([objectId]);
                })

            })

            // 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"], optional: ["highlights"] };
                    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: {
                    // 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 input1 = inputs.find(({ name }) =>
                        name === "highlights"
                    );

                    if (input1 == undefined) {
                        return;
                    }

                    // const color = input1.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);

                    gl.activeTexture(gl.TEXTURE0);
                    gl.bindTexture(gl.TEXTURE_2D, input?.getTexture().glName);
                    gl.uniform1i(this.textureUniformLocation, 0);

                    // Use composite-color render target to be modified in the shader
                    gl.activeTexture(gl.TEXTURE1);
                    gl.bindTexture(gl.TEXTURE_2D, input1?.getTexture().glName);
                    gl.uniform1i(this.textureUniformLocation, 1);

             

                    // 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,

                // 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;

                    void main() {
                        vec4 color = texture(colorTex, uv);
                        fragColor = vec4(color);
                        // fragColor = vec4(vec3(dot(color.rgb, vec3(0.2126, 0.7152, 0.0722))), color.a);
                    }`;

                    this.program = createProgram(gl, vshader, fshader);
                    this.textureUniformLocation = gl.getUniformLocation(this.program, "colorTex");
                    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.ui.add("renderNodeUI", "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>
</body>

</html>

结果

在这里插入图片描述

结语

对于ArcGIS来说,后处理为Web开发提供了更多的可能性。但是这里对于opengl和shader的要求会比较高,所以需要更多的技术储备,才能更好的去实现后处理的开发。

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值