用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支持。