簡評:做者從理論到實踐演示如何學習使用 WebGL 製做動畫。javascript
主要使用 three.js 和 GreenSock 庫,這些實驗都是手動編碼的,沒有憑藉任何 3D 或動畫軟件。html
這個過程包括以編程的方式塑造角色,一次一個方塊。在精煉比例上我花費了大多數工夫。經過微調代碼中的值來整體渲染位置,而後經過用戶輸入(大可能是移動鼠標,點擊,拖動等等)來移動每一個部分。java
這個過程的優勢不是很明顯。但它能讓我僅僅經過文本編輯器就能創造整個實驗,利用 Codepen 提供的實時預覽,整個過程很是靈活。git
話雖如此,這個過程有本身的一套限制,以保持可管理性:角色必須用盡量少的部分構建; 每一個部分由數量很小的頂點組成; 動畫必須針對數量有限的行爲。github
注意:要清楚一點,這個過程對於我來講是有用的,但若是你熟悉 3D 軟件,你能夠用它來構建本身的模型。主要是在你本身的技巧和效率中儘量達到平衡。web
Moments of Happiness 是一系列讓你開心的 WebGL 體驗。算法
這個過程的關鍵在於找到描述每一個行爲(溫馨,快樂,失望等)的最準確動做。編程
每一個方塊和每一個動做都要質問:我真的須要嗎?這會讓體驗更好,仍是僅僅心血來潮?bash
這個角色主要由方塊組成—— 甚至是火焰和煙霧!app
以編程的方式來動畫化東西多是最大的挑戰。不用任何動畫軟件或者可視化時間線你如何構建天然的動做?如何讓動畫在響應用戶輸入的時候仍然保持天然呢?
在開始任何實驗以前,我都會花一些時間觀察,記住我想要傳達的感受。我創造讓獅子涼快這個動畫的時候,養狗給了我極大的靈感。我觀察它如何舒服的閉上眼睛,而後伸長脖子讓我幫它撓癢癢。
找到正確的算法,以編程的方式翻譯,這是一種同理心和基礎數學技巧的混合。
對於「偏執鳥」(下面的),我記住了模仿一個看起來很不舒服的人一瞥的感受,試圖經過弄清楚他的眼睛和頭部運動分離了多少時間,使行爲看起來使人信服。
但有時候,你不能僅僅依賴於本身的經驗,爲了抓住一些特質,可視化的靈感有時候是必要的。幸運的是,這有 Giphy,你能夠找到任何類型的微妙的表情。我同時還在 YouTube 和 Vimeo 上位了尋找正確的動做花費了許多時間。
我在 Moments of Happiness 中製做的其中一個最須要技巧的動畫是 兔子大逃亡。
要完成這個,首先要理解奔跑週期的原理。我在 Giphy 上找到一個慢放的 GIF。
有趣的是這個 GIF 上不只僅提示了跑動的腿,還有整個身體,包括最細微的部分。
別跑開!這裏須要的三角函數類型很是基礎。大多數形式看起來就像這樣:
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 函數可讓方塊以不一樣的方式運動,這可讓你更好地理解若是使用三角函數的優點製做你的動畫。
或者你能夠直接跳到下個例子來做爲製做本身走路或奔跑週期的起點。
如今,咱們學習瞭如何用代碼來讓方塊移動,用一樣的方式,咱們將一步步地製做一個簡單的步行週期動畫。
咱們將使用和以前相同的設置,主要的不一樣在於構建不一樣的身體部位。
使用 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,天天導讀三篇國外技術類文章,歡迎投稿和關注。