Animating meteorite impacts 为陨石撞击添加动画效果
目前为止我们已经成功地从CSV文件中获取数据并使用WebGL渲染之,但是这个地图实在是没有什么意思.我们使用了陨石的质量去决定地图上显示的圆圈的半径,但是我们还没能使用点特征中解析为year的属性的陨石撞击日期.
陨石特征的year属性的取址范围是1850到2015,我们将设置一个循环动画,使得年份循环递增,并渲染当年撞击的陨石,在年份增加的过程中逐步减小那一年的陨石大小.
第一步,我们将创建一个循环动画,并将当前年份渲染到地图顶部的一个<div>标签中.将下面的标签添加到index.html的map-container下面.
<div id="year"></div>
编辑style块以包含以下规则
#year {
position: absolute;
bottom: 1em;
left: 1em;
color: white;
-webkit-text-stroke: 1px black;
font-size: 2em;
font-weight: bold;
}
现在我们定义一些代表年份范围和动画播放速度的变量,将下面这些代码加到main.js里面(放在自定义的图层上边)。
const minYear = 1850;
const maxYear = 2015;
const span = maxYear - minYear;
const rate = 10; // years per second
const start = Date.now();
let currentYear = minYear;
接下来需要将地图实例分配给一个变量,随后我们会用到这个变量:
const map = new Map({
在地图的配置底下,加入以下的render函数以让循环动画开始:
const yearElement = document.getElementById('year');
function render() {
const elapsed = rate * (Date.now() - start) / 1000;
currentYear = minYear + (elapsed % span);
yearElement.innerText = currentYear.toFixed(0);
map.render();
requestAnimationFrame(render);
}
render();
如果在此之前没有什么差错的话,现在已经可以从地图的左下角看到循环滚动的数字了。
现在要处理棘手的环节了。先前我们使用WebGL的点图层去显示我们的数据,然后用一些看上去很聪明的表达式去给陨石点动态的样式。但是如果我们想要给这些点动画时应该怎么做呢?这就是WebGLPointsLayer
这个类的局限了。
首先我们需要导入一个renderer类来替换WebGLPointsLayer
:
import Renderer from 'ol/renderer/webgl/PointsLayer';
然后使用这个renderer去创建一个自定义的图层类:
class CustomLayer extends VectorLayer {
createRenderer() {
return new Renderer(this, {
// options go here
})
}
};
接下来我们要为这个renderer提供一些选项。
Uniforms 是特征之间不会变化的值。他们有点像常量,尽管每一个帧他们的值都可以发生更改。这个选项将接收一个uniforms的对象,在这个uniforms对象中我们可以提供一个固定值或者一个函数来在运行时计算值。
将这个对象加入到渲染器的选项中:
uniforms: {
u_currentYear: function() {
return currentYear;
}
},
Attributes 是特征之间相互不同的值。他们同时被name和以当前的特征为参数的callback(回调)修饰。WebGL的渲染器接收一个attributes的数组。
既然我们想要将陨石撞击的mass和year属性同时考虑进去,让我们相应的定义两个属性:
attributes: [{
name: 'size',
callback: function (feature) {
return 32 * clamp(feature.get('mass') / 200000, 0, 1) + 16;
}
},
{
name: 'year',
callback: function (feature) {
return feature.get('year');
},
}],
最后,我们将定义两个将要使用到上面的attribs和uniforms的shaders(片段和顶点)。Shaders是技术上的代码,但是必须作为字符串传递给GPU执行。使用fragmentShader和vertexShader属性将shaders提供给渲染器:
vertexShader: `
precision mediump float;
uniform mat4 u_projectionMatrix;
uniform mat4 u_offsetScaleMatrix;
uniform mat4 u_offsetRotateMatrix;
attribute vec2 a_position;
attribute float a_index;
attribute float a_size;
attribute float a_year;
varying vec2 v_texCoord;
varying float v_year;
void main(void) {
mat4 offsetMatrix = u_offsetScaleMatrix;
float offsetX = a_index == 0.0 || a_index == 3.0 ? -a_size / 2.0 : a_size / 2.0;
float offsetY = a_index == 0.0 || a_index == 1.0 ? -a_size / 2.0 : a_size / 2.0;
vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);
gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;
float u = a_index == 0.0 || a_index == 3.0 ? 0.0 : 1.0;
float v = a_index == 0.0 || a_index == 1.0 ? 0.0 : 1.0;
v_texCoord = vec2(u, v);
v_year = a_year;
}`,
fragmentShader: `
precision mediump float;
uniform float u_currentYear;
varying vec2 v_texCoord;
varying float v_year;
void main(void) {
if (v_year > u_currentYear) {
discard;
}
vec2 texCoord = v_texCoord * 2.0 - vec2(1.0, 1.0);
float sqRadius = texCoord.x * texCoord.x + texCoord.y * texCoord.y;
float factor = pow(1.1, u_currentYear - v_year);
float value = 2.0 * (1.0 - sqRadius * factor);
float alpha = smoothstep(0.0, 1.0, value);
gl_FragColor = vec4(1.0, 0.0, 0.0, 0.5);
gl_FragColor.a *= alpha;
gl_FragColor.rgb *= gl_FragColor.a;
}`
我们不会深究顶点的着色器。它的主要职责就是设置点的大小并将year属性提供给片段的着色器。
For now, the renderer still requires you to give it a full working shader although most of it is reusable from other use cases. This might evolve with upcoming releases of OpenLayers.
目前,渲染器仍然需要您为它提供一个完整的工作着色器,尽管其中大部分都可以从其他用例中重用。 这可能会随着即将发布的 OpenLayers 版本而发展。
让我们仔细看看片段着色器:
fragmentShader: `
precision mediump float;
uniform float u_currentYear;
varying vec2 v_texCoord;
varying float v_year;
void main(void) {
if (v_year > u_currentYear) {
discard;
}
vec2 texCoord = v_texCoord * 2.0 - vec2(1.0, 1.0);
float sqRadius = texCoord.x * texCoord.x + texCoord.y * texCoord.y;
float factor = pow(1.1, u_currentYear - v_year);
float value = 2.0 * (1.0 - sqRadius * factor);
float alpha = smoothstep(0.0, 1.0, value);
gl_FragColor = vec4(1.0, 0.0, 0.0, 0.5);
gl_FragColor.a *= alpha;
gl_FragColor.rgb *= gl_FragColor.a;
}`
代码块main开始于一个条件判断:
if (v_year > u_currentYear) {
discard;
}
这意味着如果一个点的撞击年份比当前的年份高,我们就直接不渲染它。本质上我们是从GPU上过滤掉不需要渲染的点的。真有够简单的呢。
接下来的这一部分使用smoothstep
function来把正方形编程圆形。调整传递给smoothstep的参数就可以搞清楚它的工作原理:
vec2 texCoord = v_texCoord * 2.0 - vec2(1.0, 1.0);
float sqRadius = texCoord.x * texCoord.x + texCoord.y * texCoord.y;
float factor = pow(1.1, u_currentYear - v_year);
float value = 2.0 * (1.0 - sqRadius * factor);
float alpha = smoothstep(0.0, 1.0, value);
使用所有渲染器选项后,现在应该能够看到流星撞击地点的出现,然后随着时间的流逝而缩小。
关于着色器的一些附加说明:
两个着色器在开始时都有定义; 你可能已经认识了我们之前指定的uniforms和attributes,但是着色器还包括渲染器默认提供的uniforms和attributes,例如矩阵uniforms 和保存点坐标的vec2 a_position 向量;
为了将数据从顶点着色器传递到片段着色器,我们使用了变量类型; 必须在两个着色器中声明一个变量,并且它在顶点着色器中分配的值可以在片段着色器中访问。