JChehe / blog

🌈 原创&翻译 🌈
717 stars 111 forks source link

【译】隧道动画 #15

Open JChehe opened 6 years ago

JChehe commented 6 years ago

原文链接:

译者注:本文由两章节组成。

隧道动画(第一章节)

嘿,读者们! 👋

如果说哪样东西是我的真爱,那么它应该就是隧道动画 😍

不懂我的意思?那看看我之前创建的案例吧:

codepen demo 1 codepen demo 2 codepen demo 3 codepen demo 4

我甚至使用这种动画作为我们团队的 2017 愿望卡 🎆。

2017 愿望卡

我将在本文阐述上述案例的基本实现。其中,第一步是创建一个隧道(管道),并在其内部执行摄像机动画。接下来,我们看看如何自定义隧道。

对于该案例,我会使用 Three.js 实现 WebGL 部分。如果对此不熟悉,你可以看看 Rachel Smith 的 相关文章

目录

  1. 建立场景
  2. 创建一个管道(tube)几何体
  3. 基于 SVG 多边形创建一个管道
  4. 摄像机在管道内移动
  5. 添加一个光源
  6. 疯狂起来吧

1. 建立场景

第一步,搭建用于初始化 Three.js 场景的基本要素。

别忘了在页面中引入 Three.js 库

See the Pen Setup the scene by Louis Hoebregts (@Mamboleoo) on CodePen.

如果你能看到一个红色立方体在旋转,那么这就意味着我们可以继续往下走啦 📦 !

2. 创建一个管道几何体

想要在 Three.js 中创建管道,首先需要创建一条路径(path)。THREE.CatmullRomCurve3() 构建函数能实现这个需求,它能基于一个顶点数组创建一条平滑的曲线。

在案例中,我硬编码了一个点(point)数组,并将每个点转为 Vector3()

当拥有了顶点数组后,就能使用上述构造函数创建路径。

// 硬编码点数组
var points = [
  [0, 2],
  [2, 10],
  [-1, 15],
  [-3, 20],
  [0, 25]
];

// 将点转为顶点
for (var i = 0; i < points.length; i++) {
  var x = points[i][0];
  var y = 0;
  var z = points[i][10];
  points[i] = new THREE.Vector3(x, y, z);
}

// 根据顶点创建路径
var path = new THREE.CatmullRomCurve3(points);

在得到路径后,我们就能基于它创建管道了。

// 基于路径创建管道几何体
// 第一个参数是路径
// 第二个参数是组成管道的片段(segment)数量
// 第三个参数是管道半径
// 第四个参数是沿半径的片段数
// 第五个参数是指定管道是否闭合
var geometry = new THREE.TubeGeometry( path, 64, 2, 8, false );
// 红色基础材质(译者注:不受光影响)
var material = new THREE.MeshBasicMaterial( { color: 0xff0000 } );
// 创建网格(mesh)
var tube = new THREE.Mesh( geometry, material );
// 将管道网格添加至场景中
scene.add( tube );

See the Pen Create a tube geometry by Louis Hoebregts (@Mamboleoo) on CodePen.

这时应该能看到一个红色管道在场景中旋转 😊。

3. 基于 SVG 多边形创建一个管道

在大多数情况下,我们并不希望硬编码路径上的点。其实我们可以通过一些随机算法生成一系列点。但对于下一个案例,我们将从由 Adobe Illustrator 创建的 SVG 上获取点的值。

如果不在路径上设置任何贝塞尔曲线,Illustrator 会将路径作为多边形导出,如:

<svg viewBox="0 0 346.4 282.4">
    <polygon points="68.5,185.5 1,262.5 270.9,281.9 345.5,212.8 178,155.7 240.3,72.3 153.4,0.6 52.6,53.3 "/>
</svg>

我们手动地将多边形的点转化为一个数组:

var points = [
    [68.5,185.5],
    [1,262.5],
    [270.9,281.9],
    [345.5,212.8],
    [178,155.7],
    [240.3,72.3],
    [153.4,0.6],
    [52.6,53.3],
    [68.5,185.5]
];

// 切记要将最后一个参数设置为 true,否则管道不闭合
var geometry = new THREE.TubeGeometry( path, 300, 2, 20, true );

如果感兴趣,你可以创建一个动态将 SVG 字符串转为数组的函数 😉

See the Pen Create a tube from a SVG polygon by Louis Hoebregts (@Mamboleoo) on CodePen.

你也可以看到左侧是 SVG 多边形,右侧则是根据多边形的点创建的管道。

4. 摄像机在管道内移动

有了管道后,剩余的主要部分就是动画了!

我们会使用 path 非常有用的函数 path.getPointAt(t)

该函数会基于所指定的参数值,返回路径上的任意一点。参数的取值范围为 0 至 1。0 是路径的第一个点,1 是路径的最后一个点。

我们会在每帧中调用此函数,以让摄像机沿着路径移动。另外,我们还需要在每帧中增大该函数的参数值以获得意想不到的效果。

// 参数值由 0 开始
var percentage = 0;
function render(){
  // 参数值自增
  percentage += 0.001;
  // 根据参数值获取路径上的点
  var p1 = path.getPointAt(percentage%1);
  // 将摄像机放置在该点上
  camera.position.set(p1.x,p1.y,p1.z);

  renderer.render(scene, camera);
  requestAnimationFrame(render);
}
requestAnimationFrame(render);

由于 .getPointAt() 函数只接受 [0, 1] 区间内的值,我们需要对该值进行“取余”运算,以确保它不会大于 1。

目前摄像机的位置设定没有任何问题,但摄像机的朝向始终不变。为了解决该问题,我们需要将摄像机的朝向设置在比自身位置更前一点的地方。因此,在每帧中,我们不仅需要计算摄像机的自身位置,还要计算比该位置更靠前的朝向坐标。

var percentage = 0;
function render(){
  percentage += 0.001;
  var p1 = path.getPointAt(percentage%1);
  // 获取路径上更靠前的点
  var p2 = path.getPointAt((percentage + 0.01)%1);
  camera.position.set(p1.x,p1.y,p1.z);
  // 让摄像机朝向第二个点
  camera.lookAt(p2);

  renderer.render(scene, camera);
  requestAnimationFrame(render);
}

另外,材质还提供了其他可选参数。目前,管道的材质是 MeshBasicMaterial,但其只渲染了管道外层(译者注:默认情况下),而摄像机是置于管道内的,因此只需要渲染管道材质背面即可。另外,由于我们并未在场景中添加任何光源,将材质设置为线框(wireframe)时,才能清楚看到具体效果。

var material = new THREE.MeshBasicMaterial({
  color: 0xff0000, // 红色
  side : THREE.BackSide, // 渲染背面材质
  wireframe:true // 以线框的形式展示管道
});

瞧,现在摄像机在管道内动起来了!🎉

See the Pen Move the camera inside the tube by Louis Hoebregts (@Mamboleoo) on CodePen.

5. 添加光源

我并不打算在本文详细讲解光源,但会告诉你如何在管道中设置基础的光源效果。

其原理与摄像机的运动相同。我们将光源放置在摄像机的朝向位置。

// 为场景创建一个点光源
var light = new THREE.PointLight(0xffffff,1, 50);
scene.add(light);
var material = new THREE.MeshLambertMaterial({
  color: 0xff0000,
  side : THREE.BackSide
});
var percentage = 0;
function render(){
  percentage += 0.0003;
  var p1 = path.getPointAt(percentage%1);
  var p2 = path.getPointAt((percentage + 0.02)%1);
  camera.position.set(p1.x,p1.y,p1.z);
  camera.lookAt(p2);
  light.position.set(p2.x, p2.y, p2.z);

  renderer.render(scene, camera);
  requestAnimationFrame(render);
}

这就是具体效果啦!

See the Pen Add a light by Louis Hoebregts (@Mamboleoo) on CodePen.

6. 疯狂起来吧

基于最后一步(译者注:即上一步),我通过一些参数的变换,创建出新的动画类型。若感兴趣可以查看源代码。 😉

对于该案例,我为每个面设置了不同颜色。这样就得到了一个有趣的马赛克效果啦。

See the Pen Crazy 1 by Louis Hoebregts (@Mamboleoo) on CodePen.

In this one, I'm playing with the Y position of the points to generate my path. That way the tube is not only on one plane, but in three dimensions. 在该案例中,路径的生成加入了 Y 坐标(即不再为 0)。这使得管道不仅是在一个平面上,而是在三维空间上穿梭了。

See the Pen Crazy 2 by Louis Hoebregts (@Mamboleoo) on CodePen.

对于最后一个案例,我创建了 5 个半径和颜色均不相同的隧道。而为了得到更佳的显示效果,它们均被设置了不同的透明度。

See the Pen Crazy 3 by Louis Hoebregts (@Mamboleoo) on CodePen.

这就是第一章节的结尾了。而在下一章节,我将阐述如何在不使用 TubeGeometry 的情况下创建粒子管道。

若想一次性体验上述所有案例,你可以看看该 集合

我希望你能从中学习到一些东西!如果遇到任何问题,请毫不犹豫地通过 Twitter 告诉我。

隧道动画(第二章节)

正如我在上一章节的结尾说道,我们将看到如何在不使用 TubeGeometry() 的情况下创建粒子管道。

This is the kind of animation you'll be able to create at the end 你最终能创建这种类型的动画

目录

  1. 计算粒子的位置
  2. 创建管道
  3. 动起来
  4. 疯狂起来吧

1. 计算粒子的位置

为了达到想要的效果,我们需要沿着路径生成粒子。其实,Three.js 也是使用相同的方式生成管道几何体,其中不同的一点是:它加入了面(face),以形成一个常见的管道。

首先明确的细节是管道的构建元素及其半径。

这些细节由两个值设定:

// 组成管道的圆圈数量
var segments = 500;
// 组成每个圆的粒子数量
var circlesDetail = 10;
// 管道的半径
var radius = 5;

现在我们或许已经知道粒子的总数了(剧透:segments * circleDetails),而此刻则需要我们计算 佛莱纳标架(Frenet frame)

我并不是这个领域的专家,但需要我们明白的是,佛莱纳标架(Frenet frame)是为管道所有片段(segments)计算出来的值。而每个值则由单位切向量、主法向量和副法向量组成。换句话说,这些值指定了每个片段的旋转角度及其朝向。

如果你想了解更多关于佛莱纳标架的计算过程,可以看看这篇文章

多亏了 Three.js,我们不必理解上述任何知识即可让我们继续进行下去。使用 path 的内置函数即可:

var frames = path.computeFrenetFrames(segments, true);
// true 表明是否让路径闭合,在该案例中需要路径闭合

该函数返回的是三个由 Vector3() 组成的数组。

frenet frames

现在我们拥有每个片段所需的所有信息,能开始沿着每个片段生成粒子了。

我们将每个粒子以 Vector3() 形式存储在 Geometry() 中,以便后续复用。

// 创建一个用于插入粒子的空几何体
var geometry = new THREE.Geometry();

现在需要为每块片段放置粒子。这就是通过循环遍历所有片段的原因。

我不打算在这里阐述该函数是如何工作的,查看下面代码,你会发现所有细节都在注释中!⬇

// 循环遍历所有片段
for (var i = 0; i < segments; i++) {

  // 从佛莱纳标架获取片段的主法向量
  var normal = frames.normals[i];
  // 从佛莱纳标架获取片段的副法向量
  var binormal = frames.binormals[i];

  // 计算片段的索引(0 至 1)
  var index = i / segments;

  // 获取片段中心点的坐标
  // 在第一章节用于沿路径移动摄像机的函数
  var p = path.getPointAt(index);

  // Loop for the amount of particles we want along each circle
  // 循环每个圆所需的粒子数量
  for (var j = 0; j < circlesDetail; j++) {

    // 复制圆心的位置
    var position = p.clone();

    // 需要将每个点基于角度 0 至 Pi*2 进行定位
    // 如果只想要半个管道(如水滑梯),你可以从 0 至 Pi。
    var angle = (j / circlesDetail) * Math.PI * 2;

    // 计算当前角度的 sine 值
    var sin = Math.sin(angle);
    //计算当前角度的负 cosine 值
    var cos = -Math.cos(angle);

    // 根据每个点的角度、片段的主法向量与副法向量,计算出每个点的法向量
    var normalPoint = new THREE.Vector3(0,0,0);
    normalPoint.x = (cos * normal.x + sin * binormal.x);
    normalPoint.y = (cos * normal.y + sin * binormal.y);
    normalPoint.z = (cos * normal.z + sin * binormal.z);

    // 法向量乘以半径长度,因为我们所需的管道的半径不是 1。
    normalPoint.multiplyScalar(radius);

    // 圆心位置与法向量相加
    position.add(normalPoint);

    // 将该向量放到几何体中
    geometry.vertices.push(position);
  }
}

唷,这段代码并不是那么容易理解。我阅读了 Three.js 的源码后才将其编写出来。

通过下面这个案例,你可以看到粒子是如何逐个计算出来的。

(如果管道已完全显示,请点击 “Rerun”)

See the Pen Calculate the positions of the particles by Louis Hoebregts (@Mamboleoo) on CodePen.

2. 创建管道

现在已经拥有填充顶点后的几何体对象了。通过 Three.js 的 Points 构造函数,你能创建精妙的粒子案例。而且对于简单的点,其渲染性能优异。可以通过纹理或不同的颜色自定义点的样式。

与创建 网格(Mesh) 的方式相同,我们需要两个元素才能创建 Points 对象,这两者是材质(material)与几何体(geometry)。因为在第一步中几何体已被创建,所以这里需要定义的是材质。

var material = new THREE.PointsMaterial({
  size: 1, // 每个点的尺寸
  sizeAttenuation: true, // 点的大小是否依赖于其与摄像机的距离(译者注:即近大远小)
  color: 0xff0000 // 点的颜色
});

最后,创建点对象(Points),并将其添加至场景中:

var tube = new THREE.Points(geometry, material);
scene.add(tube);

See the Pen Create the tube by Louis Hoebregts (@Mamboleoo) on CodePen.

3. 动起来

为了让所有东西动起来,我们将复用前面动画案例的代码。

var percentage = 0;
function render() {

  // 变量自增
  percentage += 0.0005;
  // 获取摄像机该去的位置
  var p1 = path.getPointAt(percentage % 1);
  // 获取摄像机该朝向的位置
  var p2 = path.getPointAt((percentage + 0.01) % 1);
  camera.position.set(p1.x, p1.y, p1.z);
  camera.lookAt(p2);

  // 渲染场景
  renderer.render(scene, camera);

  // 动画循环
  requestAnimationFrame(render);
}

🎉 欢呼吧,我们有一个由粒子构成的基础隧道了 🎉

See the Pen Moving particle tunnel by Louis Hoebregts (@Mamboleoo) on CodePen.

4. 疯狂起来吧

经过第二章节的学习后,现在的你可以接触到无数个不同的隧道了!可以在下面三个案例中体验到基于上述知识构建的自定义隧道。

多彩隧道

在该案例中,我为每个点应用了自定义颜色。另外,也在场景中添加了雾化效果,让隧道产生渐隐效果。

See the Pen Crazy 4 by Louis Hoebregts (@Mamboleoo) on CodePen.

// 首先基于顶点索引创建新颜色
var color = new THREE.Color("hsl(" + (index * 360 * 4) + ", 100%, 50%)");
// 将该颜色添加到几何体对象的 colors 数组内
geometry.colors.push(color);

var material = new THREE.PointsMaterial({
  size: 0.2,
  vertexColors: THREE.VertexColors // 指定颜色应当来自几何体
});

// 为场景添加雾化效果
scene.fog = new THREE.Fog(0x000000, 30, 150);

方格洞穴(Squared cave)

See the Pen Crazy 5 by Louis Hoebregts (@Mamboleoo) on CodePen.

该隧道仅由立方体组成。我在每个顶点的位置创建新的网格(Mesh),以替换 Points 对象。另外,颜色是基于柏林噪声算法生成。

八边形隧道

<p data-height="265" data-theme-id="0" data-slug-hash="EmaReQ" data-default-tab="result" data-user="Mamboleoo" data-embed-version="2" data-pen-title="Crazy 6" class="codepen">See the Pen <a href="https://codepen.io/Mamboleoo/pen/EmaReQ/">Crazy 6</a> by Louis Hoebregts (<a href="https://codepen.io/Mamboleoo">@Mamboleoo</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

对于该案例,我将每个圆的顶点连接起来。而且调整了顶点的角度及其颜色以创造出旋转的幻觉。

for (var i = 0; i < tubeDetail; i++) {
  // 为每个圆创建新的几何体
  var circle = new THREE.Geometry();
  for (var j = 0; j < circlesDetail; j++) {
    // 将点添加到圆的顶点数组
    circle.vertices.push(position);
  }
  // 再次将第一个点的添加到圆的顶点数组,以保证圆是闭合的
  circle.vertices.push(circle.vertices[0]);
  // 以自定义颜色创建新材质
  var material = new THREE.LineBasicMaterial({
    color: new THREE.Color("hsl("+(noise.simplex2(index*10,0)*60 + 300)+",50%,50%)")
  });
  // 创建线(Line)对象
  var line = new THREE.Line(circle, material);
  // 将其添加到场景中
  scene.add(line);

感谢你阅读我这篇关于隧道动画的文章!

若你想一次性体验上述所有案例(第二章节),你可以看看该 集合

希望你会对这篇文章感兴趣!如果你创造出了漂亮的隧道,请与我分享 --> Twitter。如果遇到任何问题,请毫不犹豫地告诉我。 😉