20201303张奕博 2023.1.25

创建浮岛

如以下 两幅图所示,整个浮岛造型是一个四棱椎,整体分为四部分,顶部是由地面和河流构成的四方体、底部三块是倒置的三角。生成这些三维模型的其实也并没有多少技巧,就像搭积木一样使用 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() {}

}

最新文章

  1. Intellij IDEA的一些东西
  2. [原创]LoadRunner 12.02 录制脚本时提示无Internet访问,如何解决?
  3. 使用开源DocX 生成Word
  4. TexturePacker大图还原成小图工具带源码
  5. vim 上下左右变成ABCD 解决办法
  6. MVC中javascript直接调用Model
  7. ASP.NET从MVC5升级到MVC6
  8. mongodb取出最大值与最小值
  9. 一步一步从原理跟我学邮件收取及发送 12.telnet命令行收一封信pop3
  10. 从 MVC 到前后端分离
  11. python2 和 python3 区别
  12. 洛谷3732:[HAOI2017]供给侧改革——题解
  13. 通过xtrabackup工具对mysql数据库做全备
  14. 在python中引用jar包
  15. 【IDEA】本地新建Maven项目+配置Git和GitHub+代码上传和拉取到GitHub+其他IDEA和GitHub实战
  16. C语言结构体的学习,以及gdb的调式
  17. 关于执行memcached报错问题
  18. Ubuntu18.10安装及优化
  19. 服务器tomcat配置教程
  20. python3使用configparser解析配置文件

热门文章

  1. 从最简单的线性DP开始
  2. 琐碎的想法(五)for 的前世今生
  3. MAC上好用的解压工具
  4. 【力扣】nSum问题模板
  5. Java自动装箱与拆箱
  6. js函数中的this指向
  7. 编程哲学之 C# 篇:005——&quot;Hello,World!&quot;
  8. 【一句话】:first-child 伪类解释
  9. .net core 上传文件到本地服务器
  10. Android 初代 K-V 存储框架 SharedPreferences,旧时代的余晖?