【译】探索基于 WebGL 的动画与交互(案例学习)

@JChehe 2017-10-06 15:16:02发表于 JChehe/blog

探索基于 WebGL 的动画与交互(案例学习)


原文:Exploring Animation And Interaction Techniques With WebGL (A Case Study)

两年前,我决定在 Codepen 上开始一系列小型 WebGL 实验。今年早些时候,我终于抽出时间将它们放在一个名为 “Moments of Happiness” 的网站上。经过这些实验后,我已经找到了如何探索和学习不同 动画 和交互技术的方法,并在这些互动玩具中得以体现。

正如你将看到的,每个玩具的互动方式是非常不同的,但所有实验都有一个原则:每个角色的行为都以编程的方式响应用户的输入。没有预先计算的动画 —— 每个运动都在运行时定义。而赋予角色生命力的代码只有数行,但这也是我们的主要挑战。

定义动画和解释其意图
当一个动画与功能目的不匹配时,它常会让用户感到尴尬和懊恼。既然如此,这里有九个有助于验证功能性动画的逻辑目的。查看更多 ->

约束过程

这些实验主要基于 Three.jsGreenSock 库开发,完全手工编码,未使用任何 3D 或动画软件。

这个过程包括了以编程的方式将方块逐个组合成独特的角色。当然,我的大部分精力用在修改代码的值上,以调整比例、位置和整体渲染效果。然后,最终根据用户的输入(鼠标移动、点击和拖动等)移动角色的各个部分。

这个过程的优势并不明显。但这允许我能只使用文本编辑器来完成整个实验,从而避免导出资源和通过各个工具调整角色属性的麻烦。利用 Codepen 提供的实时预览功能,使整个过程非常灵活。

话虽如此,这个过程仍需要一套用于保证可管理性的自身约束:

  1. 角色必须由尽可能少的部分组成;
  2. 每个部分由少量顶点组成;
  3. 动画必须针对数量有限的行为。

注意:要明确的是,这个过程对我很有用,但如果你习惯于 3D 软件,那么你就应当用它来制作模型。即在你所掌握的技能中寻找适当的平衡,以尽可能高效。而当我将所有流程都放在一个工具中时,无疑会更高效。

Moments of Happiness
Moments of Happiness 是一个会让你快乐的 WebGL 实验系列。

将约束转化为机会

这个过程所需的极简主义,为寻找出最能精确描述每个表现(舒适、快乐、失望等)的动作提供了可能。

每个立方体和动作都会受到质疑:我真的需要它吗?它能让体验更佳,还仅仅是一个自封角色设计师的念头?

我最后得到的是非常简单的玩具,并且它们都生活在安静和简约的环境中。

The characters are mainly built with cubes
这些角色主要由立方体组成——即使是火焰和烟雾!

然而,这里最大的挑战可能是以编程方式让物体动起来。我们该如何在不使用动画软件或可视化时间轴的前提下构建自然生动的动作呢?我们该如何以动画的形式在响应用户输入的同时,保持动作自然呢?

步骤一:观察

在开始这些实验前,我花费了一些时间去观察、记录并思考传达何种情感。

受寒狮子的主要灵感来源是在我抚摸狗狗时产生的。我观察它在开心时是如何闭上眼睛,如何伸出脖子请求挠痒。然后寻找适当的算法以编程的方式翻译将这些动作,这就是情感与基础数学技能的融合。

Oh, that feeling!
噢,就是这种感觉!

对于“偏执鸟”(下图),我记得是模仿一个看起来不舒服的家伙,他拥有快速的眼神。为了让动作看起来更自然,我需要弄清楚它的眼睛和头部运动的时间差。

Capturing this awkward movement is just a matter of life experience.
要捕捉这种尴尬的动作也只是生活经验上的问题

但有时候,你不能只依赖于自身经验。视觉灵感有时是捕捉特征的必要条件。幸运的是,Giphy 上可以找到任意一种细微表情。我也花费了很多时间在 Youtube 和 Vimeo 上寻找适当的动作。

我们来看一个例子。

观察一个奔跑循环

“Moments of Happiness” 中最棘手的动画之一是 兔子逃出狼的魔爪

There are as many ways to run as there are reasons to flee.
有多少种逃跑方法就有多少种奔跑方式

要实现这个动作,首先要理解一个奔跑循环的运行方式。我在 Giphy 上看了一些令人激动的慢动作 GIF,直到我遇到了这一张:

若图片挂了,请复制此链接(或打开『图片来源』的链接):https://media.giphy.com/media/NmGbJwLl7Y4lG/giphy.gif
(图片来源: Giphy)

在该 GIF 中有趣的是,奔跑循环不仅是关于腿部的移动。它跟整个身体有关,也包括身体的最小部分,各个部分完全同步协调地动起来。耳朵,嘴巴,甚至是舌头的参与,无疑增强了速度和重力带来的效果。

事实上,动物种类和奔跑的原因都是决定奔跑循环的因素。如果你想深入研究奔跑循环,阅读其他更准确的参考资料也是个不错的想法。下面提供两个有用的资源:Pinteresr 上的 Run Cycle” collection 和 “Quadruped Locomotion Tutorial” 视频。

如果你有看过这些资料,那么每个奔跑循环的背后结构将会变得更加清晰。而且你的大脑会不自觉地开始捕捉身体各个部分之间的关系,而奔跑的顺序和节奏将以一种循环、可重复和可复用的形式展示。

现在,我们需要一个技术方案去实现它。

观察自动玩具

自动玩具 十分让人着迷,只需旋转一个手柄就能让这些机械玩具进行复杂的运动。我想尝试类似的技术,并探索一种更适合我们且基于代码的解决方案,而不是时间轴和关键帧。

该方案的实现思路是:无论循环动作简单与否,它都是完全依赖于主循环的处理

An automaton, a complex animation driven by one rotation of the handle
一个通过旋转手柄驱动自身运动的自动玩具,图片来自 Brinquedos Autômatos

对于奔跑循环,每条腿、耳朵、眼睛、身体和头部的运动都是由同一个主循环驱动。在某些情况下,所产生的旋转会被转换成水平运动或垂直运动(译者注:如圆周运动转换为半圆运动)。

当需要将圆周运动转为线性运动时,三角学似乎是最佳选择。

步骤 2:打磨武器,学习三角学

别走!这里所需的三角学种类是非常基础的。大多数公式就像这样:

x = cos(angle)*distance;
y = sin(angle)*distance;

这就是将点(angle, distance)的极坐标转换为笛卡尔坐标(x, y)的基础用法。

通过改变角度,可使点围绕着中心旋转。

Converting polar coordinates into Cartesian coordinates
将极坐标转换为笛卡尔坐标

只需修改公式的值,三角学就可让我们做更多复杂的运动。这种技术的美妙之处在于让运动变得平滑。

举个例子:

Examples of animations made thanks to trigonometry principles
通过三角学原理制作的动画案例

现在该你出手了

为了理解三角学,你必须亲自实践。没有实践的理论仅仅只是智力游戏。

为了实践上述公式,我们需要一个基本环境。你可以选用 Canvas、SVG 或任何具有图像 API 的库,如 Three.jsPixiJSBabylonJS.

让我们看看 Three.js 基础框架:

首先,下载最新版本的 Three.js,并将其在 htmlhead 引入:

<script type="text/javascript" src="js/three.js"></script>

然后添加整个实验的容器:

<div id="world"></div>

通过 CSS 样式将该容器覆盖浏览器视口:

#world {
	position: absolute;
	width: 100%;
	height: 100%;
	overflow: hidden;
	background: #ffffff;
}

JavaScript 部分有点长,但并不复杂:

// 初始化变量
var scene, camera, renderer, WIDTH, HEIGHT;
var PI = Math.PI;
var angle = 0;
var radius = 10;
var cube;
var cos = Math.cos;
var sin = Math.sin;

function init(event) {
  // 获取承载整个动画的容器
  var container = document.getElementById('world');

  // 获取窗口大小
  HEIGHT = window.innerHeight;
  WIDTH = window.innerWidth;

  // 创建 Three.js 场景,并设置摄像机和渲染器
  scene = new THREE.Scene();
  camera = new THREE.PerspectiveCamera( 50, WIDTH / HEIGHT, 1, 2000 );
  camera.position.z = 100;
  renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
  renderer.setSize(WIDTH, HEIGHT);
  renderer.setPixelRatio(window.devicePixelRatio ? window.devicePixelRatio : 1);
  container.appendChild(renderer.domElement);
  
  // 创建立方体
  var geom = new THREE.CubeGeometry(16,8,8, 1);
  var material = new THREE.MeshStandardMaterial({
    color: 0x401A07
  });
  cube = new THREE.Mesh(geom, material);

  // 将立方器放置在场景中
  scene.add(cube);

  // 创建并添加光源
  var globalLight = new THREE.AmbientLight(0xffffff, 1);
  scene.add(globalLight);

  // 监听 window 的 resize 事件
  window.addEventListener('resize', handleWindowResize, false);

  // 开启渲染每帧动画的循环
  loop();
}

function handleWindowResize() {
  // 如果 window 尺寸发生更改,就更新摄像机的长宽比
  HEIGHT = window.innerHeight;
  WIDTH = window.innerWidth;
  renderer.setSize(WIDTH, HEIGHT);
  camera.aspect = WIDTH / HEIGHT;
  camera.updateProjectionMatrix();
}

function loop(){
  // 每帧循环都会调用更新立方体位置的 update 函数
  update();

  // 渲染每帧场景
  renderer.render(scene, camera);
  
  // 调用下一帧 loop 函数
  requestAnimationFrame(loop);
}

// 页面加载后初始化案例
window.addEventListener('load', init, false);

现在我们创建了场景、摄像机、灯光和立方体。然后在循环中更新立方体的位置。

现在我们需要添加 update() 函数,并向函数体内添加一些三角公式:

function update(){
  // angle 每帧自增 0.1。值越大运动速度越高
  angle += .1;

  // 尝试修改 angle 和 radius,以获得不一样的动画
  cube.position.x = cos(angle) * radius;
  cube.position.y = sin(angle) * radius;

  // 如果你想看同样的原理放在对象的 rotation 属性上的效果,请取消下一句的注释
  //cube.rotation.z = cos(angle) * PI/4;

  // 或者修改 scale。注意加 1 是为了避免计算结果为负值
  //cube.scale.y = 1 + cos(angle) * .5;

  /*
  轮到你!你可能想:
  - 注释或取消注释上面的某行代码,以产生新的组合
  - 用 sin 替换 cos,或相反
  - 用其他周期函数替换 radius
  */
  例如:
  cube.position.x = cos(angle) * (sin(angle) * radius)
  ...
}

如果存在疑惑,可以看看 Codepen。正弦和余弦函数让立方体以不同方式进行移动。希望通过该案例,能让你更好地了解如何在动画中使用三角学。

或者你可以查看下一个案例,并将其作为步行循环或奔跑循环的入门案例。

如何利用三角学制作步行循环或奔跑循环

利用之前通过代码移动立方体的三角学知识,我们接下来将一步步地制作一个简单的步行循环。

与之前代码基本一致,主要不同的地方在于需要更多的立方体组成身体的各个部分。

Three.js 可以将对象组嵌入到其他对象组中。例如,我们可以创建一个含有腿部、手臂和头部的 body 组。

看看角色的制作过程:

Hero = function() {
  // 用于循环的旋转角度变量,每帧自增
  this.runningCycle = 0;
  // 创建放置 body 的网格(mesh)
  this.mesh = new THREE.Group();
  this.body = new THREE.Group();
  this.mesh.add(this.body);

  // 创建并放置在 body 的各个肢体
  var torsoGeom = new THREE.CubeGeometry(8,8,8, 1);
  this.torso = new THREE.Mesh(torsoGeom, blueMat);
  this.torso.position.y = 8;
  this.torso.castShadow = true;
  this.body.add(this.torso);

  var handGeom = new THREE.CubeGeometry(3,3,3, 1);
  this.handR = new THREE.Mesh(handGeom, brownMat);
  this.handR.position.z=7;
  this.handR.position.y=8;
  this.body.add(this.handR);

  this.handL = this.handR.clone();
  this.handL.position.z = - this.handR.position.z;
  this.body.add(this.handL);

  var headGeom = new THREE.CubeGeometry(16,16,16, 1);
  this.head = new THREE.Mesh(headGeom, blueMat);
  this.head.position.y = 21;
  this.head.castShadow = true;
  this.body.add(this.head);

  var legGeom = new THREE.CubeGeometry(8,3,5, 1);

  this.legR = new THREE.Mesh(legGeom, brownMat);
  this.legR.position.x = 0;
  this.legR.position.z = 7;
  this.legR.position.y = 0;
  this.legR.castShadow = true;
  this.body.add(this.legR);

  this.legL = this.legR.clone();
  this.legL.position.z = - this.legR.position.z;
  this.legL.castShadow = true;
  this.body.add(this.legL);

  // 确保各个肢体能投射和接收阴影
  this.body.traverse(function(object) {
    if (object instanceof THREE.Mesh) {
      object.castShadow = true;
      object.receiveShadow = true;
    }
  });
}

将这个角色放置在场景(scene)中:

function createHero() {
  hero = new Hero();
  scene.add(hero.mesh);
}

这是通过 Three.js 制作的简易角色。如果你想了解有关使用 Three.js 制作角色的更多信息,请阅读我在 Codrops 上编写的详细教程

完成 body 的构建后,我们将逐步地为每个肢体添加动画,直至形成一个简单的步行循环。

整个逻辑都放在 Hero 对象的 run 函数中:

Hero.prototype.run = function(){

  // angle 自增
  this.runningCycle += .03;
  var t = this.runningCycle;

  // 确保 angle 在 0 ~ 2PI 的区间内
  t = t % (2*PI);

  // 幅度(Amplitude)用作腿部移动的主要半径
  var amp = 4;

  // 更新各个肢体的位置和旋转
  this.legR.position.x =  Math.cos(t) * amp;
  this.legR.position.y = Math.max (0, - Math.sin(t) * amp);

  this.legL.position.x =  Math.cos(t + PI) * amp;
  this.legL.position.y = Math.max (0, - Math.sin(t + PI) * amp);

  if (t<PI){
    this.legR.rotation.z = Math.cos(t * 2 + PI/2) * PI/4;
    this.legL.rotation.z = 0;
  } else{
    this.legR.rotation.z = 0;
    this.legL.rotation.z = Math.cos(t * 2 + PI/2) *  PI/4;
  }

  this.torso.position.y = 8 - Math.cos(  t * 2 ) * amp * .2;
  this.torso.rotation.y = -Math.cos( t + PI ) * amp * .05;

  this.head.position.y = 21 - Math.cos(  t * 2 ) * amp * .3;
  this.head.rotation.x = Math.cos( t ) * amp * .02;
  this.head.rotation.y =  Math.cos( t ) * amp * .01;

  this.handR.position.x = -Math.cos( t ) * amp;
  this.handR.rotation.z = -Math.cos( t ) * PI/8;
  this.handL.position.x = -Math.cos( t + PI) * amp;
  this.handL.rotation.z = -Math.cos( t + PI) * PI/8;
}

每行代码都十分有趣,你可以在 Codepen 上找到步行循环的完整代码

为了让它更易理解,我制作了以下案例,它将步行循环进行分解,突出显示身体被移动的部分,及其每一步所使用的公式。

codepen1

See the Pen Walking cycle breakdown by Karim Maaloul (@Yakudoo) on CodePen.

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

一旦你对正弦、余弦、距离和频率运用自如,那么各类循环(如跑步、游泳、飞行,甚至是月球漫步)的制作就变得轻而易举了。

到你了!

我不会让你没有兔子玩。

下面的 Codepen 能让你对身体的各个部分应用不同的 angle 增量和幅度(amplitude)。你还可以修改循环速率,以获得更疯狂的效果。

你能为这个家伙想出不同的奔跑循环吗?玩得开心!

codepen2

See the Pen Run bunny run by Karim Maaloul (@Yakudoo) on CodePen.

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

总结

人们可能认为基于代码的动画会导致动作不自然。相反,我相信这大大提高了调整动作的灵活性。同时,这也让角色更容易实现令人欣赏的动作。

Moments of Happiness 是各个实验的集合,而每个实验都具有其挑战性。在本文中,我已详细地介绍了制作奔跑循环的解决方案。另外,在我的 Codepen 页面 中,都可以找到这些实验,并且代码可任意使用。尽情创造属于你的互动玩具吧。