three.js 專案搭建(一)——實現一個3D大西瓜
用到的庫:
建立專案
此處通過React腳手架快速建立一個專案(本篇文章並非主要介紹React,故此處快速開始,不做過多描述,不瞭解的小夥伴可以檢視React官方文件)
npx create-react-app tree-test
開始搭建
建立場景
相機、幾何體等都需要新增至場景中
```js import * as THREE from 'three';
// 建立場景 const scene = new THREE.Scene(); ```
建立相機
js
// PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )
// fov — 攝像機視錐體垂直視野角度
// aspect — 攝像機視錐體長寬比
// near — 攝像機視錐體近端面
// far — 攝像機視錐體遠端面
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// 設定相機位置
camera.position.set(0, 0, 10);
// 新增至場景中
scene.add(camera);
新增座標軸輔助器
js
// 新增座標軸輔助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
新增物體
使用three.js提供的幾何體
js
// 新增物體
// BoxGeometry(width : Float, height : Float, depth : Float, widthSegments : Integer, heightSegments : Integer, depthSegments : Integer)
// width — X軸上面的寬度,預設值為1。
// height — Y軸上面的高度,預設值為1。
// depth — Z軸上面的深度,預設值為1。
// widthSegments — (可選)寬度的分段數,預設值是1。
// heightSegments — (可選)高度的分段數,預設值是1。
// depthSegments — (可選)深度的分段數,預設值是1。
const geometry = new THREE.BoxGeometry(1, 1, 1);
// 基礎網格材質
const material = new THREE.MeshBasicMaterial({ color: 0x43ad7f1f });
// 根據幾何體和材質建立物體
const cube = new THREE.Mesh(geometry, material);
// 新增至場景中
scene.add(cube);
在圖形學中,任何物體都可以由若干個三角形拼接形成,所以我們也可以通過自己繪製三角形拼接形成圖形
js
const geometry = new THREE.BufferGeometry();
// 拼成一個矩形至少需要兩個三角形,6個點,每個點三個座標,共18個座標
const vertices = new Float32Array([
-1.1, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0,
1.0, -1.1, -1.0, 1.0,
]);
// BufferAttribute
// .array : TypedArray
// 在 array 中儲存著快取中的資料。
// .count : Integer
// 儲存 array 除以 itemSize 之後的大小。
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
const material = new THREE.MeshBasicMaterial({ color: 0x43ad7f1f });
material.wireframe = true;
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
效果如圖:
若material.wireframe = false;
後,圖形的前後是有差別的:
從z軸正方向檢視:
從z軸負方向檢視:
更進一步,我們生成隨機半透明的幾何圖形
js
// 新增物體
for (let i = 0; i < 50; i++) {
// 每個三角形需要三個頂點,每個頂點需要三個值
const geometry = new THREE.BufferGeometry();
// 注意:此處new Float32Array時一定要傳入引數,否則圖形不會顯示
const positionArray = new Float32Array(9);
for (let j = 0; j < 9; j++) {
positionArray[j] = Math.random() * 10 - 5;
}
geometry.setAttribute(
'position',
new THREE.BufferAttribute(positionArray, 3)
);
let color = new THREE.Color(Math.random(), Math.random(), Math.random());
// 注意:此處一定要設定transparent=true後再設定opacity
const material = new THREE.MeshBasicMaterial({
color,
transparent: true,
opacity: 0.5,
});
const mesh = new THREE.Mesh(geometry, material);
// material.wireframe = true;
scene.add(mesh);
}
效果:
初始化渲染器及軌道
```js // 初始化渲染器 const renderer = new THREE.WebGLRenderer(); // 設定渲染的尺寸大小 renderer.setSize(window.innerWidth, window.innerHeight); // 建立軌道控制器 // OrbitControls( object : Camera, domElement : HTMLDOMElement ) // object: (必須)將要被控制的相機。該相機不允許是其他任何物件的子級,除非該物件是場景自身。
// domElement: 用於事件監聽的HTML元素。 const controls = new OrbitControls(camera, renderer.domElement); // 為控制器設定阻尼,讓控制器有真實的效果 controls.enableDamping = true; ```
渲染
本demo採用React Hook,📢注意renderer.domElement
並非React認識的元素,所以需要採用JS原生方式新增子元素(或許是我不知道,很高興有大佬評論告知其他新增到頁面上的寫法)
js
useEffect(() => {
const app = document.getElementById('App');
app?.appendChild(renderer.domElement);
// 使用渲染器,通過相機將場景渲染進來
render();
});
const render = () => {
controls.update();
renderer.render(scene, camera);
// 此處採用動畫是為了響應軌道滑行變換
requestAnimationFrame(render);
};
動畫
此處簡單做了個動畫示例,更復雜的動畫可以檢視gsap
js
// 設定動畫
gsap.to(cube.position, { x: 5, duration: 5 });
GUI工具的使用
```js import * as dat from 'dat.gui';
// 例項化視覺化GUI工具 可以通過按 H 鍵隱藏GUI面板 const gui = new dat.GUI(); //可傳遞引數{ closed:true ,width:400 } // gui.hide() //隱藏GUI面板,可通過按兩次 H鍵開啟顯示
// 往GUI面板新增要顯示的物件的引數 // 引數一:物件;引數二:要調整的物件屬性;引數三:最小值;引數四:最大值;引數五:調整精度 // 新增配置 cube y 軸座標 gui.add(cube.position, 'y', -3, 3, 0.01); // 新增配置 cube 是否可見 gui.add(cube, 'visible');
const params = { color: '#ffff00', fn: () => { gsap.to(cube.position, { z: 5, duration: 2, yoyo: true, repeat: -1 }); }, }; // 新增一個色盤,顏色改變時改變cube的顏色 gui.addColor(params, 'color').onChange(value => { cube.material.color.set(value); }); // 新增一個名稱為”立方體運動“的設定項,控制是否執行params.fn函式 gui.add(params, 'fn').name('立方體運動'); // 建立一個資料夾,用於存放這些設定 const folder = gui.addFolder('設定立方體'); // 設定 cube 是否顯示線框 folder.add(cube.material, 'wireframe'); ``` 上述配置之後效果如下圖👇🏻
完整程式碼
```js //app.js
import React, { useEffect } from 'react'; import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import gsap from 'gsap'; import * as dat from 'dat.gui'; // 例項化視覺化GUI工具 可以通過按 H 鍵隱藏GUI面板 const gui = new dat.GUI(); //可傳遞引數{ closed:true ,width:400 } // gui.hide() //隱藏GUI面板,可通過按兩次 H鍵開啟顯示
function App() { // 建立場景 const scene = new THREE.Scene();
// PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number ) // fov — 攝像機視錐體垂直視野角度 // aspect — 攝像機視錐體長寬比 // near — 攝像機視錐體近端面 // far — 攝像機視錐體遠端面 const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); // 設定相機位置 camera.position.set(0, 0, 10); scene.add(camera);
// 新增物體 // BoxGeometry(width : Float, height : Float, depth : Float, widthSegments : Integer, heightSegments : Integer, depthSegments : Integer) // width — X軸上面的寬度,預設值為1。 // height — Y軸上面的高度,預設值為1。 // depth — Z軸上面的深度,預設值為1。 // widthSegments — (可選)寬度的分段數,預設值是1。 // heightSegments — (可選)高度的分段數,預設值是1。 // depthSegments — (可選)深度的分段數,預設值是1。 const geometry = new THREE.BoxGeometry(1, 1, 1); // 基礎網格材質 const material = new THREE.MeshBasicMaterial({ color: 0x43ad7f1f }); // 根據幾何體和材質建立物體 const cube = new THREE.Mesh(geometry, material); scene.add(cube);
//#region GUI 面板 // 往GUI面板新增要顯示的物件的引數 // 引數一:物件;引數二:要調整的物件屬性;引數三:最小值;引數四:最大值;引數五:調整精度 gui.add(cube.position, 'y', -3, 3, 0.01); gui.add(cube, 'visible'); const params = { color: '#ffff00', fn: () => { gsap.to(cube.position, { z: 5, duration: 2, yoyo: true, repeat: -1 }); }, }; gui.addColor(params, 'color').onChange(value => { cube.material.color.set(value); }); gui.add(params, 'fn').name('立方體運動'); const folder = gui.addFolder('設定立方體'); folder.add(cube.material, 'wireframe'); //#endregion
// 初始化渲染器 const renderer = new THREE.WebGLRenderer(); // 設定渲染的尺寸大小 renderer.setSize(window.innerWidth, window.innerHeight); // 建立軌道控制器 // OrbitControls( object : Camera, domElement : HTMLDOMElement ) // object: (必須)將要被控制的相機。該相機不允許是其他任何物件的子級,除非該物件是場景自身。 // domElement: 用於事件監聽的HTML元素。 const controls = new OrbitControls(camera, renderer.domElement); // 為控制器設定阻尼,讓控制器有真實的效果 controls.enableDamping = true;
// 新增座標軸輔助器 const axesHelper = new THREE.AxesHelper(5); scene.add(axesHelper);
// 將webgl渲染的canvas內容新增到頁面上 useEffect(() => { const app = document.getElementById('App'); app?.appendChild(renderer.domElement); // 設定動畫 gsap.to(cube.position, { x: 5, duration: 5 }); // 使用渲染器,通過相機將場景渲染進來 render(); });
window.addEventListener('resize', () => { // 更新攝像頭 camera.aspect = window.innerWidth / window.innerHeight; // 更新攝像機的投影矩陣 camera.updateProjectionMatrix(); // 更新渲染器 renderer.setSize(window.innerWidth, window.innerHeight); // 設定渲染器的畫素比 renderer.setPixelRatio(window.devicePixelRatio); }); window.addEventListener('dblclick', () => { const fullScreenElement = document.fullscreenElement; if (fullScreenElement) { document.exitFullscreen(); } else { renderer.domElement.requestFullscreen(); } });
const render = () => { controls.update(); renderer.render(scene, camera); requestAnimationFrame(render); };
return
; }export default App; ```
效果
進一步加工
匯入紋理
為了讓看到文章的小夥伴也獲得貼圖,我就直接放掘金上傳圖片的url作為示例了 ```js // 匯入紋理 new THREE.TextureLoader().load( 'http://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/172a9f6fc326477f9a328e64127bc8ed~tplv-k3u1fbpfcp-watermark.image?', function (texture) { // in this example we create the material when the texture is loaded const material = new THREE.MeshStandardMaterial({ map: texture, // 粗糙程度 roughness: 0.5, }); // 新增物體 const cubeGeometry = new THREE.SphereGeometry(2, 32, 16); const cube = new THREE.Mesh(cubeGeometry, material); scene.add(cube); },
// 目前暫不支援onProgress的回撥
undefined,
// onError回撥
function (err) {
console.error('An error happened.', err);
}
); ``` 另外,一些可以獲取貼圖的網站: - http://www.poliigon.com/textures - http://www.arroway-textures.ch/textures - http://quixel.com/bridge
燈光
js
// 燈光
// 環境光
const light = new THREE.AmbientLight(0x404040);
scene.add(light);
// 直線光
const lineLight = new THREE.DirectionalLight(0xffffff);
lineLight.position.set(10, 10, 10);
scene.add(lineLight);
效果
題外話
村上春樹說,世界上存在著不能流淚的悲傷,這種悲傷無法向人解釋,它永遠一成不變,如無風夜晚的雪花,靜靜地沉積在心裡,當你雖不認同卻只能無奈的屈服,當你不滿與現狀卻又看不清出路,當你充滿憂慮卻又無力改變,當焦慮變成了每一個異鄉人的家常便飯,我們又該如何去完成這一堂人生的必修課。