簡評:作者從理論到實踐演示如何學習使用 WebGL 製作動畫。
約束過程
主要使用 three.js 和 GreenSock 庫,這些實驗都是手動編碼的,沒有憑藉任何 3D 或動畫軟體。
這個過程包括以程式設計的方式塑造角色,一次一個方塊。在精煉比例上我花費了大多數工夫。通過微調程式碼中的值來總體渲染位置,然後通過使用者輸入(大多是移動滑鼠,點選,拖動等等)來移動每個部分。
這個過程的優點不是很明顯。但它能讓我僅僅通過文字編輯器就能創造整個實驗,利用 Codepen 提供的實時預覽,整個過程非常靈活。
話雖如此,這個過程有自己的一套限制,以保持可管理性:角色必須用盡可能少的部分構建; 每個部分由數量很小的頂點組成; 動畫必須針對數量有限的行為。
注意:要清楚一點,這個過程對於我來說是有用的,但如果你熟悉 3D 軟體,你可以用它來構建自己的模型。主要是在你自己的技巧和效率中儘可能達到平衡。
Moments of Happiness 是一系列讓你開心的 WebGL 體驗。
把約束變成機會
這個過程的關鍵在於找到描述每個行為(舒適,快樂,失望等)的最準確動作。
每個方塊和每個動作都要質問:我真的需要嗎?這會讓體驗更好,還是僅僅心血來潮?
這個角色主要由方塊組成—— 甚至是火焰和煙霧!
以程式設計的方式來動畫化東西可能是最大的挑戰。不用任何動畫軟體或者視覺化時間線你如何構建自然的動作?如何讓動畫在響應使用者輸入的時候仍然保持自然呢?
步驟 1: 觀察
在開始任何實驗之前,我都會花一些時間觀察,記住我想要傳達的感覺。我創造讓獅子涼快這個動畫的時候,養狗給了我極大的靈感。我觀察它如何舒服的閉上眼睛,然後伸長脖子讓我幫它撓癢癢。
找到正確的演算法,以程式設計的方式翻譯,這是一種同理心和基礎數學技巧的混合。
對於“偏執鳥”(下面的),我記住了模仿一個看起來很不舒服的人一瞥的感覺,試圖通過弄清楚他的眼睛和頭部運動分離了多少時間,使行為看起來令人信服。
但有時候,你不能僅僅依賴於自己的經驗,為了抓住一些特質,視覺化的靈感有時候是必要的。幸運的是,這有 Giphy,你可以找到任何型別的微妙的表情。我同時還在 YouTube 和 Vimeo 上位了尋找正確的動作花費了許多時間。
觀察奔跑週期
我在 Moments of Happiness 中製作的其中一個最需要技巧的動畫是 兔子大逃亡。
要完成這個,首先要理解奔跑週期的原理。我在 Giphy 上找到一個慢放的 GIF。
有趣的是這個 GIF 上不僅僅提示了跑動的腿,還有整個身體,包括最細微的部分。
步驟 2: 磨刀不誤砍柴工,學習三角函式
別跑開!這裡需要的三角函式型別非常基礎。大多數形式看起來就像這樣:
x = cos(angle)*distance;
y = sin(angle)*distance;複製程式碼
這基本上用於將點(角度,距離)的極座標轉換為笛卡爾座標(x,y)。
通過角度變化,我們可以讓點圍著中心旋轉。
感謝三角函式,我們才可以做許多複雜的動作,只需設定公式的不同值即可。這種技術的漂亮之處在於你從動作中獲取的平滑度。
下面是一些案例:
現在,到你了
要理解三角函式,你必須親自實踐。光講理論僅僅是假把式。
為了實現上面的一些公式,我們需要安裝基礎環境。用畫布,SVG 或者其他任何圖形 API,如three.js, PixiJS 或者 BabylonJS 都能完成。
讓我們來使用非常基礎的 three.js 開始。
首先,下載最新版本的 three.js,然後在 html 頭部中匯入這個庫:
<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 部分有點長,但也不復雜:
// Initialize variables.
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) {
// Get the container that will hold the animation.
var container = document.getElementById('world');
// Get window size.
HEIGHT = window.innerHeight;
WIDTH = window.innerWidth;
// Create a three.js scene; set up the camera and the renderer.
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);
// Create the cube.
var geom = new THREE.CubeGeometry(16,8,8, 1);
var material = new THREE.MeshStandardMaterial({
color: 0x401A07
});
cube = new THREE.Mesh(geom, material);
// Add the cube to the scene.
scene.add(cube);
// Create and add a light source.
var globalLight = new THREE.AmbientLight(0xffffff, 1);
scene.add(globalLight);
// Listen to the window resize.
window.addEventListener('resize', handleWindowResize, false);
// Start a loop that will render the animation in each frame.
loop();
}
function handleWindowResize() {
// If the window is resized, we have to update the camera aspect ratio.
HEIGHT = window.innerHeight;
WIDTH = window.innerWidth;
renderer.setSize(WIDTH, HEIGHT);
camera.aspect = WIDTH / HEIGHT;
camera.updateProjectionMatrix();
}
function loop(){
// Call the update function in each frame to update the cube position.
update();
// Render the scene in each frame.
renderer.render(scene, camera);
// Call the loop function in next frame.
requestAnimationFrame(loop);
}
// Initialize the demo when the page is loaded.
window.addEventListener('load', init, false);複製程式碼
這裡,我們基本上建立了一個場景,一個相機,一個燈光和一個方塊。然後,我們開始了一個迴圈來更新方塊每幀的位置。
現在,我們需要新增 update() 函式,我們可以插入一些三角函式的公式:
function update(){
// The angle is incremented by 0.1 every frame. Try higher values for faster animation.
angle += .1;
// Try modifying the angle and/or radius for a different movement.
cube.position.x = cos(angle) * radius;
cube.position.y = sin(angle) * radius;
// You might want to use the same principle on the rotation property of an object. Uncomment the next line to see what happens.
//cube.rotation.z = cos(angle) * PI/4;
//Or vary the scale. Note that 1 is added as an offset to avoid a negative scale value.
//cube.scale.y = 1 + cos(angle) * .5;
/*
Your turn! You might want to:
- comment or uncomment the lines above to try new combinations,
- replace cos by sin and vice versa,
- replace radius with an other cyclic function.
For example :
cube.position.x = cos(angle) * (sin(angle) *radius);
...
*/
}複製程式碼
如果你感覺迷路了,你可以在 Codepen(很酷的線上編輯器,提供實時預覽) 中開啟這個專案。使用 sine 或者 cosine 函式可以讓方塊以不同的方式運動,這可以讓你更好地理解如果使用三角函式的優勢製作你的動畫。
或者你可以直接跳到下個例子來作為製作自己走路或奔跑週期的起點。
步驟 3:如何使用三角函式製作步行或奔跑動畫
現在,我們學習瞭如何用程式碼來讓方塊移動,用同樣的方式,我們將一步步地製作一個簡單的步行週期動畫。
我們將使用和之前相同的設定,主要的不同在於構建不同的身體部位。
使用 three.js,在一組物件中嵌入另一組物件是可能的。例如,我們可以建立一個身體組包括腿,手臂和頭。
讓我們看看是怎麼來的:
Hero = function() {
// This will be incremented later at each frame and will be used as the rotation angle of the cycle.
this.runningCycle = 0;
// Create a mesh that will hold the body.
this.mesh = new THREE.Group();
this.body = new THREE.Group();
this.mesh.add(this.body);
// Create the different parts and add them to the 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);
// Ensure that every part of the body casts and receives shadows.
this.body.traverse(function(object) {
if (object instanceof THREE.Mesh) {
object.castShadow = true;
object.receiveShadow = true;
}
});
}複製程式碼
現在我們需要把這個模型加入到場景中:
function createHero() {
hero = new Hero();
scene.add(hero.mesh);
}複製程式碼
使用 three.js 建立一個模型就是這麼簡單。如果想學習更多關於 three.js 建立模型的知識,可以參考我在 Codrops 上的詳細指導。
建立這個身體後,我們將要讓這些部分一個接一個移動起來,知道達成一個簡單的步行動畫。
整體邏輯在於 Hero 物件中的 run 函式:
Hero.prototype.run = function(){
// Increment the angle.
this.runningCycle += .03;
var t = this.runningCycle;
// Ensure that the angle we will use is between 0 and 2 Pi.
t = t % (2*PI);
// Amplitude is used as the main radius of the legs movement.
var amp = 4;
// Update the position and rotation of every part of the body.
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 中找到步行動畫所有的程式碼。(裡面有每一步的演示結果)
一旦你熟練掌握 sine 和 consine 函式,距離和頻率,製作不同的週期動畫也會變得相當簡單,比如奔跑,游泳,飛翔,甚至月球漫步。
到你了!
作者還給出了一個兔子奔跑的動畫,有興趣可以試試。點選 Codepen 連結檢視。
知乎專欄:極光日報
原文連結:Exploring Animation And Interaction Techniques With WebGL (A Case Study)
極光日報,極光開發者 的 Side Project,每天導讀三篇國外技術類文章,歡迎投稿和關注。