<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Custom RenderNode - Color modification | Sample | ArcGIS Maps SDK for JavaScript</title>
<!-- Load the ArcGIS Maps SDK for JavaScript from CDN -->
<script type="module" src="https://js.arcgis.com/5.0/"></script>
<arcgis-scene basemap="satellite">
<arcgis-zoom slot="top-left"></arcgis-zoom>
<arcgis-navigation-toggle slot="top-left"></arcgis-navigation-toggle>
<arcgis-compass slot="top-left"></arcgis-compass>
<calcite-block slot="top-right" expanded="" heading="Toggle Render Node" id="renderNodeUI">
<calcite-label layout="inline">
<calcite-switch id="renderNodeToggle" checked=""> </calcite-switch>
const [RenderNode] = await $arcgis.import(["@arcgis/core/views/3d/webgl/RenderNode.js"]);
// Get the reference to the Scene component and wait until ready
const viewElement = document.querySelector("arcgis-scene");
await viewElement.viewOnReady();
// Create and compile WebGL shader objects
function createShader(gl, src, type) {
const shader = gl.createShader(type);
gl.shaderSource(shader, src);
gl.compileShader(shader);
// Create and link WebGL program object
function createProgram(gl, vsSource, fsSource) {
const program = gl.createProgram();
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);
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
// covenience console output to help debugging shader code
console.error(`Failed to link program:
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)}`);
// Derive a new subclass from RenderNode called LuminanceRenderNode
const LuminanceRenderNode = RenderNode.createSubclass({
constructor: function () {
// consumes and produces define the location of 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
this.gl?.deleteProgram(this.program);
if (this.positionBuffer) {
this.gl?.deleteBuffer(this.positionBuffer);
this.gl?.deleteVertexArray(this.vao);
// Define getter and setter for class member enabled
return this.produces != null;
// Setting produces to null disables the render node
this.produces = value ? "composite-color" : null;
// 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();
// 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);
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));
textureUniformLocation: null,
// Setup screen space filling triangle
ensureScreenSpacePass(gl) {
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
if (this.program != null) {
// The vertex shader program
// Sets position from 0..1 for fragment shader
// Forwards texture coordinates to fragment shader
const vshader = `#version 300 es
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
uniform sampler2D colorTex;
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 it to the component's view
const luminanceRenderNode = new LuminanceRenderNode({ view: viewElement.view });
// Toggle button to enable/disable the custom render node
const renderNodeToggle = document.getElementById("renderNodeToggle");
renderNodeToggle.addEventListener("calciteSwitchChange", () => {
luminanceRenderNode.enabled = !luminanceRenderNode.enabled;