
宣告:本文涉及圖文和模型素材僅用于個人學習、研究和欣賞,請勿二次修改、非法傳播、轉載、出版、商用、及進行其他獲利行為,
摘要
兔年到了,祝大家身體健,康萬事順利,本文內容作為兔年新春紀念頁面,將使用 Three.js 及 其他前端開發知識,創建一個以兔子為主題的 3D 簡單的趣味頁面 Rabbit craft go,本文內容包括使用純代碼創建三維浮島、小河、樹木、兔子、胡蘿卜以及兔子的運動互動、浮島的影片效果等,本文包含的知識點相對比較簡單,主要包括 使用 Three.js 網格立方體搭建三維卡通場景、鍵盤事件的監聽與三維場景影片的結合等,如果仔細閱讀并實踐過本專欄《Three.js 進階之旅》的話,非常容易掌握,

??兔子造型來源于 Three.js開源論壇,頁面整體造型靈感來源于《我的世界》,頁面名稱靈感來源于游戲《Lara Craft Go》,
效果
我們先來看看實作效果,頁面加載完成后是一個游戲操作提示界面,可以通過鍵盤 ? 空格鍵 及 W、 A、 S、 D 或方向鍵操作小兔子運動,點擊開始按鈕后,游戲提示界面消失,可以看到倒三角造型的天空浮島及浮島上方的樹木 ??、河流 ?、橋 ??、胡蘿卜 ??、兔子 ?? 等元素,接著攝像機鏡頭 ?? 自動拉近并聚焦到兔子上,

按照操作提示界面的按鍵,可以操作兔子進行前進、轉向、跳躍等運動,當兔子的運動位置觸碰到胡蘿卜時,胡蘿卜會消失同時兔子會進行跳躍運動,當兔子運動到小河或者超出浮島范圍時,兔子則會墜落到下方,

打開以下鏈接,在線預覽效果,大屏訪問效果更佳,
?????在線預覽地址:https://dragonir.github.io/rabbit-craft-go
本專欄系列代碼托管在 Github 倉庫【threejs-odessey】,后續所有目錄也都將在此倉庫中更新,
??代碼倉庫地址:[email protected]:dragonir/threejs-odessey.git
實作
文章篇幅有限,因此刪減了三維模型的位置資訊等細節調整代碼,只提供構建三維模型的整體思路邏輯,想了解該部分內容的詳細介紹可以閱讀本專欄前幾篇文章及閱讀本文配套原始碼,現在,我們來看看整個頁面的實作詳細步驟:
頁面結構
Rabbit Craft Go 頁面的整體結構如下,其中 canvas.webgl 是用于渲染場景的容器、剩余標簽都是一些裝飾元素或提示語,
<canvas ></canvas>
<div id="mask">
<div >
<div >
<div ><span >W/↑</span></div>
<div ><span >A/←</span><span >S/↓</span><span >D/→</span></div>
<div ><span >space</span></div>
</div>
<p ><b>W</b>: 行走 <b>S</b>: 停止 <b>A</b>: 向左轉 <b>D</b>: 向右轉 <b>空格鍵</b>: 跳躍</p>
<p ><button id="start_button">開始</button></p>
</div>
</div>
<a class='github' href='https://github.com/dragonir/threejs-odessey' target='_blank' rel='noreferrer'>
<span class='author'>three.js odessey</span>
</a>
<h1 >RABBIT CRAFT GO!</h1>
<div ><i></i></div>
場景初始化
場景初始化程序中,我們引入必需的開發資源,并初始化渲染場景、相機、控制器、光照、頁面縮放適配等,其中外部資源的引入,其中 OrbitControls 用于頁面鏡頭縮放及移動控制;TWEEN 和 Animations 用于生成鏡頭補間影片,也就是剛開始時浮島由遠及近的鏡頭切換影片中效果;Island、Carrot、Rabbit、Waterfall 等是用來構建三維世界的類,為了使場景更加卡通化,使用了 THREE.sRGBEncoding 渲染效果,場景中添加了兩種光源,其中 THREE.DirectionalLight 用來生成陰影效果,
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { TWEEN } from "three/examples/jsm/libs/tween.module.min.js";
import Animations from './environment/animation';
import Island from './environment/island';
import Carrot from './environment/carrot';
import Rabbit from './environment/rabbit';
import Waterfall from './environment/waterfall';
// 初始化渲染器
const canvas = document.querySelector('canvas.webgl');
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
alpha: true
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.shadowMap.enabled = true;
renderer.shadowMap.needsUpdate = true;
// 初始化場景
const scene = new THREE.Scene();
// 初始化相機
const camera = new THREE.PerspectiveCamera(60, sizes.width / sizes.height, 1, 5000)
camera.position.set(-2000, -250, 2000);
// 鏡頭控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.enableDamping = true;
controls.enablePan = false;
controls.dampingFactor = 0.15;
// 頁面縮放事件監聽
window.addEventListener('resize', () => {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
// 更新渲染
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
// 更新相機
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
});
// 光照
const ambientLight = new THREE.AmbientLight(0xffffff, 1);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
scene.add(directionalLight);

創建浮島
如以下 ?? 兩幅圖所示,整個浮島造型是一個四棱椎,整體分為四部分,頂部是由地面和河流構成的四方體、底部三塊是倒置的三角,生成這些三維模型的其實也并沒有多少技巧,就像搭積木一樣使用 Three.js 提供的立方體網格通過計算拼接到一起即可,類 Island 包含一個方法 generate 用于創建上述三維模型,并將所創建模型添加到三維分組 floorMesh 中用于外部呼叫,其中棱柱部分是通過 CylinderBufferGeometry 來實作的,
export default class Island {
constructor() {
this.floorMesh = new THREE.Group();
this.generate();
}
generate() {
// 左側地面
const leftFieldMat = new THREE.MeshToonMaterial({
color: 0x015521d,
side: THREE.DoubleSide,
});
const leftFieldGeom = new THREE.BoxBufferGeometry(800, 30, 1800);
this.leftFieldMesh = new THREE.Mesh(leftFieldGeom, leftFieldMat);
// 右側地面
this.rightFieldMesh = this.leftFieldMesh.clone();
const mapCapMat = new THREE.MeshMatcapMaterial({
matcap: new THREE.TextureLoader().load('./images/matcap.png'),
side: THREE.DoubleSide
})
// 頂部棱柱
const topFieldGeom = new THREE.CylinderBufferGeometry(1200, 900, 200, 4, 4);
this.topFieldMesh = new THREE.Mesh(topFieldGeom, mapCapMat);
// 中間棱柱
const middleFieldGeom = new THREE.CylinderBufferGeometry(850, 600, 200, 4, 4);
this.middleFieldMesh = new THREE.Mesh(middleFieldGeom, mapCapMat);
// 底部棱錐
const bottomFieldGeom = new THREE.ConeBufferGeometry(550, 400, 4);
this.bottomFieldMesh = new THREE.Mesh(bottomFieldGeom, mapCapMat);
// 河面
const strGroundMat = new THREE.MeshLambertMaterial({
color: 0x75bd2d,
side: THREE.DoubleSide,
});
const strCroundGeom = new THREE.BoxBufferGeometry(205, 10, 1800);
this.strGroundMesh = new THREE.Mesh(strCroundGeom, strGroundMat);
// 小河
const streamMat = new THREE.MeshLambertMaterial({
color: 0x0941ba,
side: THREE.DoubleSide,
});
const streamGeom = new THREE.BoxBufferGeometry(200, 16, 1800);
this.streamMesh = new THREE.Mesh(streamGeom, streamMat);
// ...
}
};
浮島俯視圖是一個正方形,

浮島側視圖是一個倒三角形,

創建水流
接下來,我們為河流添加一個小瀑布,使場景動起來,流動的瀑布三維水滴 ?? 滴落效果的是通過創建多個限定范圍內隨機位置的 THREE.BoxBufferGeometry 來實作水滴模型,然后通過水滴的顯示隱藏影片實作視覺上的水滴墜落效果,Waterfall 類用于創建單個水滴,它為水滴初始化隨機位置和速度,并提供一個 update 方法用來更新它們,
export default class Waterfall {
constructor (scene) {
this.scene = scene;
this.drop = null;
this.generate();
}
generate () {
this.geometry = new THREE.BoxBufferGeometry(15, 50, 5);
this.material = new THREE.MeshLambertMaterial({ color: 0x0941ba });
this.drop = new THREE.Mesh(this.geometry, this.material);
this.drop.position.set((Math.random() - 0.5) * 200, -50, 900 + Math.random(1, 50) * 10);
this.scene.add(this.drop);
this.speed = 0;
this.lifespan = Math.random() * 50 + 50;
this.update = function() {
this.speed += 0.07;
this.lifespan--;
this.drop.position.x += (5 - this.drop.position.x) / 70;
this.drop.position.y -= this.speed;
};
}
};
完成水滴創建后,不要忘了需要在頁面重繪影片 tick 方法中像這樣更新已創建的水滴陣列 drops,使其看起來生成向下流動墜落的效果,
for (var i = 0; i < drops.length; i++) {
drops[i].update();
if (drops[i].lifespan < 0) {
scene.remove(scene.getObjectById(drops[i].drop.id));
drops.splice(i, 1);
}
}

創建橋
在河流上方添加一個小木橋 ??,這樣小兔子就可以通過木橋在小河兩邊移動了, 類 Bridge 通過 generate 方法創建一個小木橋,并通過三維模型組 bridgeMesh 將其匯出,我們可以在上面創建的 Island 類中使用它,將其添加到三維場景中,
export default class Bridge {
constructor() {
this.bridgeMesh = new THREE.Group();
this.generate();
}
generate() {
var woodMat = new THREE.MeshLambertMaterial({
color: 0x543b14,
side: THREE.DoubleSide
});
// 木頭
for (var i = 0; i < 15; i++) {
var blockGeom = new THREE.BoxBufferGeometry(10, 3, 70);
var block = new THREE.Mesh(blockGeom, woodMat);
this.bridgeMesh.add(block);
}
// 橋尾
var geometry_rail_v = new THREE.BoxBufferGeometry(3, 20, 3);
var rail_1 = new THREE.Mesh(geometry_rail_v, woodMat);
var rail_2 = new THREE.Mesh(geometry_rail_v, woodMat);
var rail_3 = new THREE.Mesh(geometry_rail_v, woodMat);
var rail_4 = new THREE.Mesh(geometry_rail_v, woodMat);
// ...
}
}

創建樹
從預覽動圖和頁面可以看到,浮島上共有兩種樹 ??,綠色的高樹和粉紅色的矮樹,樹的實作也非常簡單,是使用了兩個 BoxBufferGeometry 拼接到一起,類 Tree 和 LeafTree 分別用于生成這兩種樹木,接收引數 (x, y, z) 分別表示樹木在場景中的位置資訊,我們可以在 Island 輔導上添加一些樹木,構成浮島上的一片小森林,
export default class Tree {
constructor(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
this.treeMesh = new THREE.Group();
this.generate();
}
generate() {
// 樹干
var trunkMat = new THREE.MeshLambertMaterial({
color: 0x543b14,
side: THREE.DoubleSide
});
var trunkGeom = new THREE.BoxBufferGeometry(20, 200, 20);
this.trunkMesh = new THREE.Mesh(trunkGeom, trunkMat);
// 樹葉
var leavesMat = new THREE.MeshLambertMaterial({
color: 0x016316,
side: THREE.DoubleSide
});
var leavesGeom = new THREE.BoxBufferGeometry(80, 400, 80);
this.leavesMesh = new THREE.Mesh(leavesGeom, leavesMat);
this.treeMesh.add(this.trunkMesh);
this.treeMesh.add(this.leavesMesh);
this.treeMesh.position.set(this.x, this.y, this.z);
// ...
}
}
矮樹
export default class LeafTree {
constructor(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
this.treeMesh = new THREE.Group();
this.generate();
}
generate() {
// ...
}
}

創建胡蘿卜
接著,在地面上添加一些胡蘿卜 ??,胡蘿卜身體部分是通過四棱柱 CylinderBufferGeometry 實作的,然后通過 BoxBufferGeometry 立方體來實作胡蘿卜的兩片葉子,場景中可以通過 Carrot 類來添加胡蘿卜,本頁面示例中是通過回圈呼叫添加了 20 個隨機位置的胡蘿卜,
export default class Carrot {
constructor() {
this.carrotMesh = new THREE.Group();
this.generate();
}
generate() {
const carrotMat = new THREE.MeshLambertMaterial({
color: 0xd9721e
});
const leafMat = new THREE.MeshLambertMaterial({
color: 0x339e33
});
// 身體
const bodyGeom = new THREE.CylinderBufferGeometry(5, 3, 12, 4, 1);
this.body = new THREE.Mesh(bodyGeom, carrotMat);
// 葉子
const leafGeom = new THREE.BoxBufferGeometry(5, 10, 1, 1);
this.leaf1 = new THREE.Mesh(leafGeom, leafMat);
this.leaf2 = this.leaf1.clone();
// ...
this.carrotMesh.add(this.body);
this.carrotMesh.add(this.leaf1);
this.carrotMesh.add(this.leaf2);
}
};
for (let i = 0; i < 20; i++) {
carrot[i] = new Carrot();
scene.add(carrot[i].carrotMesh);
carrot[i].carrotMesh.position.set(-170 * Math.random() * 3 - 300, -12, 1400 * Math.random() * 1.2 - 900);
}

創建兔子
最后,來創建頁面的主角兔子 ??,兔子全部都是由立方體 BoxBufferGeometry 搭建而成的,整體可以分解為頭、眼睛、耳朵、鼻子、嘴、胡須、身體、尾巴、四肢等構成,構建兔子時的核心要素就是各個立方體位置和縮放比例的調整,需要具備一定的審美能力,當然本例中使用的兔子是在 Three.js 社區開源代碼的基礎上改造的 ??,
完成兔子的整體外形之后,我們通過 gsap 給兔子添加一些運動影片效果和方法以供外部呼叫,其中 blink() 方法用于眨眼、jump() 方法用于原地跳躍、nod() 方法用于點頭、run() 方法用于奔跑、fall() 方法用于邊界檢測時檢測到超出運動范圍時使兔子墜落效果等,完成 Rabbit 類后,我們就可以在場景中初始化小兔子,
import { TweenMax, Power0, Power1, Power4, Elastic, Back } from 'gsap';
export default class Rabbit {
constructor() {
this.bodyInitPositions = [];
this.runningCycle = 0;
this.rabbitMesh = new THREE.Group();
this.bodyMesh = new THREE.Group();
this.headMesh = new THREE.Group();
this.generate();
}
generate() {
var bodyMat = new THREE.MeshLambertMaterial({
color: 0x5c6363
});
var tailMat = new THREE.MeshLambertMaterial({
color: 0xc2bebe
});
var nouseMat = new THREE.MeshLambertMaterial({
color: 0xed716d
});
// ...
var pawMat = new THREE.MeshLambertMaterial({
color: 0xbf6970
});
var bodyGeom = new THREE.BoxBufferGeometry(50, 50, 42, 1);
var headGeom = new THREE.BoxBufferGeometry(44, 44, 54, 1);
var earGeom = new THREE.BoxBufferGeometry(5, 60, 10, 1);
var eyeGeom = new THREE.BoxBufferGeometry(20, 20, 8, 1);
var irisGeom = new THREE.BoxBufferGeometry(8, 8, 8, 1);
var mouthGeom = new THREE.BoxBufferGeometry(8, 16, 4, 1);
var mustacheGeom = new THREE.BoxBufferGeometry(0.5, 1, 22, 1);
var spotGeom = new THREE.BoxBufferGeometry(1, 1, 1, 1);
var legGeom = new THREE.BoxBufferGeometry(33, 33, 10, 1);
var pawGeom = new THREE.BoxBufferGeometry(45, 10, 10, 1);
var pawFGeom = new THREE.BoxBufferGeometry(20, 20, 20, 1);
var tailGeom = new THREE.BoxBufferGeometry(20, 20, 20, 1);
var nouseGeom = new THREE.BoxBufferGeometry(20, 20, 15, 1);
var tailGeom = new THREE.BoxBufferGeometry(23, 23, 23, 1);
this.body = new THREE.Mesh(bodyGeom, bodyMat);
this.bodyMesh.add(this.body);
this.head = new THREE.Mesh(headGeom, bodyMat);
this.bodyMesh.add(this.legL);
this.headMesh.add(this.earR);
this.rabbitMesh.add(this.bodyMesh);
this.rabbitMesh.add(this.headMesh);
// ...
}
blink() {
var sp = 0.5 + Math.random();
if (Math.random() > 0.2)
TweenMax.to([this.eyeR.scale, this.eyeL.scale], sp / 8, {
y: 0,
ease: Power1.easeInOut,
yoyo: true,
repeat: 3
});
}
// 跳躍
jump() {
var speed = 10;
var totalSpeed = 10 / speed;
var jumpHeight = 150;
TweenMax.to(this.earL.rotation, totalSpeed / 2, {
z: "+=.3",
ease: Back.easeOut,
yoyo: true,
repeat: 1
});
TweenMax.to(this.earR.rotation, totalSpeed / 2, {
z: "-=.3",
ease: Back.easeOut,
yoyo: true,
repeat: 1
});
// ...
}
// 點頭
nod() {}
// 奔跑
run() {}
// 移動
move() {}
// 墜落
fall() {}
// 動作銷毀
killNod() {}
killJump() {}
killMove() {}
}

將兔子添加到場景中,

添加影片和操作
為了使兔子可以運動和可互動,我們通過監聽鍵盤按鍵的方式來呼叫兔子類內置的對應影片方法,兔子的方向轉動可以通過修改兔子的旋轉屬性 rotation 來實作,
// 兔子控制
const rabbitControl = {
tureLeft: () => {
rabbit && (rabbit.rabbitMesh.rotation.y -= Math.PI / 2);
},
turnRight: () => {
rabbit && (rabbit.rabbitMesh.rotation.y += Math.PI / 2);
},
stopMove: () => {
rabbitMoving = false;
rabbit.killMove();
rabbit.nod();
},
}
// 鍵盤監聽
document.addEventListener('keydown', e => {
if (e && e.keyCode) {
switch(e.keyCode) {
// 左
case 65:
case 37:
rabbitControl.tureLeft();
break;
// 右
case 68:
case 39:
rabbitControl.turnRight();
break;
// 前
case 87:
case 38:
rabbitMoving = true;
break;
// 空格鍵
case 32:
!rabbitJumping && rabbit.jump() && (rabbitJumping = true);
break;
default:
break;
}
}
});
document.addEventListener('keyup', e => {
if (e && e.keyCode) {
switch(e.keyCode) {
case 83:
case 40:
case 87:
case 38:
rabbitMoving = false;
rabbit.killMove();
rabbit.nod();
break;
case 32:
setTimeout(() => {
rabbitJumping = false;
}, 800);
break;
}
}
});
為了使場景更加真實和趣味,我們可以添加一些邊界檢測方法,當兔子位置處于非可運動區域如小河、浮島之外等區域時,可以呼叫兔子的 fall(),方法使其墜落,當檢測到兔子的位置和胡蘿卜的位置重疊時,給兔子添加了一個 jump() 跳躍動作并使檢測到的這個胡蘿卜從場景中移除,
const checkCollision = () => {
for (let i = 0; i < 20; i++) {
let rabbCarr = rabbit.rabbitMesh.position.clone().sub(carrot[i].carrotMesh.position.clone());
if (rabbCarr.length() <= 20) {
rabbit.jump();
scene.remove(carrot[i].carrotMesh);
rabbCarr = null;
}
}
// 檢查是否是地面的邊界
var rabbFloor = island.floorMesh.position.clone().sub(rabbit.rabbitMesh.position.clone());
if (
rabbFloor.x <= -900 ||
rabbFloor.x >= 900 ||
rabbFloor.z <= -900 ||
rabbFloor.z >= 900
) {
rabbit.fall();
}
// 小河檢測
var rabbStream = rabbit.rabbitMesh.position.clone().sub(island.streamMesh.position.clone());
if (
(rabbStream.x >= -97 &&
rabbStream.x <= 97 &&
rabbStream.z >= -900 &&
rabbStream.z <= 688) ||
(rabbStream.x >= -97 && rabbStream.x <= 97 && rabbStream.z >= 712)
) {
rabbit.fall();
}
}

頁面裝飾
最后,我們來制作一個其實頁面,中間部分是鍵盤操作說明,底部是一些裝飾文案圖片,操作提示下方是一個開始按鈕,我們給這個按鈕添加一個通過 TWEEN.js 實作的鏡頭補間影片效果,當點擊按鈕時,頁面首先顯示的是倒置三角造型的浮島,然后鏡頭慢慢方法拉近,顯示出兔子運動的區域,本頁面為了使其看起來更加符合游戲主題,標題文案使用了一種像素化的字體 ?,
const startButton = document.getElementById('start_button');
const mask = document.getElementById('mask');
startButton.addEventListener('click', () => {
mask.style.display = 'none';
Animations.animateCamera(camera, controls, { x: 50, y: 120, z: 1000 }, { x: 0, y: 0, z: 0 }, 3600, () => {});
});

??原始碼地址:https://github.com/dragonir/threejs-odessey
總結
本文中主要包含的知識點包括:
- 使用
Three.js網格立方體搭建三維卡通場景 - 鍵盤事件的監聽與三維場景影片的結合
想了解其他前端知識或其他未在本文中詳細描述的Web 3D開發技術相關知識,可閱讀我往期的文章,如果有疑問可以在評論中留言,如果覺得文章對你有幫助,不要忘了一鍵三連哦 ??,
附錄
- [1]. ?? Three.js 打造繽紛夏日3D夢中情島
- [2]. ?? Three.js 實作炫酷的賽博朋克風格3D數字地球大屏
- [3]. ?? Three.js 實作2022冬奧主題3D趣味頁面,含冰墩墩
- [4]. ?? Three.js 實作3D開放世界小游戲:阿貍的多元宇宙
- [5]. ?? 1000粉!使用Three.js實作一個創意紀念頁面
...- 【Three.js 進階之旅】系列專欄訪問 ??
- 更多往期【3D】專欄訪問 ??
- 更多往期【前端】專欄訪問 ??
參考
- [1]. three.js journey
- [2]. threejs.org
本文作者:dragonir 本文地址:https://www.cnblogs.com/dragonir/p/17064580.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/542354.html
標籤:其他
下一篇:JavaScript 實作繼承
