用Three.js绘制一个3D天体系统
年前就一直研究了下WebGL相关的东西,看了很多资料和文档,这里做了一些小实践,记录分享一下。
demo:
前置知识
WebGL和Threejs的关系:
WebGL是一种 3D 绘图协议,这种绘图技术标准结合了JavaScript和OpenGL ES 2.0,在HTML5的Canvas元素中使用,从而可以在 Web 浏览器中呈现 3D 场景,
而Threejs是对WebGL的封装,可以让之前很少接触OpenGL的研发人员直接上手3D开发。掌握WebGL有利于理解Threejs的各种api,理解threejs开发的理念。
上手Threejs之前,最好多看看理解理解WebGL,GLSL,线性代数,一些几何算法。
具体相关,可以到网上搜索。
着手开发
创建三要素
threejs三要素:场景,相机,渲染器,这三个对象是threejs一个3d场景必须创建的三要素:
let scene = new THREE.Scene(); //创建场景
let camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
1,
cameraFar
); //创建透视相机 (参数分别是 FOV:可视角度, aspect ratio:宽高比, near:近剪切面, far:远剪切面)
// 渲染器
let renderer = new THREE.WebGLRenderer({
canvas
});
renderer.render(scene, camera);
创建天体物体
3d天体主要以太阳系为模型,这里需要创建中间的太阳和八大行星。物体的创建在three里面有很全的几何类,球体,圆环,正方体等等,这些类主要以Mesh为基类,采用三角形网格。这里我们把行星的初始封装成一个方法:
function initStar(name, speed, angle, color, distance, volume, ringInfo) {
let mesh = new THREE.Mesh(
new THREE.SphereGeometry(volume, 16, 16),
new THREE.MeshLambertMaterial({
color
})
);
mesh.position.x = distance; // 右手坐标系,x即为在同一个平面上行星距离太阳的距离
// 其他自定义属性
mesh.receiveShadow = true;
mesh.castShadow = true;
mesh.name = name;
// !行星轨道
let track = new THREE.Mesh(
new THREE.RingGeometry(distance - 0.2, distance + 0.2, 64, 1),
new THREE.MeshBasicMaterial({
color: 0x888888,
side: THREE.DoubleSide
})
);
track.rotation.x = -Math.PI / 2;
scene.add(track);
let star = {
name,
speed,
angle,
distance,
volume,
Mesh: mesh
};
// 有行星环的情况
if (ringInfo) {
// console.log("进入了ring,Info为", ringInfo);
let ring = new THREE.Mesh(
new THREE.RingGeometry(ringInfo.innerRedius, ringInfo.outerRadius, 32, 6),
new THREE.MeshBasicMaterial({
color: ringInfo.color,
side: THREE.DoubleSide,
opacity: 0.7,
transparent: true
})
);
ring.name = `Ring of ${name}`;
ring.rotation.x = -Math.PI / 3;
ring.rotation.y = -Math.PI / 4;
scene.add(ring);
star.ring = ring;
}
scene.add(mesh);
return star;
}
name, speed, angle, color, distance, volume, ringInfo的参数意义分别是,行星名字,初始角度,距离太阳的直线距离,行星颜色,行星x轴坐标(离恒星太阳的距离),半径,行星环信息。
注意three中采用的是右手坐标系
行星和恒星处于同一平面,所以y轴坐标为0,差别是x轴,以太阳为中心当做原点的话,初始化行星的distance参数就是离原点恒星的距离。通过计算三角函数,可以算出坐标系中的xy轴值。
运动和动画
运动主要是动态计算设置每个行星的x,y轴。
这里的y轴实际对应是three坐标系中的z轴。天体都在一个平面,天体在three坐标系中的y轴都为0。
// 行星公转
function revolution(star) {
star.angle += star.speed;
star.angle > Math.PI * star.distance &&
(star.angle -= Math.PI * star.distance);
star.Mesh.position.set(
star.distance * Math.sin(star.angle),
0,
star.distance * Math.cos(star.angle)
);
}
function move() {
//太阳自转
Sun.rotation.y += 0.008; // 旋转网格的x轴
// 行星公转
stars.map((star) => revolution(star));
control.update(clock.getDelta()); //此处传入的delta是两次animationFrame的间隔时间,用于计算速度
renderer.render(scene, camera);
requestAnimationFrame(move);
}
注意threejs里面几乎所有的动画都是用rFA做的,rFA做动画的好处就是能保证整体动画速度不会被“拖慢”,相对的保证动画流畅。这一点其实网上很多博客资料都讲了,但是都没有说清楚是怎么保证动画流畅的,而且这里的流畅是有歧义的,rFA会采用跳过某些帧的方式表现动画,有时候动画表现上会出现“卡顿”,所以这里的流畅是相对结果而言。
什么意思呢?打个比喻:
比如说你的游戏逻辑
你有一个人物在移动,移动速度是每秒60px,也就是每帧1px
如果你的游戏逻辑执行时间超过了 1/60 秒
那结果就是,一秒钟过后,人物没有正确的移动 60px
但如果你用 rAF 保证上一帧逻辑不阻塞下一帧逻辑
你的运算就不会堵住
但人物的位置是对的
再举个例子 手机屏幕 你做一个方块 手指拖动到哪他就移动到哪
如果运算卡住的话 他会不跟手 你手拖很远了他还在慢慢移动
但是如果运算不阻塞 即便可能会有点瞬移 但方块一直在你手指下。
所以rFA保证动画流畅就是这么个意思。
光源
做到这,跑来的话你发现是黑乎乎的一片,因为场景里还缺少光源。
定义光源和环境光。
光源就是真实的一个光源点,以中间的太阳恒星为光源点,公转的行星背部也有阴影的真实效果,光源点的参数可以定义光颜色,光照强度,以及光照到0强度的距离:
PointLight( color : Integer, intensity : Float, distance : Number,
decay : Float ) color - (可选参数)) 十六进制光照颜色。 缺省值 0xffffff (白色)。 intensity
- (可选参数) 光照强度。 缺省值 1。
distance - 这个距离表示从光源到光照强度为0的位置。 当设置为0时,光永远不会消失(距离无穷大)。缺省值 0. decay -
沿着光照距离的衰退量。缺省值 1。 在 physically correct 模式中,decay = 2。
环境光主要是模拟整体环境的光,这种光每个狭隙都能照射到,理想中的均匀光。配合宇宙背景小点点行星亮光会更真实。
//环境光
let ambient = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambient);
/*太阳光*/
let sunLight = new THREE.PointLight(0xddddaa, 1.5, 500);
scene.add(sunLight);
行星运动的轨迹
为了更好区别每个行星的运动,需要给每个行星公转的轨迹显示出来。
其实就是在初始化行星的时候,在行星的distance基础上初始化一个圆环物体,设置内环外环半径。
// !行星轨道
let track = new THREE.Mesh(
new THREE.RingGeometry(distance - 0.2, distance + 0.2, 64, 1),
new THREE.MeshBasicMaterial({
color: 0x888888,
side: THREE.DoubleSide
})
);
track.rotation.x = -Math.PI / 2;
scene.add(track);
注意需要旋转默认圆环体是竖着的,需要旋转一下。
视角控制
引入第一人称视角控制,视角跟着鼠标和键盘的方向键控制视角和距离。
/*镜头控制*/
control = new THREE.FirstPersonControls(camera, canvas);
control.movementSpeed = 100; //镜头移速
control.lookSpeed = 0.125; //视角改变速度
control.lookVertical = true; //是否允许视角上下改变
camera.lookAt(new THREE.Vector3(0, 0, 0));
FirstPersonControls库需要作为文件单独引入,three官方还有其他控制相关的库。
其他一些细节
还有很多其他一些细节,太阳的外燃烧蒙层,限定视角范围,行星环,鼠标移动到行星显示文字,星星背景等,都可以在源码里看到或者待完善。
tip:在vscode里没有好用的three的Snippets,可以npm i three,利用npm three包的ts智能提示。three中的loader加载物体的纹理皮肤或者字体,3d模型等在本地会被cors block,需要本地工程化,起个node服务或者webpack server支持。