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的要求会比较高,所以需要更多的技术储备,才能更好的去实现后处理的开发。