ArcGIS JSAPI 最新版(4.29
)更新了 RenderNode
类,取代了之前 externalrender
类。
而 RenderNode 使用更为灵活,功能也更加强大,详情可见官方示例:改变颜色。
在使用 RenderNode 过程中,遇到一个奇怪的问题,实现动画效果时,鼠标移入、悬浮移动时会加速,移出则减速。
经过调试终于解决问题,这里记录一下。
本文包含问题原因、解决问题和在线示例三部分。
问题原因
为了重现这个问题,笔者修改了官方示例代码,在 RenderNode render 函数中,调试 time 变量;
示例中手动增加 time 的值,改变风车的叶片的角度,从而实现动画。(原示例利用时间的变化来实现
)
render() 每帧都会触发,而鼠标移入、悬浮移动的时候也会触发 render() ,因此就触发了多次
,导致 time 的值会以倍数增加
,
从而使动画更快。鼠标移出,速度恢复,看起来像是降低速度。
// render 渲染,鼠标移动会触发多次
render(inputs) {
this.resetWebGLState();
const output = this.bindRenderTarget();
const gl = this.gl;
this.time = this.time || 1710923219.633;;
this.time += 10;
const time = this.time/1000;
// 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;
glMatrix.mat4.rotateY(this.tempMatrix4, this.tempMatrix4, bladeRotation);
}
// Draw continuously
this.requestRender();
// return output fbo (= input fbo)
return output;
}
解决问题
解决问题也比较容易,这里提供两种途径:
- 使用时间(Date)的变化来实现动画。
const time = Date.now() / 1000.0;
- 在 render() 外部使用修改参数的数值。
let time_ = 1710923219.633;
function addTime(){
time_ += 15;
requestAnimationFrame(addTime);
}
addTime();
完整代码
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<title>Custom RenderNode - Animated Windmills | Sample | ArcGIS Maps SDK for JavaScript 4.29</title>
<link rel="stylesheet" href="https://js.arcgis.com/4.29/esri/themes/light/main.css" />
<script src="https://js.arcgis.com/4.29/"></script>
<style>
html,
body,
#viewDiv {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
}
</style>
<!-- A simple windmill model -->
<script src="https://developers.arcgis.com/javascript/latest/sample-code/custom-render-node-windmills/live/windmill.js"></script>
<!-- A simple fragment shader -->
<script id="shader-fs" type="x-shader/x-fragment">
precision mediump float;
varying vec3 vLightColor;
void main(void) {
gl_FragColor = vec4(vLightColor, 1.0);
}
</script>
<!-- A simple vertex shader -->
<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 aVertexPosition;
attribute vec3 aVertexNormal;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat3 uNormalMatrix;
uniform vec3 uAmbientColor;
uniform vec3 uLightingDirection;
uniform vec3 uDirectionalColor;
varying vec3 vLightColor;
void main(void) {
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;
}
</script>
<!-- Our application -->
<script>
require([
"esri/core/Accessor",
"esri/Map",
"esri/views/SceneView",
"esri/views/3d/webgl/RenderNode",
"esri/views/3d/webgl",
"esri/geometry/Extent",
"esri/rest/query",
"esri/rest/support/Query",
"esri/widgets/Home",
"https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.3.2/gl-matrix-min.js",
"esri/core/promiseUtils"
], (Accessor, Map, SceneView, RenderNode, webgl, Extent, query, Query, Home, glMatrix, promiseUtils) => {
/*********************
* Settings
*********************/
// The clipping extent for the scene (in WGS84)
const mapExtent = new Extent({
xmax: -130,
xmin: -100,
ymax: 40,
ymin: 20,
spatialReference: {
wkid: 4326
}
});
// Request weather station data in this SR
const inputSR = {
wkid: 3857
};
// 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;
/*********************
* Create a map
*********************/
const map = new Map({
basemap: "hybrid",
ground: "world-elevation"
});
/*********************
* Create a scene view
*********************/
const view = new SceneView({
container: "viewDiv",
map: map,
viewingMode: "global",
clippingArea: mapExtent,
extent: mapExtent,
camera: {
position: {
x: -12977859.07,
y: 4016696.94,
z: 348.61,
spatialReference: { wkid: 102100 }
},
heading: 316,
tilt: 85
}
});
const homeBtn = new Home({
view: view
});
// Add the home button to the top left corner of the view
view.ui.add(homeBtn, "top-left");
/*******************************************************
* Query the wind direction (live data)
******************************************************/
const getWindDirection = () => {
const layerURL =
"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) => {
return {
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 = () => {
const layerURL =
"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) => {
return results.features;
});
}
/***********************************************
* Install our render node once we have the data
**********************************************/
promiseUtils
.eachAlways([getWindDirection(), getWeatherStations(), view.when()])
.then((results) => {
const wind = results[0].value;
const stations = results[1].value;
new WindmillRenderNode({view, wind, stations});
})
.catch((error) => {
console.log(error);
});
let time_ = 1710923219.633;
function addTime(){
time_ += 15;
requestAnimationFrame(addTime);
}
addTime();
/********************************
* Create an external render node
*******************************/
const WindmillRenderNode = RenderNode.createSubclass({
// Input data
wind: null,
stations: null,
view: null,
// Number of stations to render
numStations: null,
// Local origin
localOrigin: null,
localOriginRender: null,
// Vertex and index buffers
vboBasePositions: null,
vboBaseNormals: null,
iboBase: null,
vboBladesPositions: null,
vboBladesNormals: null,
iboBlades: null,
// Vertex and index data
windmillBasePositions: null,
windmillBaseNormals: null,
windmillBaseIndices: null,
// Shader
program: null,
// Shader attribute and uniform locations
programAttribVertexPosition: null,
programAttribVertexNormal: null,
programUniformProjectionMatrix: null,
programUniformModelViewMatrix: null,
programUniformNormalMatrix: null,
programUniformAmbientColor: null,
programUniformLightingDirection: null,
programUniformDirectionalColor: null,
// Per-instance data
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),
tempVec3: new Array(3),
/**
* Set up render node
*/
initialize() {
this.consumes.required.push("opaque-color");
this.produces = "opaque-color";
this.initShaders();
this.initData(this.wind, this.stations);
},
/**
* Called each time the scene is rendered.
* This is part of the RenderNode interface.
*/
// render 渲染,鼠标移动会触发多次
render(inputs) {
this.resetWebGLState();
const output = this.bindRenderTarget();
const gl = this.gl;
this.time = this.time || 1710923219.633;;
this.time += 10;
const time = this.time/1000;
// Set some global WebGL state
gl.enable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
gl.disable(gl.BLEND);
// Enable our shader
gl.useProgram(this.program);
this.setCommonUniforms();
// Draw all the bases (one draw call)
this.bindWindmillBase();
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;
// Blade transformation:
// 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);
glMatrix.mat4.translate(this.tempMatrix4, this.tempMatrix4, this.windmillInstanceBladeOffset[i]);
glMatrix.mat4.rotateZ(this.tempMatrix4, this.tempMatrix4, this.windmillInstanceWindDirection[i]);
glMatrix.mat4.rotateY(this.tempMatrix4, this.tempMatrix4, bladeRotation);
glMatrix.mat4.scale(this.tempMatrix4, this.tempMatrix4, this.windmillInstanceBladeScale[i]);
glMatrix.mat4.multiply(this.tempMatrix4, this.windmillInstanceInputToRender[i], this.tempMatrix4);
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);
}
// Draw continuously
this.requestRender();
// return output fbo (= input fbo)
return output;
},
/**
* Loads a shader from a <script> html tag
*/
getShader(gl, id) {
const shaderScript = document.getElementById(id);
if (!shaderScript) {
return null;
}
let str = "";
let k = shaderScript.firstChild;
while (k) {
if (k.nodeType == 3) {
str += k.textContent;
}
k = k.nextSibling;
}
let shader;
if (shaderScript.type == "x-shader/x-fragment") {
shader = gl.createShader(gl.FRAGMENT_SHADER);
} else if (shaderScript.type == "x-shader/x-vertex") {
shader = gl.createShader(gl.VERTEX_SHADER);
} else {
return null;
}
gl.shaderSource(shader, str);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(shader));
return null;
}
return shader;
},
/**
* Links vertex and fragment shaders into a GLSL program
*/
linkProgram(gl, fragmentShader, vertexShader) {
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
return null;
}
return shaderProgram;
},
/**
* Initializes all shaders requried by the application
*/
initShaders() {
const gl = this.gl;
const fragmentShader = this.getShader(gl, "shader-fs");
const vertexShader = this.getShader(gl, "shader-vs");
this.program = this.linkProgram(gl, fragmentShader, vertexShader);
if (!this.program) {
alert("Could not initialize shaders");
}
gl.useProgram(this.program);
// Program attributes
this.programAttribVertexPosition = gl.getAttribLocation(this.program, "aVertexPosition");
this.programAttribVertexNormal = gl.getAttribLocation(this.program, "aVertexNormal");
// Program uniforms
this.programUniformProjectionMatrix = gl.getUniformLocation(this.program, "uProjectionMatrix");
this.programUniformModelViewMatrix = gl.getUniformLocation(this.program, "uModelViewMatrix");
this.programUniformNormalMatrix = gl.getUniformLocation(this.program, "uNormalMatrix");
this.programUniformAmbientColor = gl.getUniformLocation(this.program, "uAmbientColor");
this.programUniformLightingDirection = gl.getUniformLocation(this.program, "uLightingDirection");
this.programUniformDirectionalColor = gl.getUniformLocation(this.program, "uDirectionalColor");
},
/**
* 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);
return buffer;
},
/**
* 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);
return buffer;
},
/**
* 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 = 600.0;
return (60 * ((windSpeed * 1000) / 3600) * tipSpeedRatio) / (Math.PI * 2 * bladeLength);
},
/**
* Initializes all windmill data
*
* General overview:
* - 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) {
const gl = this.gl;
this.numStations = Math.min(stations.length, maxWindmills);
// Choose a local origin.
// In our case, we simply use the map center.
// For global scenes, you'll need multiple local origins.
const localOriginSR = mapExtent.center.spatialReference;
this.localOrigin = [mapExtent.center.x, mapExtent.center.y, 0];
// Calculate local origin in render coordinates with 32bit precision
this.localOriginRender = webgl.toRenderCoordinates(
view,
this.localOrigin,
0,
localOriginSR,
new Float32Array(3),
0,
1
);
// 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");
// Speed and direction.
this.windmillInstanceWindSpeed[i] = wind.speed;
this.windmillInstanceRPM[i] = this.getRPM(wind.speed, bladeLength);
this.windmillInstanceWindDirection[i] = (wind.direction / 180) * Math.PI;
// Offset and scale
const towerScale = towerHeight / windmillHeight;
this.windmillInstanceTowerScale[i] = towerScale;
const bladeScale = bladeLength / windmillBladeSize;
this.windmillInstanceBladeScale[i] = [bladeScale, bladeScale, bladeScale];
this.windmillInstanceBladeOffset[i] = glMatrix.vec3.create();
glMatrix.vec3.scale(this.windmillInstanceBladeOffset[i], windmill_blades_offset, towerScale);
// Transformation from input to render coordinates.
const inputSR = station.geometry.spatialReference;
const point = [
station.geometry.x,
station.geometry.y,
station.getAttribute("POINT_Z") || station.geometry.z
];
const inputToRender = webgl.renderCoordinateTransformAt(
view,
point,
inputSR,
new Float64Array(16)
);
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);
glMatrix.mat4.rotateZ(positionMatrix, positionMatrix, this.windmillInstanceWindDirection[i]);
glMatrix.mat4.multiply(positionMatrix, this.windmillInstanceInputToRender[i], positionMatrix);
// 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
glMatrix.vec3.forEach(
this.windmillBasePositions,
0,
i * numCoordinates,
numVertices,
glMatrix.vec3.transformMat4,
positionMatrix
);
// Subtract local origin coordinates
glMatrix.vec3.forEach(
this.windmillBasePositions,
0,
i * numCoordinates,
numVertices,
glMatrix.vec3.subtract,
this.localOriginRender
);
// Transform normals into render coordinates
glMatrix.vec3.forEach(
this.windmillBaseNormals,
0,
i * numCoordinates,
numVertices,
glMatrix.vec3.transformMat3,
normalMatrix
);
// Re-normalize normals
glMatrix.vec3.forEach(
this.windmillBaseNormals,
0,
i * numCoordinates,
numVertices,
glMatrix.vec3.normalize
);
// Append index data
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.
*/
bindWindmillBase() {
const gl = this.gl;
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.
*/
bindWindmillBlades() {
const gl = this.gl;
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;
return output;
},
/**
* Sets common shader uniforms
*/
setCommonUniforms() {
const gl = this.gl;
const camera = this.camera;
gl.uniform3fv(
this.programUniformDirectionalColor,
this.getFlatColor(this.sunLight.diffuse, this.tempVec3)
);
gl.uniform3fv(this.programUniformAmbientColor, this.getFlatColor(this.sunLight.ambient, this.tempVec3));
gl.uniform3fv(this.programUniformLightingDirection, this.sunLight.direction);
gl.uniformMatrix4fv(this.programUniformProjectionMatrix, false, this.camera.projectionMatrix);
}
});
});
</script>
</head>
<body>
<div id="viewDiv"></div>
</body>
</html>
在线示例
渲染完成之后观察叶片速度,晃动鼠标观察叶片速度变快,停止晃动鼠标,叶片速度恢复。
JSAPI 在线示例:custom-render-node 逐帧刷新问题