05-ArcGIS For JavaScript-RenderNode后处理效果
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 esin 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 conversionconst fshader = `#version 300 esprecision 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 objectsfunction createShader(gl, src, type) {const shader = gl.createShader(type);gl.shaderSource(shader, src);gl.compileShader(shader);return shader;}// Create and link WebGL program objectfunction 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 codeconsole.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 LuminanceRenderNodeconst LuminanceRenderNode = RenderNode.createSubclass({constructor: function () {// consumes and produces define the location of the the render node in the render pipelinethis.consumes = { required: ["composite-color"] };this.produces = "composite-color";},// Ensure resources are cleaned up when render node is removeddestroy() {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 enabledenabled: {get: function () {return this.produces != null;},set: function (value) {// Setting produces to null disables the render nodethis.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 targetconst input = inputs.find(({ name }) => name === "composite-color");const color = input.getTexture();// Acquire the composite framebuffer object, and bind framebuffer as current targetconst output = this.acquireOutputFramebuffer();const gl = this.gl;// Clear newly acquired framebuffergl.clearColor(0, 0, 0, 1);gl.colorMask(true, true, true, true);gl.clear(gl.COLOR_BUFFER_BIT);// Prepare custom shaders and geometry for screenspace renderingthis.ensureShader(this.gl);this.ensureScreenSpacePass(gl);// Bind custom programgl.useProgram(this.program);// Use composite-color render target to be modified in the shadergl.activeTexture(gl.TEXTURE0);gl.bindTexture(gl.TEXTURE_2D, color.glName);gl.uniform1i(this.textureUniformLocation, 0);// Issue the render call for a screen space render passgl.bindVertexArray(this.vao);gl.drawArrays(gl.TRIANGLES, 0, 3);// use depth from input on output framebufferoutput.attachDepth(input.getAttachment(gl.DEPTH_STENCIL_ATTACHMENT));return output;},program: null,textureUniformLocation: null,positionLocation: null,vao: null,positionBuffer: null,// Setup screen space filling triangleensureScreenSpacePass(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 programsensureShader(gl) {if (this.program != null) {return;}// The vertex shader program// Sets position from 0..1 for fragment shader// Forwards texture coordinates to fragment shaderconst vshader = `#version 300 esin 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 conversionconst fshader = `#version 300 esprecision 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 SceneViewconst luminanceRenderNode = new LuminanceRenderNode({ view });// Toggle button to enable/disable the custom render nodeconst 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 objectIdhighlight = layerView.highlight([objectId]);})})// Create and compile WebGL shader objectsfunction createShader(gl, src, type) {const shader = gl.createShader(type);gl.shaderSource(shader, src);gl.compileShader(shader);return shader;}// Create and link WebGL program objectfunction 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 codeconsole.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 LuminanceRenderNodeconst LuminanceRenderNode = RenderNode.createSubclass({constructor: function () {// consumes and produces define the location of the the render node in the render pipelinethis.consumes = { required: ["composite-color"], optional: ["highlights"] };this.produces = "composite-color";},// Ensure resources are cleaned up when render node is removeddestroy() {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 enabledenabled: {get: function () {return this.produces != null;},set: function (value) {// Setting produces to null disables the render nodethis.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 targetconst 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 targetconst output = this.acquireOutputFramebuffer();const gl = this.gl;// Clear newly acquired framebuffergl.clearColor(0, 0, 0, 1);gl.colorMask(true, true, true, true);gl.clear(gl.COLOR_BUFFER_BIT);// Prepare custom shaders and geometry for screenspace renderingthis.ensureShader(this.gl);this.ensureScreenSpacePass(gl);// Bind custom programgl.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 shadergl.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 passgl.bindVertexArray(this.vao);gl.drawArrays(gl.TRIANGLES, 0, 3);// use depth from input on output framebufferoutput.attachDepth(input.getAttachment(gl.DEPTH_STENCIL_ATTACHMENT));return output;},program: null,textureUniformLocation: null,positionLocation: null,vao: null,positionBuffer: null,// Setup screen space filling triangleensureScreenSpacePass(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 programsensureShader(gl) {if (this.program != null) {return;}// The vertex shader program// Sets position from 0..1 for fragment shader// Forwards texture coordinates to fragment shaderconst vshader = `#version 300 esin 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 conversionconst fshader = `#version 300 esprecision 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 SceneViewconst luminanceRenderNode = new LuminanceRenderNode({ view });// Toggle button to enable/disable the custom render nodeconst 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的要求会比较高,所以需要更多的技术储备,才能更好的去实现后处理的开发。