3D Model Loading

Three.js 之博客装逼指北

2021年08月15日 技术分享>前端技术 ,
本站文章除注明转载外,均为原创文章。如需转载请注明出处:
https://kwokronny.top/202108/threejs-blog-fake-x-manual/

先上效果

先上最终效果,我的博客虽然干货很少,不过 逼格 还是令自己满意的,日后也会尝试多多分享,毕竟分享真的很检验对知识的牢固,未发现的知识盲区,也更考验写作能力。

博客链接: https://kwokronny.top

CSS3 是不够实现我的骚气哒~

在建博客前需要看了很多大佬的博客,大佬们的虽然简洁的界面,但内容的深度,文章的逼格,令人神往,无需博客外包装的浮华,就让大家争相阅读。
很久没自己设计网站,审美下降了不少,在几天不断的浏览中,定下了要比大家装的好,就得用大家不太常用的技术栈,three.js是我最终选择的装逼方案。

开始装逼

寻找模型

希望通过 3D 的效果达到 装逼 的目的,需要先建模,这玩意学起来一时半会是不可能的,所以就开始寻找免费非商用的 3D 模型,在免费的素材中寻找了许多的素材,最终选择了热爱的 MineCraft 的人物模型。

SketchFab(https://sketchfab.com/)

下载模型选择 GLTF,我们找的示例让我们引用的即是 GLTFLoader。当然有时也会下载到一些导出有问题的,亦可以下载原始结构,再通过 3D 编辑器 重新导出,此处推荐 Blender3D,我选择的模型上传者也刚好同名,也不知道是不是该软件商提供的。

尝试编写

之后就先开始了 three.js 的尝试性编写,首先就是要先看 three.js 的文档了,进入官网,里面有许多的优秀案例及示例。

three.js 文档
建议先在网上先迅速阅读了 3d 的相关基础知识,及大佬们写的手把手教你玩 three.js 之类的文章

我的学习方法很多时候是先抄作业的基础上尝试查阅属性、改变属性,我们可以先从示例找到可以抄的示例先,我选的模型刚好有相关的三个骨架动画,所以阅读此示例的源码,可以优先将下载的模型引入并动起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
html,
body {
height: 100%;
}
.bg-canvas {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div class="bg-canvas"></div>
<script src="https://unpkg.com/three@0.131.3/build/three.min.js"></script>
<script src="https://unpkg.com/three@0.131.3/examples/js/loaders/GLTFLoader.js"></script>
<script>
(function () {
var scene, renderer, camera, container;
var mixer, clock, actions;

function init() {
// 获取绘制容器及容器尺寸
container = document.querySelector(".bg-canvas");
var width = container.getBoundingClientRect().width;
var height = container.getBoundingClientRect().height;

// 实例化透视摄像机,我们看见的即是此摄像机的视角。
camera = new THREE.PerspectiveCamera(45, width / height, 1, 2000);
camera.position.set(0, 2, 16);

// 让模型帧动画按此实例化的时钟更新
clock = new THREE.Clock();

// 实例化场景
scene = new THREE.Scene();

// 实例化半球光,先把场景按灯光颜色照亮
var hemiLight = new THREE.HemisphereLight(0xf2f2f2, 0xf2f2f2);
hemiLight.position.set(0, 50, 0);
scene.add(hemiLight);

// 实例化太阳光,照亮的同时,为模型添加上阴影
var dirLight = new THREE.DirectionalLight(0xffffff);
dirLight.position.set(0, 30, 0);
dirLight.castShadow = true;
dirLight.shadow.camera.near = 0.1;
dirLight.shadow.camera.far = 60;
scene.add(dirLight);

var loader = new THREE.GLTFLoader();

// 加载模型
loader.load("./zombie.gltf", function (gltf) {
var model = gltf.scene;
model.traverse(function (object) {
if (object.isMesh) {
object.frustumCulled = false;
// 投射阴影
object.castShadow = true;
}
});
model.position.set(0.6, 0, 2.5);
model.scale.set(1.5, 1.5, 1.5);
scene.add(model);

// 实例化动画混合器
mixer = new THREE.AnimationMixer(model);
actions = {
push_up: mixer.clipAction(gltf.animations[0]),
idle: mixer.clipAction(gltf.animations[1]),
walk: mixer.clipAction(gltf.animations[2]),
};
actions.idle.play();
// loader 需要时间加载,所以我们的渲染动画行为需要在loader的回调函数内
animate();
});

// 实例化 WebGL渲染器,相关配置可以对照着文档调整
renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.shadowMap.enabled = true;

renderer.setSize(width, height);
container.appendChild(renderer.domElement);
}

// 动画循环
function animate() {
requestAnimationFrame(animate);
var delta = clock.getDelta();
mixer.update(delta);
render();
}

function render() {
renderer.render(scene, camera);
}

init();
})();
</script>
</body>
</html>

上面示例完成后,我们通过抄作业,大致了解了如果加入模型,动画渲染,摄像头视角等,对照着相关的类,查阅一遍属性及函数,就可以大概掌握其用法啦。

皮肤纹理我忘记是在哪里拿到的了,可能是在别的下载网站找到模型中 纹理贴图的 UV 刚好一致的 图片,替换图片即可,需要的可自行下载

增加元素

那只有一个主角相对单调,我们可以根据我们抄作业学习到的,加一些其它模型,丰富场景。

最终决定加入这个模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
// loader 是已实例化的加载器,无须多次实例化加载器加载模型
loader.load("/gltf/pumpkin.gltf", function (gltf) {
var model = gltf.scene;
model.traverse(function (object) {
if (object.isMesh) {
object.frustumCulled = false;
object.castShadow = true;
}
});
model.rotation.set(0, 1, 0);
model.position.set(1, 0.1, -2.4);
let scale = 0.33;
model.scale.set(scale, scale, scale);
scene.add(model);
});
// ...

场景还没有给他加地面,Zombie 和 南瓜车 还似悬在空中,需要再为场景加一块地。随意搜索了张 MineCraft 的草地贴图,在 three.js 示例 中找到有可能关联的示例,刚好第一个就有草地,用小学的英语发现了该代码片段。

由于设想给这个 3D 效果加上些简单交互,为了不太违和,将这个效果似展厅的方式展现,可以通过按钮控制主体的旋转以及 Zombie 的动作切换,最终选择了 CylinderGeometry(圆柱几何体),完成主体的效果。

其中主体的效果我们还需要不断的调整他到一个合适的位置,且当操控旋转时,需要一起多个几何体一同旋转,所以我们需要给这 3 个主体 群组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 建立一个 3D对象,将主体效果的 3个主体 包括进来,统一操控
stage = new THREE.Object3D();
scene.add(stage);

// 新增的草地

var groundTexture = new THREE.TextureLoader().load("/gltf/textures/grass.png");
//设置纹理的水平与垂直应用重复渲染
groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping;
//设置纹理的重复渲染次数
groundTexture.repeat.set(4, 4);
// 增加材质的清晰度
groundTexture.anisotropy = 16;
groundTexture.encoding = THREE.sRGBEncoding;

var bottomBase = new THREE.Mesh(new THREE.CylinderBufferGeometry(5, 5, 0.1, 32), new THREE.MeshLambertMaterial({ map: groundTexture }));
// 材质是否接收阴影
bottomBase.receiveShadow = true;
stage.add(bottomBase);

// ...
// 以及将 loader 回调中加入场景 scene.add(model)改为
stage.add(model);

增加危险隔离带

确认主体效果即布局后,由于 主体效果 主要在右下角,右上角总感觉有些空,适当的想像场景,最后想到了增加隔离带,黄色与绿色的搭配也意外的合适,也就顺带定下了博客的主色等颜色的设计。

最初采用的是 css3 应用 animationbackground-position 调整达到隔离带的动效,但该方案基础扎实的小伙伴会发现,易引起重绘,CPU 转的嗡嗡的,表示很淦。

所以最近也就抽空将 隔离带 改用 Three.js 完成。
首先我们先在官方的示例中尝试抄抄作业,但并没有找到相似关联的示例,我们通过前面抄作业的学习,大概思考出效果的实现方式,常见的自然就是纹理偏移、纹理动画了。

搜索到的纹理操控文章: Threejs 纹理对象 Texture 阵列、偏移、旋转(纹理动画)

最终隔离带的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 因纹理变量需要给animate函数引用,所以需要在外面声明此纹理变量
var warningTexture;

//...
//function init(){
warningTexture = new THREE.TextureLoader().load("/img/warning_zone.png");
warningTexture.wrapS = THREE.RepeatWrapping;
warningTexture.repeat.set(4, 1);

var warningBar = new THREE.Mesh(
// PlaneGeometry平面缓冲几何体
new THREE.PlaneGeometry((0.8 * (424 * 4)) / 60, 0.8),
new THREE.MeshBasicMaterial({ map: warningTexture })
);
scene.add(warningBar);

//...
//function animate(){
//...
warningTexture.offset.x -= 0.007;
//}

未完待续

我的博客是基于 hexo 改自行尝试编写主题,还未整理成可开源的主题项目,后期有时间也争取分享出来供大家参考。

three.js 我还只是菜鸡,大佬看看热闹,不要笑我,有好的玩法,也望不吝赐教~哈哈哈哈。

附件:最终代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
html,
body {
height: 100%;
}
.bg-canvas {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div class="bg-canvas"></div>
<script src="https://unpkg.com/three@0.131.3/build/three.min.js"></script>
<script src="https://unpkg.com/three@0.131.3/examples/js/loaders/GLTFLoader.js"></script>
<script>
(function () {
var scene, renderer, camera, container;
var mixer, clock, actions, warningTexture;

function init() {
// 获取绘制容器及容器尺寸
container = document.querySelector(".bg-canvas");
var width = container.getBoundingClientRect().width;
var height = container.getBoundingClientRect().height;

// 实例化透视摄像机,我们看见的即是此摄像机的视角。
camera = new THREE.PerspectiveCamera(45, width / height, 1, 2000);
camera.position.set(0, 2, 16);

clock = new THREE.Clock();

// 实例化场景
scene = new THREE.Scene();

// 实例化半球光,先把场景按灯光颜色照亮
var hemiLight = new THREE.HemisphereLight(0xf2f2f2, 0xf2f2f2);
hemiLight.position.set(0, 50, 0);
scene.add(hemiLight);

var dirLight = new THREE.DirectionalLight(0xffffff);
dirLight.position.set(0, 30, 0);
dirLight.castShadow = true;
dirLight.shadow.camera.near = 0.1;
dirLight.shadow.camera.far = 60;
scene.add(dirLight);

warningTexture = new THREE.TextureLoader().load("./warning_zone.png");
warningTexture.wrapS = THREE.RepeatWrapping;
warningTexture.repeat.set(4, 1);

var warningBar = new THREE.Mesh(
// PlaneGeometry平面缓冲几何体
new THREE.PlaneGeometry((0.8 * (424 * 4)) / 60, 0.8),
new THREE.MeshBasicMaterial({ map: warningTexture })
);
warningBar.position.set(0, 1.5, 5.4);
warningBar.rotation.set(0, -0.3, -0.1);
scene.add(warningBar);

stage = new THREE.Object3D();
scene.add(stage);

var groundTexture = new THREE.TextureLoader().load("./textures/grass.png");
groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping;
groundTexture.repeat.set(4, 4);
groundTexture.anisotropy = 16;
groundTexture.encoding = THREE.sRGBEncoding;

var bottomBase = new THREE.Mesh(new THREE.CylinderBufferGeometry(5, 5, 0.1, 32), new THREE.MeshLambertMaterial({ map: groundTexture }));
bottomBase.receiveShadow = true;
stage.add(bottomBase);

var loader = new THREE.GLTFLoader();

loader.load("./pumpkin.gltf", function (gltf) {
var model = gltf.scene;
model.traverse(function (object) {
if (object.isMesh) {
object.frustumCulled = false;
object.castShadow = true;
}
});
model.rotation.set(0, 1, 0);
model.position.set(1, 0.1, -2.4);
let scale = 0.33;
model.scale.set(scale, scale, scale);
stage.add(model);
});
// 加载模型
loader.load("./zombie.gltf", function (gltf) {
var model = gltf.scene;
model.traverse(function (object) {
if (object.isMesh) {
object.frustumCulled = false;
// 投射阴影
object.castShadow = true;
}
});
model.position.set(0.6, 0, 2.5);
model.scale.set(1.5, 1.5, 1.5);
stage.add(model);

// 实例化动画混合器
mixer = new THREE.AnimationMixer(model);
actions = {
push_up: mixer.clipAction(gltf.animations[0]),
idle: mixer.clipAction(gltf.animations[1]),
walk: mixer.clipAction(gltf.animations[2]),
};
actions.walk.play();
animate();
});

// 实例化 WebGL渲染器,相关配置可以对照着文档调整
renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.shadowMap.enabled = true;

renderer.setSize(width, height);
container.appendChild(renderer.domElement);
}

function animate() {
requestAnimationFrame(animate);
var delta = clock.getDelta();
warningTexture.offset.x -= 0.007;
mixer.update(delta);
render();
}

function render() {
renderer.render(scene, camera);
}

init();
})();
</script>
</body>
</html>