<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Custom RenderNode - Depth of field | 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" ground="world-elevation">
<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 expanded label="render node UI" id="renderNodeUI">
<div><em>Click in scene to set focus distance</em></div>
<calcite-label layout="inline">
<calcite-slider id="focus-distance" min="0" max="5000" label-handles> </calcite-slider>
<calcite-label layout="inline">
<calcite-slider id="aperture" min="0" max="5" step="0.1" label-handles> </calcite-slider>
const [IntegratedMeshLayer, RenderNode, webgl, SpatialReference] = await $arcgis.import([
"@arcgis/core/layers/IntegratedMeshLayer.js",
"@arcgis/core/views/3d/webgl/RenderNode.js",
"@arcgis/core/views/3d/webgl.js",
"@arcgis/core/geometry/SpatialReference.js",
// get the scene component and wait until ready
const viewElement = document.querySelector("arcgis-scene");
await viewElement.viewOnReady();
latitude: 48.13498888794968,
longitude: 11.568034615845615,
viewElement.environment = {
directShadowsEnabled: true,
// adding Munich integrated mesh layer to map
const meshLayer = new IntegratedMeshLayer({
url: "https://tiles-eu1.arcgis.com/7cCya5lpv5CmFJHv/arcgis/rest/services/Munich_3D_Mesh_City_Mapper_2_SURE_43/SceneServer",
viewElement.map.add(meshLayer);
// user-controllable parameters
function createShader(gl, src, type) {
const shader = gl.createShader(type);
gl.shaderSource(shader, src);
gl.compileShader(shader);
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);
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)}`);
// Create a new custom render node class by subclassing from RenderNode
const DepthOfFieldRenderNode = 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";
const input = inputs.find(({ name }) => name === "composite-color");
// Get color and depth texture of current target
const color = input.getTexture();
const depth = input.getTexture(this.gl.DEPTH_STENCIL_ATTACHMENT);
const output = this.acquireOutputFramebuffer();
gl.clearColor(0, 0, 0, 1);
gl.colorMask(true, true, true, true);
gl.clear(gl.COLOR_BUFFER_BIT);
this.ensureShader(this.gl);
this.ensureScreenSpacePass(gl);
gl.useProgram(this.program);
// Bind color and depth textures for access in shader programs
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, color.glName);
gl.uniform1i(this.textureUniformLocation, 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, depth.glName);
gl.uniform1i(this.depthTextureUniformLocation, 1);
gl.activeTexture(gl.TEXTURE0);
// Set up gl uniforms for access in shader programs
gl.uniform1f(this.focusUniformLocation, focus);
gl.uniform1f(this.apertureUniformLocation, aperture * 0.00001);
gl.uniform2fv(this.nearFarUniformLocation, [this.camera.near, this.camera.far]);
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,
depthTextureUniformLocation: null,
focusUniformLocation: null,
apertureUniformLocation: null,
nearFarUniformLocation: null,
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);
if (this.program != null) {
const vshader = `#version 300 es
gl_Position = vec4(position, 0.0, 1.0);
uv = position * 0.5 + vec2(0.5);
// Fragment shader program
const fshader = `#version 300 es
uniform sampler2D colorTex;
uniform sampler2D depthTex;
const float maxBlur = 0.015;
// sample points that we use for image blurring
const vec2 samplePoints[41] = vec2[41](
// noise function used to smooth the edges of the blur
float x = (fract(sin(dot(gl_FragCoord.xy ,vec2(12.9898,78.233) * 0.143)) * 43758.5453) - 0.5) / 2.0;
float y = (fract(sin(dot(gl_FragCoord.xy ,vec2(12.9898,78.233) * 0.219)) * 43758.5453) - 0.5) / 2.0;
// convert depth value [0, 1] to [near, far] values
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]));
// read depth texture and calculate depth value
float linearDepth(vec2 uv) {
ivec2 iuv = ivec2(uv * vec2(textureSize(depthTex, 0)));
float depth = texelFetch(depthTex, iuv, 0).r;
return linearizeDepth(depth);
vec2 size = vec2(textureSize(colorTex, 0));
vec2 aspectCorrection = vec2(1.0, size.x / size.y);
float viewZ = linearDepth(uv);
float factor = (focus + viewZ); // viewZ is <= 0, so this is a difference equation
vec2 blur = vec2(clamp(factor * aperture, -maxBlur, maxBlur));
for (int i = 0; i < 41; i++) {
color += pow(texture(colorTex, uv.xy + ((samplePoints[i] + noise) * aspectCorrection) * blur), vec4(2.2));
fragColor = pow(color / 41.0, vec4(1./2.2));
this.program = createProgram(gl, vshader, fshader);
this.textureUniformLocation = gl.getUniformLocation(this.program, "colorTex");
this.depthTextureUniformLocation = gl.getUniformLocation(this.program, "depthTex");
this.positionLocation = gl.getAttribLocation(this.program, "position");
this.focusUniformLocation = gl.getUniformLocation(this.program, "focus");
this.apertureUniformLocation = gl.getUniformLocation(this.program, "aperture");
this.nearFarUniformLocation = gl.getUniformLocation(this.program, "nearFar");
// Initializes the new custom render node and connects it to the component's view
const dofRenderNode = new DepthOfFieldRenderNode({ view: viewElement.view });
// Calculates distance from map point to camera in render coordinates
function distanceInRenderCoordinates(mapPoint, cameraPosition) {
webgl.toRenderCoordinates(
cameraPosition.longitude,
return Math.sqrt((p[0] - p[3]) ** 2 + (p[1] - p[4]) ** 2 + (p[2] - p[5]) ** 2);
// Click handler to get distance from camera to clicked point.
// distance is then used in dofRenderNode to render with the selected focal point
viewElement.addEventListener("arcgisViewClick", function (event) {
const mapPoint = event.detail.mapPoint;
// Map point and camera position are in map coordinates.
// Need to be converted to render coordinates to be used in shader
focus = distanceInRenderCoordinates(mapPoint, viewElement.camera.position);
focusSlider.value = Math.round(focus);
dofRenderNode.requestRender();
// Sliders to control DoF parameters
const focusSlider = document.getElementById("focus-distance");
focusSlider.value = focus;
focusSlider.addEventListener("calciteSliderInput", (event) => {
focus = parseFloat(event.target.value);
dofRenderNode.requestRender();
const apertureSlider = document.getElementById("aperture");
apertureSlider.value = aperture;
apertureSlider.addEventListener("calciteSliderInput", (event) => {
aperture = parseFloat(event.target.value);
dofRenderNode.requestRender();