OpenLayers Workshop(四)WebGL Meteor Shower

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 向量;

为了将数据从顶点着色器传递到片段着色器,我们使用了变量类型; 必须在两个着色器中声明一个变量,并且它在顶点着色器中分配的值可以在片段着色器中访问。

了解更多概念,可以去看这些 attributevarying 和 uniform

发表评论