<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Custom RenderNode - Animated Windmills | 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="hybrid" 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>
<arcgis-home slot="top-left"></arcgis-home>
import glMatrix from "https://cdn.jsdelivr.net/npm/gl-matrix@2.3.2/+esm";
import Windmill from "https://developers.arcgis.com/javascript/latest/assets/sample-code/custom-render-node-windmills/windmill.js"; // A simple windmill model
const [RenderNode, webgl, Extent, query, Query, promiseUtils] = await $arcgis.import([
"@arcgis/core/views/3d/webgl/RenderNode.js",
"@arcgis/core/views/3d/webgl.js",
"@arcgis/core/geometry/Extent.js",
"@arcgis/core/rest/query.js",
"@arcgis/core/rest/support/Query.js",
"@arcgis/core/core/promiseUtils.js",
// Request weather station data in this SR
// Maximum number of windmills
const maxWindmills = 100;
// Size of the windmills.
// The raw model has a height of ~10.0 units.
const windmillHeight = 10;
const windmillBladeSize = 4;
/*******************************************************
* Get the reference to the Scene component,
* and to SceneView once component is ready
******************************************************/
const viewElement = document.querySelector("arcgis-scene");
await viewElement.viewOnReady();
const view = viewElement.view;
spatialReference: { wkid: 102100 },
/*******************************************************
******************************************************/
// 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)}`);
/*******************************************************
* Query the wind direction (live data)
******************************************************/
const getWindDirection = () => {
"https://services.arcgis.com/V6ZHFr6zdgNZuVG0/arcgis/rest/services/weather_stations_010417/FeatureServer/0/query";
const queryObject = new Query();
queryObject.returnGeometry = true;
queryObject.outFields = ["WIND_DIRECT", "WIND_SPEED"];
queryObject.where = "STATION_NAME = 'Palm Springs'";
return query.executeQueryJSON(layerURL, queryObject).then((results) => {
direction: results.features[0].getAttribute("WIND_DIRECT") || 0,
speed: results.features[0].getAttribute("WIND_SPEED") || 0,
/*******************************************************
* Query some weather stations within the visible extent
******************************************************/
const getWeatherStations = () => {
"https://services.arcgis.com/V6ZHFr6zdgNZuVG0/arcgis/rest/services/palm_springs_wind_turbines/FeatureServer/0";
const queryObject = new Query();
queryObject.returnGeometry = true;
queryObject.outFields = ["tower_h", "blade_l", "POINT_Z"];
queryObject.where = "tower_h > 0";
queryObject.outSpatialReference = inputSR;
return query.executeQueryJSON(layerURL, queryObject).then((results) => {
/***********************************************
* Install our render node once we have the data
**********************************************/
.eachAlways([getWindDirection(), getWeatherStations(), view.when()])
const wind = results[0].value;
const stations = results[1].value;
new WindmillRenderNode({ view, wind, stations });
/********************************
* Create an external render node
*******************************/
const WindmillRenderNode = RenderNode.createSubclass({
// Number of stations to render
// Vertex and index buffers
vboBladesPositions: null,
windmillBasePositions: null,
windmillBaseNormals: null,
windmillBaseIndices: null,
// Shader attribute and uniform locations
programAttribVertexPosition: null,
programAttribVertexNormal: null,
programUniformProjectionMatrix: null,
programUniformModelViewMatrix: null,
programUniformNormalMatrix: null,
programUniformAmbientColor: null,
programUniformLightingDirection: null,
programUniformDirectionalColor: null,
windmillInstanceWindSpeed: null,
windmillInstanceRPM: null,
windmillInstanceWindDirection: null,
windmillInstanceTowerScale: null,
windmillInstanceBladeScale: null,
windmillInstanceBladeOffset: null,
windmillInstanceInputToRender: null,
// Temporary matrices and vectors,
// used to avoid allocating objects in each frame.
tempMatrix4: new Array(16),
tempMatrix3: new Array(9),
this.consumes.required.push("opaque-color");
this.produces = "opaque-color";
this.initData(this.wind, this.stations);
* Called each time the scene is rendered.
* This is part of the RenderNode interface.
const output = this.bindRenderTarget();
const time = Date.now() / 1000.0;
// Set some global WebGL state
gl.enable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
gl.useProgram(this.program);
this.setCommonUniforms();
// Draw all the bases (one draw call)
glMatrix.mat4.identity(this.tempMatrix4);
// Apply local origin by translation the view matrix by the local origin, this will
// put the view origin (0, 0, 0) at the local origin
glMatrix.mat4.translate(this.tempMatrix4, this.tempMatrix4, this.localOriginRender);
glMatrix.mat4.multiply(this.tempMatrix4, this.camera.viewMatrix, this.tempMatrix4);
gl.uniformMatrix4fv(this.programUniformModelViewMatrix, false, this.tempMatrix4);
// Normals are in world coordinates, normal transformation is therefore identity
glMatrix.mat3.identity(this.tempMatrix3);
gl.uniformMatrix3fv(this.programUniformNormalMatrix, false, this.tempMatrix3);
gl.drawElements(gl.TRIANGLES, this.windmillBaseIndices.length, gl.UNSIGNED_SHORT, 0);
// Draw all the blades (one draw call per set of blades)
this.bindWindmillBlades();
for (let i = 0; i < this.numStations; ++i) {
// Current rotation of the blade (varies with time, random offset)
const bladeRotation = (time / 60) * this.windmillInstanceRPM[i] + i;
// 1. Scale (according to blade size)
// 2. Rotate around Y axis (according to wind speed, varies with time)
// 3. Rotate around Z axis (according to wind direction)
// 4. Translate along Z axis (to where the blades are attached to the base)
// 5. Transform to render coordinates
// 6. Transform to view coordinates
glMatrix.mat4.identity(this.tempMatrix4);
this.windmillInstanceBladeOffset[i],
this.windmillInstanceWindDirection[i],
glMatrix.mat4.rotateY(this.tempMatrix4, this.tempMatrix4, bladeRotation);
this.windmillInstanceBladeScale[i],
this.windmillInstanceInputToRender[i],
glMatrix.mat3.normalFromMat4(this.tempMatrix3, this.tempMatrix4);
glMatrix.mat4.multiply(this.tempMatrix4, this.camera.viewMatrix, this.tempMatrix4);
gl.uniformMatrix4fv(this.programUniformModelViewMatrix, false, this.tempMatrix4);
gl.uniformMatrix3fv(this.programUniformNormalMatrix, false, this.tempMatrix3);
gl.drawElements(gl.TRIANGLES, Windmill.blades_indices.length, gl.UNSIGNED_SHORT, 0);
// return output fbo (= input fbo)
* Initializes all shaders requried by the application
const vertexShader = `#version 300 es
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat3 uNormalMatrix;
uniform vec3 uAmbientColor;
uniform vec3 uLightingDirection;
uniform vec3 uDirectionalColor;
gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aVertexPosition, 1.0);
vec3 transformedNormal = normalize(uNormalMatrix * aVertexNormal);
float directionalLightWeighting = max(dot(transformedNormal, uLightingDirection), 0.0);
vLightColor = uAmbientColor + uDirectionalColor * directionalLightWeighting;
const fragmentShader = `#version 300 es
fragColor = vec4(vLightColor, 1.0);
this.program = createProgram(gl, vertexShader, fragmentShader);
alert("Could not initialize shaders");
gl.useProgram(this.program);
this.programAttribVertexPosition = gl.getAttribLocation(this.program, "aVertexPosition");
this.programAttribVertexNormal = gl.getAttribLocation(this.program, "aVertexNormal");
this.programUniformProjectionMatrix = gl.getUniformLocation(
this.programUniformModelViewMatrix = gl.getUniformLocation(
this.programUniformNormalMatrix = gl.getUniformLocation(this.program, "uNormalMatrix");
this.programUniformAmbientColor = gl.getUniformLocation(this.program, "uAmbientColor");
this.programUniformLightingDirection = gl.getUniformLocation(
this.programUniformDirectionalColor = gl.getUniformLocation(
* Creates a vertex buffer from the given data.
createVertexBuffer(gl, data) {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// We have filled vertex buffers in 64bit precision,
// convert to a format compatible with WebGL
const float32Data = new Float32Array(data);
gl.bufferData(gl.ARRAY_BUFFER, float32Data, gl.STATIC_DRAW);
* Creates an index buffer from the given data.
createIndexBuffer(gl, data) {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW);
* Rotations per second of our turbine for a given wind speed (in km/h).
* This is not an exact physical formula, but rather a rough guess used
* to show qualitative differences between wind speeds.
getRPM(windSpeed, bladeLength) {
const tipSpeedRatio = 6.0;
return (60 * ((windSpeed * 1000) / 3600) * tipSpeedRatio) / (Math.PI * 2 * bladeLength);
* Initializes all windmill data
* - We create a single vertex buffer with all the vertices of all windmill bases.
* This way we can render all the bases in a single draw call.
* - Storing the vertices directly in render coordinates would introduce precision issues.
* We store them in the coordinate system of a local origin of our choice instead.
* - We create a vertex buffer with the vertices of one set of windmill blades.
* Since the blades are animated, we render each set of blades with a different,
* time-dependent transformation.
initData(wind, stations) {
this.numStations = Math.min(stations.length, maxWindmills);
// Choose a local origin.
// In our case, we simply use the view's center.
// For global scenes, you'll need multiple local origins.
const localOriginSR = view.center.spatialReference;
this.localOrigin = [view.center.x, view.center.y, 0];
// Calculate local origin in render coordinates with 32bit precision
this.localOriginRender = webgl.toRenderCoordinates(
// Extract station data into flat arrays.
this.windmillInstanceWindSpeed = new Float32Array(this.numStations);
this.windmillInstanceRPM = new Float32Array(this.numStations);
this.windmillInstanceWindDirection = new Float32Array(this.numStations);
this.windmillInstanceTowerScale = new Float32Array(this.numStations);
this.windmillInstanceBladeScale = new Array(this.numStations);
this.windmillInstanceBladeOffset = new Array(this.numStations);
this.windmillInstanceInputToRender = new Array(this.numStations);
for (let i = 0; i < this.numStations; ++i) {
const station = stations[i];
const bladeLength = station.getAttribute("blade_l");
const towerHeight = station.getAttribute("tower_h");
this.windmillInstanceWindSpeed[i] = wind.speed;
this.windmillInstanceRPM[i] = this.getRPM(wind.speed, bladeLength);
this.windmillInstanceWindDirection[i] = (wind.direction / 180) * Math.PI;
const towerScale = towerHeight / windmillHeight;
this.windmillInstanceTowerScale[i] = towerScale;
const bladeScale = bladeLength / windmillBladeSize;
this.windmillInstanceBladeScale[i] = [bladeScale, bladeScale, bladeScale];
this.windmillInstanceBladeOffset[i] = glMatrix.vec3.create();
this.windmillInstanceBladeOffset[i],
// Transformation from input to render coordinates.
const inputSR = station.geometry.spatialReference;
station.getAttribute("POINT_Z") || station.geometry.z,
const inputToRender = webgl.renderCoordinateTransformAt(
this.windmillInstanceInputToRender[i] = inputToRender;
// Transform all vertices of the windmill base into the coordinate system of
// the local origin, and merge them into a single vertex buffer.
this.windmillBasePositions = new Float64Array(
this.numStations * Windmill.base_positions.length,
this.windmillBaseNormals = new Float64Array(
this.numStations * Windmill.base_normals.length,
this.windmillBaseIndices = new Uint16Array(
this.numStations * Windmill.base_indices.length,
for (let i = 0; i < this.numStations; ++i) {
// Transformation of positions from local to render coordinates
const positionMatrix = new Float64Array(16);
glMatrix.mat4.identity(positionMatrix);
this.windmillInstanceWindDirection[i],
this.windmillInstanceInputToRender[i],
// Transformation of normals from local to render coordinates
const normalMatrix = new Float64Array(9);
glMatrix.mat3.normalFromMat4(normalMatrix, positionMatrix);
// Append vertex and index data
const numCoordinates = Windmill.base_positions.length;
const numVertices = numCoordinates / 3;
for (let j = 0; j < numCoordinates; ++j) {
this.windmillBasePositions[i * numCoordinates + j] =
Windmill.base_positions[j] * this.windmillInstanceTowerScale[i];
this.windmillBaseNormals[i * numCoordinates + j] = Windmill.base_normals[j];
// Transform vertices into render coordinates
this.windmillBasePositions,
glMatrix.vec3.transformMat4,
// Subtract local origin coordinates
this.windmillBasePositions,
// Transform normals into render coordinates
this.windmillBaseNormals,
glMatrix.vec3.transformMat3,
this.windmillBaseNormals,
const numIndices = Windmill.base_indices.length;
for (let j = 0; j < numIndices; ++j) {
this.windmillBaseIndices[i * numIndices + j] =
Windmill.base_indices[j] + i * numVertices;
// Upload our data to WebGL
this.vboBasePositions = this.createVertexBuffer(gl, this.windmillBasePositions);
this.vboBaseNormals = this.createVertexBuffer(gl, this.windmillBaseNormals);
this.vboBladesPositions = this.createVertexBuffer(gl, Windmill.blades_positions);
this.vboBladesNormals = this.createVertexBuffer(gl, Windmill.blades_normals);
this.iboBase = this.createIndexBuffer(gl, this.windmillBaseIndices);
this.iboBlades = this.createIndexBuffer(gl, Windmill.blades_indices);
* Activates vertex attributes for the drawing of the windmill base.
gl.bindBuffer(gl.ARRAY_BUFFER, this.vboBasePositions);
gl.enableVertexAttribArray(this.programAttribVertexPosition);
gl.vertexAttribPointer(this.programAttribVertexPosition, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vboBaseNormals);
gl.enableVertexAttribArray(this.programAttribVertexNormal);
gl.vertexAttribPointer(this.programAttribVertexNormal, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.iboBase);
* Activates vertex attributes for the drawing of the windmill blades.
gl.bindBuffer(gl.ARRAY_BUFFER, this.vboBladesPositions);
gl.enableVertexAttribArray(this.programAttribVertexPosition);
gl.vertexAttribPointer(this.programAttribVertexPosition, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vboBladesNormals);
gl.enableVertexAttribArray(this.programAttribVertexNormal);
gl.vertexAttribPointer(this.programAttribVertexNormal, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.iboBlades);
* Returns a color vector from a {color, intensity} object.
getFlatColor(src, output) {
output[0] = src.color[0] * src.intensity;
output[1] = src.color[1] * src.intensity;
output[2] = src.color[2] * src.intensity;
* Sets common shader uniforms
const camera = this.camera;
this.programUniformDirectionalColor,
this.getFlatColor(this.sunLight.diffuse, this.tempVec3),
this.programUniformAmbientColor,
this.getFlatColor(this.sunLight.ambient, this.tempVec3),
gl.uniform3fv(this.programUniformLightingDirection, this.sunLight.direction);
this.programUniformProjectionMatrix,
this.camera.projectionMatrix,