关于three.js:Threejs-进阶之旅新春特典Rabbit-craft-go-🐇

申明:本文波及图文和模型素材仅用于集体学习、钻研和观赏,请勿二次批改、非法流传、转载、出版、商用、及进行其余获利行为。

摘要

兔年到了,祝大家身材健,康万事顺利。本文内容作为兔年新春留念页面,将应用 Three.js 及 其余前端开发常识,创立一个以兔子为主题的 3D 简略的趣味页面 Rabbit craft go。本文内容包含应用纯代码创立三维浮岛、小河、树木、兔子、胡萝卜以及兔子的静止交互、浮岛的动画成果等。本文蕴含的知识点绝对比较简单,次要包含 应用 Three.js 网格立方体搭建三维卡通场景、键盘事件的监听与三维场景动画的联合等,如果仔细阅读并实际过本专栏《Three.js 进阶之旅》的话,非常容易把握。

🚩 兔子造型来源于 Three.js开源论坛,页面整体造型灵感来源于《我的世界》,页面名称灵感来源于游戏《Lara Craft Go》。

成果

咱们先来看看实现成果,页面加载实现后是一个游戏操作提醒界面,能够通过键盘 空格键WASD 或方向键操作小兔子静止。点击开始按钮后,游戏提醒界面隐没,能够看到倒三角造型的天空浮岛及浮岛上方的树木 🌳、河流 、桥 🌉、胡萝卜 🥕、兔子 🐇 等元素,接着摄像机镜头 📹 主动拉近并聚焦到兔子上。

依照操作提醒界面的按键,能够操作兔子进行后退、转向、跳跃等静止,当兔子的静止地位触碰到胡萝卜时,胡萝卜会隐没同时兔子会进行跳跃静止。当兔子静止到小河或者超出浮岛范畴时,兔子则会坠落到下方。

关上以下链接,在线预览成果,大屏拜访成果更佳。

  • 👁‍🗨 在线预览地址:https://dragonir.github.io/rabbit-craft-go

本专栏系列代码托管在 Github 仓库【threejs-odessey】,后续所有目录也都将在此仓库中更新

🔗 代码仓库地址:git@github.com:dragonir/threejs-odessey.git

实现

文章篇幅无限,因而删减了三维模型的地位信息等细节调整代码,只提供构建三维模型的整体思路逻辑,想理解该局部内容的具体介绍能够浏览本专栏前几篇文章及浏览本文配套源码。当初,咱们来看看整个页面的实现具体步骤:

页面构造

Rabbit Craft Go 页面的整体构造如下,其中 canvas.webgl 是用于渲染场景的容器、残余标签都是一些装璜元素或提醒语。

<canvas class="webgl"></canvas>
<div class="mask" id="mask">
  <div class="box">
    <div class="keyboard">
      <div class="row"><span class="key">W/↑</span></div>
      <div class="row"><span class="key">A/←</span><span class="key">S/↓</span><span class="key">D/→</span></div>
      <div class="row"><span class="key space">space</span></div>
    </div>
    <p class="tips"><b>W</b>: 行走&emsp;<b>S</b>: 进行&emsp;<b>A</b>: 向左转&emsp;<b>D</b>: 向右转&emsp;<b>空格键</b>: 跳跃</p>
    <p class="start"><button class="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 class="title">RABBIT CRAFT GO!</h1>
<div class="banner"><i></i></div>

场景初始化

场景初始化过程中,咱们引入必须的开发资源,并初始化渲染场景、相机、控制器、光照、页面缩放适配等。其中内部资源的引入,其中 OrbitControls 用于页面镜头缩放及挪动管制;TWEENAnimations 用于生成镜头补间动画,也就是刚开始时浮岛由远及近的镜头切换动画中成果;IslandCarrotRabbitWaterfall 等是用来构建三维世界的类。为了使场景更加卡通化,应用了 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 拼接到一起。类 TreeLeafTree 别离用于生成这两种树木,接管参数 (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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理