用 Three-js 畫個 3D 生日蛋糕送給他(她)

作爲整天和 UI 打交道的前端工程師,是否想在他(她)生日的時候用代碼送上一份驚喜呢?

不妨用 Three.js 做個 3D 的蛋糕送給 ta,既浪漫又能展現你技術的魅力。

這篇文章我們就來學習下如何用 Three.js 畫一個蛋糕。

代碼地址:https://github.com/QuarkGluonPlasma/threejs-exercize

Three.js 相關基礎

Three.js 是通過場景 Scene 來管理所有的物體的,加到 Scene 的物體還可以分個組:

const scene = new THREE.Scene();

scene.add(xxx);

const group = new THREE.Group();
group.add(yyy);
group.add(zzz);

scene.add(group);

想要把 Scene 中的所有物體渲染出來,需要指定一個相機 camera,然後用 renderer 來渲染,如果有動畫效果,要用 requestAnimationFrame 來一幀幀不斷渲染。

const renderer = new THREE.WebGLRenderer();

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

相機 camera 分爲從一個點去看的透視相機 PerspectiveCamera,還有從一個面去投影的正交相機 OrthographicCamera。

透視相機的特點是近大遠小,而正交的則不是,就是一個平行投影,大小不變。

三維世界還需要指定一個光源,不然是全黑的,光源種類很多,常用的有這些:

三維場景中的物體有很多種,比如永遠面向相機的平面是 Sprite(我們做 “漫天花雨” 效果用的那個),還有由三角形構成的物體叫做 Mesh。

Mesh 比較常用,它是由一個個三角形構成的幾何體,還可以在每個面上貼圖。所以,參數有兩個,幾何體 Geometry 和材質 Material。

比如圓柱體就是一個 Mesh,創建它的時候要指定圓柱幾何體 CylinderBufferGeometry 和每個面的材質 Material。

const 圓柱幾何體 = new THREE.CylinderBufferGeometry(上圓半徑, 下圓半徑, 高度, 側面分段數量);

const 側面材質 = new THREE.MeshBasicMaterial({map: 紋理圖片});
const 上面材質 = new THREE.MeshBasicMaterial({color: 'red'});
const 下面材質 = new THREE.MeshBasicMaterial({color: 'red'});

const 圓柱 = new THREE.Mesh(圓柱幾何體, [側面材質, 上面材質, 下面材質]);

MeshBasicMaterial 是基礎的材質,可以通過 color 來指定顏色,也可以通過 map 來指定紋理圖片 texture。

各種 Mesh 中比較特殊是文字,它用的是 TextGeometry,文字需要從一個 xxx.typeface.json 中加載。

而這種 json 文件可以用字體文件 ttf 來轉換得到。用 ttf 轉 typeface.json 的這個網站來轉:

之後就可以顯示文字了:

const fontLoader = new THREE.FontLoader();

fontLoader.load('./font/xxx.typeface.json'function (font) {
    var textGeometry = new THREE.TextGeometry('文字', 參數);
    const textMaterial = [
        new THREE.MeshBasicMaterial({color: '字體顏色'}),
        new THREE.MeshBasicMaterial({color: '側面顏色'}),
    ];

    const text = new THREE.Mesh(textGeometry, textMaterial);
});

這些就是我們會用到的 Three.js 基礎,簡單做個小結:

Three.js 是通過 Scene 來管理各種物體的,物體還可以分下組。

物體中常見的有 Mesh 和 Sprite 等,Sprite 是永遠面向相機的一個平面,Mesh 是由三角形構成的三維物體。Mesh 要指定幾何體 Geometry 和材質 Material,常用的材質可以是顏色或者紋理貼圖。其中文字 TextGeometry 比較特殊,需要一個 typeface.json 的文件,這個可以由 ttf 轉換得到。

場景中的物體準備好之後,還需要設置下光源 Light 和相機 Camera,相機主要有從點去看的透視相機和從一個平面去投影的正交相機,之後就可以通過渲染器 Renderer 渲染出來了,結合 requestAnimationFrame 來一幀幀的渲染。

基礎學完之後,正式開始畫蛋糕了。

畫 3D 蛋糕

蛋糕其實就是由 4 個圓柱體加上文字構成的,每個圓柱體都設置了不同的位置,圓柱體的側面和上下面都貼上不同的貼圖,就是一個蛋糕。

我們先準備蛋糕的貼圖:

使用紋理加載器 TextureLoader 去加載他們:

const cakeTexture1 = new THREE.TextureLoader().load('img/cake1.png');
const cakeTexture2 = new THREE.TextureLoader().load('img/cake2.png');
const cakeTexture3 = new THREE.TextureLoader().load(`img/cake3.png`);
const cakeTexture4 = new THREE.TextureLoader().load('img/cake4.png');

然後構成紋理貼圖的材質:

const cakeMaterail1 = new THREE.MeshBasicMaterial({map: cakeTexture1});
const cakeMaterail2 = new THREE.MeshBasicMaterial({map: cakeTexture2});
const cakeMaterail3 = new THREE.MeshBasicMaterial({map: cakeTexture3});
const cakeMaterail4 = new THREE.MeshBasicMaterial({map: cakeTexture4});

除了紋理貼圖的材質外,再準備個顏色構成的材質:

const pinkMaterial = new THREE.MeshBasicMaterial({color: 'pink'});

然後創建 4 個圓柱體的物體(Mesh),使用不同的貼圖材質和顏色材質:

const cakeGeometry1 = new THREE.CylinderBufferGeometry(100, 100, 70, 40);
const cakePart1 = new THREE.Mesh(cakeGeometry1, [cakeMaterail1, pinkMaterial, pinkMaterial]);

圓柱體的幾何體 CylinderBufferGeometry 的參數分別是 上面圓的半徑,下面圓的半徑,高度,側面的分割次數。

上面圓半徑保持一致,這樣纔是圓柱體。側面分割次數設置爲 40,這樣比較圓滑。

之後還設置下位移,然後就可以加到蛋糕分組裏了。

我們用同樣的方式創建四個圓柱體,設置不同的大小和位置,貼不同的圖:

const cakeGeometry1 = new THREE.CylinderBufferGeometry(100, 100, 70, 40);
const cakePart1 = new THREE.Mesh(cakeGeometry1, [cakeMaterail1, pinkMaterial, pinkMaterial]);
cakePart1.translateY(45)

const cakeGeometry2 = new THREE.CylinderBufferGeometry(120, 120, 70, 40);
const cakePart2 = new THREE.Mesh(cakeGeometry2,[cakeMaterail3, pinkMaterial, pinkMaterial]);
cakePart2.translateY(-25)

const cakeGeometry3 = new THREE.CylinderBufferGeometry(140, 140, 60, 40);
const cakePart3 = new THREE.Mesh(cakeGeometry3, [cakeMaterail2, pinkMaterial, pinkMaterial]);
cakePart3.translateY(-90)

const cakeGeometry4 = new THREE.CylinderBufferGeometry(160, 160, 10, 40);
const cakePart4 = new THREE.Mesh(cakeGeometry4, [cakeMaterail4, cakeMaterail4, cakeMaterail4]);
cakePart4.translateY(-120)

cake.add(cakePart1)
cake.add(cakePart2)
cake.add(cakePart3)
cake.add(cakePart4)

如果對座標位置拿不準,可以在 Scene 中加上一個座標的輔助工具 AxisHelper。參數是座標軸長度。

const axisHelper = new THREE.AxisHelper(2500);
scene.add(axisHelper);

然後是文字的部分,這個要先通過字體文件 ttf 轉成 typeface.json 的文件,然後用 fontLoader 來加載,之後創建相應的 Mesh:

fontLoader.load('./font/guang.typeface.json'function (font) {
    var textGeometry = new THREE.TextGeometry('光光'{
        font: font,
        size: 30,
        height: 5,
        bevelEnabled: true,
        bevelSize: 10,
    });
    const textMaterial = ['white''red'].map(color => new THREE.MeshBasicMaterial({color}));

    const text = new THREE.Mesh(textGeometry, textMaterial);
    text.translateY(90)
    text.translateX(-45)

    cake.add(text); 
});

TextGeometry 需要設置的參數有字體大小 size,厚度 height,以及邊緣是否是曲面 bevelEnabled,和曲面的大小 bevelSize。

我們這裏的效果是需要開啓曲面的。

4 個圓柱體畫完了,文字也畫完了,那蛋糕就算是畫完了,之後設置下光源、相機,就可以用 Renderer 渲染了。

光源使用環境光,因爲要均勻的照射:

const light = new THREE.AmbientLight(0xCCCCCC);
scene.add(light);

相機使用正交相機,因爲不需要近大遠小的透視效果:

const width = window.innerWidth;
const height = window.innerHeight;
//窗口寬高比
const k = width / height;
//三維場景顯示範圍的高度
const s = 200;

const camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);

camera.position.set(0, 100, 500)
camera.lookAt(scene.position);

正交相機的參數分別是左右上下遠近的三維視野範圍,我們指定高度爲 200,然後根據窗口的寬高比算出寬度。遠近可以設置一個比較大的範圍。

之後就可以用 Renderer 來渲染了。把渲染出的 canvas 的 dom 掛載到 body 上。

const renderer = new THREE.WebGLRenderer();

renderer.setSize(width, height);
//設置背景顏色
renderer.setClearColor(0xFFFFFF, 1);
document.body.appendChild(renderer.domElement);

function render() {        
    renderer.render(scene, camera);

    cake.rotation.y += 0.005;

    requestAnimationFrame(render)
}
render()

在每幀 render 之前,還做了個圍繞 y 軸的自動旋轉。

還要支持手動的旋轉,這個直接使用 Three.js 的軌道控制器 OrbitControls 就行。

const controls = new THREE.OrbitControls(camera);

參數是相機,因爲這種視野的改變就是通過改變相機位置和朝向來實現的。

創建了 Scene 中的蛋糕的每一部分,設置好了光源、相機,用渲染器做了一幀幀的渲染,並且添加了用鼠標來改變視角的軌道控制器之後,就完成了 3D 蛋糕的製作。

我們來看下效果:

代碼地址:https://github.com/QuarkGluonPlasma‍/threejs-exercize

全部代碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>生日蛋糕</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
        }
    </style>
    <script src="./js/three.js"></script>
    <script src="./js/OrbitControls.js"></script>
</head>
<body>
<script>
    const width = window.innerWidth;
    const height = window.innerHeight;
    //窗口寬高比
    const k = width / height;
    //三維場景顯示範圍的寬度
    const s = 200;

    const camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);

    const fontLoader = new THREE.FontLoader();

    const scene = new THREE.Scene();

    const cake = new THREE.Group();

    const renderer = new THREE.WebGLRenderer();


    function create() {
        renderer.setSize(width, height);
        //設置背景顏色
        renderer.setClearColor(0xFFFFFF, 1);
        document.body.appendChild(renderer.domElement);

        camera.position.set(0, 100, 500)
        camera.lookAt(scene.position);

        const light = new THREE.AmbientLight(0xCCCCCC);
        scene.add(light);

        const axisHelper = new THREE.AxisHelper(2500);
        scene.add(axisHelper);

        const cakeTexture1 = new THREE.TextureLoader().load('img/cake1.png');
        const cakeTexture2 = new THREE.TextureLoader().load('img/cake2.png');
        const cakeTexture3 = new THREE.TextureLoader().load(`img/cake3.png`);
        const cakeTexture4 = new THREE.TextureLoader().load('img/cake4.png');

        const cakeMaterail1 = new THREE.MeshBasicMaterial({map: cakeTexture1});
        const cakeMaterail2 = new THREE.MeshBasicMaterial({map: cakeTexture2});
        const cakeMaterail3 = new THREE.MeshBasicMaterial({map: cakeTexture3});
        const cakeMaterail4 = new THREE.MeshBasicMaterial({map: cakeTexture4}); 

        const pinkMaterial = new THREE.MeshBasicMaterial({color: 'pink'});

        const cakeGeometry1 = new THREE.CylinderBufferGeometry(100, 100, 70, 40);
        const cakePart1 = new THREE.Mesh(cakeGeometry1, [cakeMaterail1, pinkMaterial, pinkMaterial]);
        cakePart1.translateY(45)
 
        const cakeGeometry2 = new THREE.CylinderBufferGeometry(120, 120, 70, 40);
        const cakePart2 = new THREE.Mesh(cakeGeometry2,[cakeMaterail3, pinkMaterial, pinkMaterial]);
        cakePart2.translateY(-25)

        const cakeGeometry3 = new THREE.CylinderBufferGeometry(140, 140, 60, 40);
        const cakePart3 = new THREE.Mesh(cakeGeometry3, [cakeMaterail2, pinkMaterial, pinkMaterial]);
        cakePart3.translateY(-90)

        const cakeGeometry4 = new THREE.CylinderBufferGeometry(160, 160, 10, 40);
        const cakePart4 = new THREE.Mesh(cakeGeometry4, [cakeMaterail4, cakeMaterail4, cakeMaterail4]);
        cakePart4.translateY(-120)

        cake.add(cakePart1)
        cake.add(cakePart2)
        cake.add(cakePart3)
        cake.add(cakePart4)

        fontLoader.load('./font/guang.typeface.json'function (font) {
            var textGeometry = new THREE.TextGeometry('光光'{
                font: font,
                size: 30,
                height: 5,
                bevelEnabled: true,
                bevelSize: 10,
            });
            const textMaterial = ['white''red'].map(color => new THREE.MeshBasicMaterial({color}));

            const text = new THREE.Mesh(textGeometry, textMaterial);
            text.translateY(90)
            text.translateX(-45)
            cake.add(text); 
        });

        scene.add(cake);
    }


    function render() {        
        renderer.render(scene, camera);

        cake.rotation.y += 0.005;

        requestAnimationFrame(render)
    }

    create()
    render()

    const controls = new THREE.OrbitControls(camera);
</script>
</body>
</html>

總結

本文我們用 Three.js 來實現了 3D 蛋糕的效果。

首先我們學習了下 Three.js 的基礎:通過 Scene 來管理物體,物體可以分組,物體包括 Mesh、Sprite 等,Mesh 是三角形構成的 3D 物體,要分別指定幾何體 Geometry 和材質 Material。材質可以是紋理(Texture)貼圖、也可以是顏色。其中文字的 Mesh 需要做 ttf 到 typeface.json 的轉換,加載這個 json 才能顯示文字。

物體創建完了之後,還要設置相機、燈光等,然後通過渲染器來一幀幀的渲染。

調試的時候還可以添加 AxisHelper 座標系輔助工具來輔助開發。

然後我們實現了 3D 蛋糕:

通過 4 個圓柱體 + 文字來畫的,圓柱體用了不同的紋理貼圖材質,設置了不同的位置,然後組成蛋糕的 group。

設置了環境光,使用了正交相機,還啓用了軌道控制器 OrbitControls,來實現鼠標拖拽改變相機位置,進而改變視野角度的效果。

下個他(她)的生日,不妨試試用 Three.js 畫個蛋糕送給他(她),或許會有不一樣的收穫哦。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/r0lUIIbcHGSz_DyrCqwPag