通過漫天花雨來入門 Three.js
隨著元宇宙概念的火爆,3D 渲染相關的技術頻繁被提及,而 Three.js 是基于 WebGL 的 api 封裝的用于簡化 3D 場景的開發的框架, 是入門 3D 的不錯的抓手,今天我們就來入門下 Three.js。
我們基于 Three.js 來實現一個花瓣雨的效果。
Three.js 的基礎
Three.js 用于渲染一個 3D 的場景,里面會有很多物體,比如立方體、圓柱、圓環、圓錐等各種幾何體(以 Geometry 為后綴),比如點(Points)線(Line)面(Sprite)等基礎物體。這些所有的物體怎么管理呢?
用一個場景 Scene 來承載,所有的物體都會被添加到 Scene 里。
所以有這樣的 api:
- const scene = new THREE.Scene();
- scene.add(xxx);
- scene.add(yyy);
當然,物體之間可以做分組 Group,組內的物體可以統一管理,之后再添加到 Scene 里。
- const scene = new THREE.Scene();
- const group = new THREE.Group();
- group.add(xxx);
- group.add(yyy);
- scene.add(group);
這種場景、物體、分組的概念,在很多游戲引擎中也有類似的 api,大家都是這么來管理的。
可以添加到 Scene 中的物體,除了幾何體(Geometry)、點線面等,還有輔助工具,比如坐標系工具(AxisHelper)。其實這些工具也是用集合體、點線面封裝出來的,只不過是作為工具來臨時添加到 Scene 中。
- const axisHelper = new THREE.AxisHelper(max);
- scene.add(axisHelper)
有了場景和場景中的各種物體,怎么渲染出來呢?
調用 Renderer,這個類是專門負責渲染 Scene 中各種物體的。
但是還有個問題,三維的世界(scene)怎么渲染到二維的屏幕呢?
如圖,從一個點找個角度來看三維世界,或者從一個平面來平行的看三維世界,看到的就是二維的。
這兩種方式,第一種叫做透視、第二種叫做正交。
生成二維圖像,就像照相機的功能一樣,所以這種概念叫做 Camera。
在 Three.js 里面有 PerspectiveCamera (透視相機)和 OrthographicCamera(正交相機),分別對應上面兩種三維轉二維的方式。
這兩個 Camera 的參數還是挺多的,但是理解了也挺簡單:
- new Three.PerspectiveCamera( fov, aspect, near, far )
- new Three.OrthographicCamera( left, right, top, bottom, near, far )
先看透視相機的,它要看三維世界,那就要有一個最近和最遠兩個位置,然后從一個點看過去會有一個視野的角度,看到的畫面還有個寬高比。
這就是為什么 PerspectCamera 有 near、far、fov、aspect 這四個參數。
正交相機的參數也是差不多的意思,不過因為不是從一個點,看的,而是從一個面做的投影,那么就沒有角度的參數,而是有上下左右的四個面位置的參數。
正交相機的上下左右位置也不是隨便的,比例要和畫面的寬高比一樣,所以一般都是這么算:
- const width = window.innerWidth;
- const height = window.innerHeight;
- //窗口寬高比
- const k = width / height;
- //三維場景的顯示的上下范圍
- const s = 200;
- // 上下范圍 s 再乘以寬高比 k 就是左右的范圍,而遠近隨便設置一個數就行
- const camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
上面的正交相機的參數里面,遠近可以設置為 1 和 1000,上下設置為 200,左右就可以根據寬高比算出來。這就是相機所看到的二維畫面的范圍。
有了場景 Scene 中的各種物體,有了照相機 Camera,就可以用渲染器 Renderer 渲染出畫面來了。
- const renderer = new THREE.WebGLRenderer();
- //設置渲染區域尺寸
- renderer.setSize(width, height)
- renderer.render(scene, camera)
不過,一般不會只渲染一幀,有動畫效果的話,會使用 requestAnimationFrame 的 api 一幀幀的不停渲染。
- function render() {
- renderer.render(scene, camera)
- requestAnimationFrame(render)
- }
- render();
這就是 Three.js 的大概流程:Scene 中有幾何體Geometry、點線面、輔助工具等各種物體,物體還可以做分組,然后通過正交或者透視相機來設置看到的二維畫面,之后用 Renderer 渲染出來。有動畫效果的話,要用 requestAnimationFrame 來一幀幀的渲染。
下面我們來實現一下花瓣雨的效果。
花瓣雨實現
首先我們要創建場景 Scene 中的物體,也就是各種花瓣,這個需要顯示的是一個平面,可以用 Sprite。
Sprite 是精靈的意思,在 Three.js 中,它就是一個永遠面向相機的二維平面。
我們給 Sprite 貼上花瓣的紋理就可以了。
我們先準備一些花瓣的貼圖,類似這種:
花瓣的數量有很多,我們生成 400 個,加到花瓣分組里,然后添加到場景中:
- const scene = new THREE.Scene();
- /**
- * 花瓣分組
- */
- const petal = new THREE.Group();
- function create() {
- var texture1 = new THREE.TextureLoader().load("img/h1.png");
- var texture2 = new THREE.TextureLoader().load("img/h2.png");
- var texture3 = new THREE.TextureLoader().load("img/h3.png");
- var texture4 = new THREE.TextureLoader().load("img/h4.png");
- var texture5 = new THREE.TextureLoader().load("img/h5.png");
- var imageList = [texture1, texture2, texture3, texture4, texture5];
- for (let i = 0; i < 400; i++) {
- var spriteMaterial = new THREE.SpriteMaterial({
- map: imageList[Math.floor(Math.random() * imageList.length)],//設置精靈紋理貼圖
- });
- var sprite = new THREE.Sprite(spriteMaterial);
- petal.add(sprite);
- sprite.scale.set(40, 50, 1);
- sprite.position.set(2000 * (Math.random() - 0.5), 2000 * Math.random(), 0)
- }
- scene.add(petal)
- }
- create();
400 個 Sprite 隨機貼上了不同的花瓣的紋理貼圖,然后設置了下放縮,之后隨機設置了一個在場景中的位置。
我們在 Scene 中加入坐標系輔助工具來看下坐標:
- const axisHelper = new THREE.AxisHelper(1000);
- scene.add(axisHelper)
紅色是 x 軸,向右是遞增的,綠色是 y 軸,向上是遞增的。z 軸我們暫時用不到。
所以,根據代碼,花瓣的 x 的范圍就是隨機的 -1000 到 1000,y 的范圍就是 0 到 2000。
然后,我們創建正交相機:
- 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, 200, 500)
- camera.lookAt(scene.position)
我們創建相機的時候指定了二維能顯示的范圍,相機在這個范圍內的哪個位置都行。
然后創建渲染器,設置下大小和背景顏色,把渲染到的 canvas 元素插入到 dom 中。
- const renderer = new THREE.WebGLRenderer();
- //設置渲染區域尺寸
- renderer.setSize(width, height)
- //設置背景顏色
- renderer.setClearColor(0xFFFFFF, 1)
- //body元素中插入canvas對象
- document.body.appendChild(renderer.domElement)
之后就用 requestAnimation 不斷地一幀幀渲染就行了。
- function render() {
- petal.children.forEach(sprite => {
- sprite.position.y -= 1;
- sprite.position.x += 0.5;
- if (sprite.position.y < -400) {
- sprite.position.y = 800;
- }
- if (sprite.position.x > 1000) {
- sprite.position.x = -1000
- }
- });
- renderer.render(scene, camera)
- requestAnimationFrame(render)
- }
每次重新渲染之前,我們修改下花瓣的位置,產生下落效果,如果超出了范圍,就移到上面去重新開始落,這樣就是不間斷的花瓣雨效果。
完整代碼如下:
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>花瓣雨</title>
- <style>
- body {
- margin: 0;
- overflow: hidden;
- }
- </style>
- <script src="js/three.min.js"></script>
- </head>
- <body>
- <script>
- const scene = new THREE.Scene();
- /**
- * 花瓣分組
- */
- const petal = new THREE.Group();
- 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 renderer = new THREE.WebGLRenderer();
- function create() {
- //設置相機位置
- camera.position.set(0, 200, 500)
- camera.lookAt(scene.position)
- //設置渲染區域尺寸
- renderer.setSize(width, height)
- //設置背景顏色
- renderer.setClearColor(0xFFFFFF, 1)
- //body元素中插入canvas對象
- document.body.appendChild(renderer.domElement)
- // const axisHelper = new THREE.AxisHelper(1000);
- // scene.add(axisHelper)
- var textureTree1 = new THREE.TextureLoader().load("img/h1.png");
- var textureTree2 = new THREE.TextureLoader().load("img/h2.png");
- var textureTree3 = new THREE.TextureLoader().load("img/h3.png");
- var textureTree4 = new THREE.TextureLoader().load("img/h4.png");
- var textureTree5 = new THREE.TextureLoader().load("img/h5.png");
- var imageList = [textureTree1, textureTree2, textureTree3, textureTree4, textureTree5];
- for (let i = 0; i < 400; i++) {
- var spriteMaterial = new THREE.SpriteMaterial({
- map: imageList[Math.floor(Math.random() * imageList.length)],//設置精靈紋理貼圖
- });
- var sprite = new THREE.Sprite(spriteMaterial);
- petal.add(sprite);
- sprite.scale.set(40, 50, 1);
- sprite.position.set(2000 * (Math.random() - 0.5), 2000 * Math.random(), 0)
- }
- scene.add(petal)
- }
- function render() {
- petal.children.forEach(sprite => {
- sprite.position.y -= 1;
- sprite.position.x += 0.5;
- if (sprite.position.y < -400) {
- sprite.position.y = 800;
- }
- if (sprite.position.x > 1000) {
- sprite.position.x = -1000
- }
- });
- renderer.render(scene, camera)
- requestAnimationFrame(render)
- }
- create()
- render()
- </script>
- </body>
- </html>
總結
Three.js 是為了簡化 3D 渲染的框架,它提供了場景 Scene 的 api,里面可以包含各種可渲染的物體:立方體、圓錐等各種幾何體 Geometry、點線面、坐標系等輔助工具。這些物體還可以通過 Group 分組來統一管理。
Sence 要渲染出來需要指定一個相機,分為從點去看的透視相機 PerspectiveCamera,從平面去投影的正交相機 OrthographicCamera。理解了它們的原理才能理解 Camera 的參數。
之后通過 Renderer 渲染出來,如果有動畫需要用 requestAnimationFrame 來一幀幀的渲染。
這是 Three.js 的大概渲染流程。
之后我們實現了一個花瓣雨的案例。用到了 Sprite 這種物體,它是一個永遠面向相機的平面,用來做這種效果很合適。
當然,Three.js 的東西還是比較多的,這篇文章只是入下門,后面我們會繼續深入,做更多的有意思的 3D 場景和效果。