创建一个3D模型在自己博客里

最近在美化博客的时候,发现许多博客都有一个Live2d的二次元角色在博客页面,还会跟着鼠标动,有些还会互动。

像这样

这样的

虽然我知道Hexo有技术支持实现这样的功能,实现起来也很简单。

但是我更喜欢像在Minecraft主页实现的小人这样效果:

由于需要将3D模型展示在自己的博客里,于是便开始寻找解决方案,经过一番搜索,发现了一个比较方便的方案,那就是使用Three.js。里面有非常多、看起来非常NB的示例,都是用了Three.js

Three.js

Three.js是一个基于WebGL的JavaScript库,它可以帮助我们轻松地创建3D场景、模型、动画等。我们可以使用Three.js来创建一个3D模型,并将其展示在自己的博客里。

获得3D模型


首先第一步肯定是拿到模型,说到模型,我是基本没有建模过的,但是搞到一个Minecraft里面的人物模型还是非常简单的。

我也是查了好多资料,才发现居然有专门针对Minecraft建模的软件:Blockbench。下载链接:Downloads - Blockbench

打开软件,选择左侧的Minecraft Skin,然后选择Create New Model

最后选择一下你的Minecraft 人物的皮肤贴图,直接点击Confiem。

然后我们就看到我们的模型已经建好了,是不是非常快!

如果你是双层皮肤,那么你可能需要点击一下右下角的的小眼睛,他把你外面的皮肤隐藏掉了。

最后我们,调整一下模型的动作,改为立正姿势,方便我们以后给模型加动作,然后就可以直接保存模型了(记得保存为glTF格式)。

就这样模型问题就解决了,我们得到了一个glTF格式的模型文件。

在博客中展示3D模型


设置模型的放置位置

接下来,我们需要在博客中展示3D模型。我们可以使用Three.js来加载和渲染3D模型。

首先在我们博客里找个地方放我们的模型:

我使用的是hexo-theme-butterfly-dev这个主题,我们在主题文件夹下找到footer.pug文件,添加一下内容

1
2
3
4
5
6
7
8
9
// 在footer中插入Three.js渲染容器
#threejs-container(style="position: fixed; bottom: 10px; left: 10px; width: 250px; height: 600px; z-index: 1000;")
// CSS 在屏幕宽度小于810px是隐藏#threejs-container所展示的内容
style.
@media only screen and (max-width: 810px) {
#threejs-container {
display: none;
}
}
  1. #threejs-container 渲染容器的 div 元素
    • position: fixed;:将容器固定在页面的指定位置,不随页面滚动而移动。
    • bottom: 10px; left: 10px;:将容器固定在距离页面底部 10px、左侧 10px 的位置。
    • width: 250px; height: 600px;:设置容器的宽度为 250px,高度为 600px。
    • z-index: 1000;:设置容器的 z 轴层级,确保容器显示在页面的前面,不被其他元素遮挡。
  2. style. 块定义了一些 CSS 样式,用于处理不同屏幕宽度下的显示效果:
    • @media only screen and (max-width: 810px) { ... }:表示当屏幕的宽度小于等于 810px 时,隐藏 #threejs-container 元素,使其不显示在页面上。

这样我们的放置模型的位置就定下来了,在页面的左下角,位于顶层,不随页面滚动。

使用Threejs加载模型

我们编写一个js代码,名字就叫threejs-model.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
// 声明全局变量
let model, mixer, head, body, rightArm, leftArm, rightLeg, leftLeg;
const clock = new THREE.Clock();
let mouseX = 0, mouseY = 0;

// 初始化函数
function init() {
// 获取容器
var container = document.getElementById('threejs-container'); //获取加载模型的渲染窗口
if (!container) {
console.error("Element with ID 'threejs-container' not found.");
return;
}

// 设置场景、相机和渲染器
const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.z = 3;

// 创建渲染器并启用 alpha 透明度
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setClearColor(0xffffff,0); // 设置背景为透明
container.appendChild(renderer.domElement);

// 添加环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

// 添加半球光
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1);
hemisphereLight.position.set(0, 1, 0);
scene.add(hemisphereLight);

// 加载模型
const loader = new THREE.GLTFLoader();
loader.load('/models/model.gltf', function(gltf) { //加载模型
model = gltf.scene;
//根据具体需求调整模型位置
model.rotation.y = Math.PI; // 使模型旋转180度,面向相机
model.position.set(0, -1, 0); // 将模型向下移动1个单位,使头部显示出来
scene.add(model);

// 获取各部位节点
head = model.getObjectByName('Head');
body = model.getObjectByName('Body');
rightArm = model.getObjectByName('RightArm');
leftArm = model.getObjectByName('LeftArm');
rightLeg = model.getObjectByName('RightLeg');
leftLeg = model.getObjectByName('LeftLeg');

// 创建动画混合器
mixer = new THREE.AnimationMixer(model);

// 手臂和腿的摆动动画
const rightArmRotationKF = new THREE.QuaternionKeyframeTrack('RightArm.quaternion', [0, 0.5, 1], [
...new THREE.Quaternion().setFromEuler(new THREE.Euler(0.3, 0, 0)).toArray(),
...new THREE.Quaternion().setFromEuler(new THREE.Euler(-0.3, 0, 0)).toArray(),
...new THREE.Quaternion().setFromEuler(new THREE.Euler(0.3, 0, 0)).toArray()
]);

const leftArmRotationKF = new THREE.QuaternionKeyframeTrack('LeftArm.quaternion', [0, 0.5, 1], [
...new THREE.Quaternion().setFromEuler(new THREE.Euler(-0.3, 0, 0)).toArray(),
...new THREE.Quaternion().setFromEuler(new THREE.Euler(0.3, 0, 0)).toArray(),
...new THREE.Quaternion().setFromEuler(new THREE.Euler(-0.3, 0, 0)).toArray()
]);

const rightLegRotationKF = new THREE.QuaternionKeyframeTrack('RightLeg.quaternion', [0, 0.5, 1], [
...new THREE.Quaternion().setFromEuler(new THREE.Euler(-0.3, 0, 0)).toArray(),
...new THREE.Quaternion().setFromEuler(new THREE.Euler(0.3, 0, 0)).toArray(),
...new THREE.Quaternion().setFromEuler(new THREE.Euler(-0.3, 0, 0)).toArray()
]);

const leftLegRotationKF = new THREE.QuaternionKeyframeTrack('LeftLeg.quaternion', [0, 0.5, 1], [
...new THREE.Quaternion().setFromEuler(new THREE.Euler(0.3, 0, 0)).toArray(),
...new THREE.Quaternion().setFromEuler(new THREE.Euler(-0.3, 0, 0)).toArray(),
...new THREE.Quaternion().setFromEuler(new THREE.Euler(0.3, 0, 0)).toArray()
]);

// 创建动画剪辑
const clip = new THREE.AnimationClip('walk', 1, [
rightArmRotationKF, leftArmRotationKF,
rightLegRotationKF, leftLegRotationKF
]);

// 应用并播放动画
const action = mixer.clipAction(clip);
action.setLoop(THREE.LoopRepeat);
action.play();
});

// 监听鼠标移动事件
document.addEventListener('mousemove', onDocumentMouseMove, false);

// 动画循环
function animate() {
requestAnimationFrame(animate);

if (mixer) {
mixer.update(clock.getDelta());
}

// 动态调整头部和身体部分的旋转
if (head) {
head.rotation.y = mouseX * Math.PI * 0.3; // 头部更大幅度左右旋转
head.rotation.x = mouseY * Math.PI * 0.1; // 头部轻微上下旋转
}

if (body) {
body.rotation.y = mouseX * Math.PI * 0.1; // 身体轻微左右旋转
}

if (rightArm && leftArm) {
rightArm.rotation.y = mouseX * Math.PI * 0.05; // 右臂轻微左右旋转
leftArm.rotation.y = mouseX * Math.PI * 0.05; // 左臂轻微左右旋转
}

if (rightLeg && leftLeg) {
rightLeg.rotation.y = mouseX * Math.PI * 0.05; // 右腿轻微左右旋转
leftLeg.rotation.y = mouseX * Math.PI * 0.05; // 左腿轻微左右旋转
}

renderer.render(scene, camera);
}

animate();
}

// 鼠标移动事件处理函数
function onDocumentMouseMove(event) {
mouseX = (event.clientX / window.innerWidth) * 2 - 1;
mouseY = -(event.clientY / window.innerHeight) * 2 + 1;
}

// 等待 DOM 完全加载
document.addEventListener('DOMContentLoaded', function() {
init();
});

ok,到这里基本就完成了。

导入必要文件

1
loader.load('/models/model.gltf', function(gltf) { //加载模型

现在我们根据上面代码创建一个文件夹:

在博客的根目录下的 /source/models/model.gltf 添加model.gltf模型文件

在博客的根目录下的/source/static/js/threejs-model.js创建js文件添加上面的js代码

在主题的_config,yml文件中引用threejs的CND和我们自己编写js代码:

1
2
3
4
5
6
inject:
head:
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
bottom:
- <script async data-pjax src="/static/js/threejs-model.js"></script>

查看效果

我们直接一个三连,查看效果

1
2
3
hexo cl
hexo g
hexo s