在上一篇文章中,我整理了 Threejs 中的一些基本概念。现在这一篇,就使用上次所整理的内容加上一些不复杂的算法,来实现一个运转着的太阳系模型。这里的所有行星大小及公转时间,尽量贴合实际。
索引
实现思路
这篇文章于早前发出,收到了前辈的指教。反思后发现确实后面的内容比较流水账,欠缺一个思路的总结和梳理,而只是一味实现。这里我先将整体的实现方案梳理出来,接着我们再分解并一步步去实现它。
我将其主要分为两个部分,静态的实现,以及动态的鼠标交互。
静态实现
- 太阳系模型的核心是八大行星围绕着太阳做公转运动。涉及到两点,一是运动,二是公转。运动使用动画即可完成。至于公转,实际上行星的运转是做椭圆周运动,我们在这里将其简化为圆周运动,那么核心就是计算圆周运动的坐标点。
- 那么实现这个静态的太阳系模型就有一定的思路了,首先我们用球体初始化行星,按照距离远近摆放在场景中。然后添加环状图形来当做轨道,辅助观察。接着文本采用外部引入的方式,加载后位置基本与行星相同,减去一定大小将其放在正上方。最后,将行星按照计算出的坐标信息,每一帧去设置位置。最终的效果就是动态的公转效果。
- 这里有几点值得注意:
- 字体在 Threejs 中需要另外加载,格外引入。
- 行星的初始化位置可以用随机的方式来分布,使其更真实。当然,有精力的话可以参考实际去计算坐标,摆放接近真实的位置。
- 后来在行星中添加了月亮,涉及的计算就是把以太阳左标为原点改为了以地球坐标为原点,其他计算方式则相同。
- 为行星添加光照效果,就是在太阳的位置上放一个点光源,然后在周围的地方摆放一个环境光光源优化视觉效果。
- 采用图片纹理的方式优化行星的视觉效果。
动态交互
- 场景基本完成后。我会添加一个鼠标交互,即鼠标移到行星位置上时,显示行星的名字。
- 这里主要的复杂点在于坐标系的转化。3d 的坐标系和浏览器平时的坐标系原点及坐标位置不一。这点在下文我详细说明。
场景初始化
- 首先,创建一个场景用来渲染这一切。
1 | let scene = new THREE.Scene() |
创建行星
- 初始化各大星球,即创建球体。在参考了真实大小和距离后,我们尽量按接近真实的比例来创建。
- 首先我们创建一个对象数组来保存各星球的相关数据。包括名字,大小,位置,颜色等等属性。
1 | const planets = [{ |
- 根据这些属性,我们再用形状和材质来创建球体。
1 | function initPlanet(size, color) { |
- 这里我采用遍历数组的数据的方式来依次创建球体。并用一个 THREE.Group 对象来统一保存。
- 在初始化的数据里,我们可以拿到位置等信息,在这里给球体也加上相应的位置。
1 | function initPlanets() { |
- 场景中添加球体物体后,理论上应该能看到九个球体了。但是,这里只能见到一个球体。因为像机的默认视角是正对着球,而此时的球的位置正好连成一条线。我们来调整一下,让相机从高处俯视。
1 | camera.position.set(0, 10, 20) |
创建轨道
- 为了更清晰得看清行星运转,我们添加一些轨道线来辅助。
- 所用的是
RingGeometry
几何形状。创建的方式类似行星。 - 但是需要注意的是,创建的环默认是垂直于水平面的,我们需要将它做一个旋转,转到水平面上来。
1 | function initTrack(size, color) { |
- 这样就可以在场景中看到行星在轨道上了。
创建文本
- 我们使用文本来标识行星的名字,位置在行星正上方。
- THREE 创建文本需要先加载字体。通常使用字体是先通过官方的方法加载 json 文件。
1 | const textLoader = new THREE.FontLoader() |
- 在自己的案例中,parcel 打包后似乎没有静态文件目录,于是这里了解了下 THREE.Font 部分代码后,发现只需要重新构造一个就可以。
1 | import Font from './assets/fonts/helvetiker_regular.typeface.json' |
- 最终创建文本的方式和其他物体相似,也是形状和纹理。
1 | function initText(content) { |
- 最后,只要让文本使用对应行星的位置,再做一些小小的位移即可。
行星添加公转效果
- 在场景里添加以上图形后,我们能看到部分物体了,虽然可能位置有部分重叠。现在我们来给每个行星添加动态效果,让所有行星运转。
- 公转的本质是改变行星的坐标位置,每一帧进行一次重定位,那么球体看起来就是运动的。
- 这里的公转,就是围绕太阳做圆周运动,以太阳为中心,距离为半径,圆周上的点就是位置。所以每个行星的位置计算就是半径和弧度的计算。
- 例如距离为 5 的星球,圆周长则为
2π * r
, 即Math.PI * 2 * 5
,计算每帧的弧度则为2π * r / 60 / 60 / r
即Math.PI * 2 * 5 / 60 / 60 / 5
. - 所有行星在同一水平面,所以 y 的坐标固定,具体的位置就是
Math.sin(Math.PI * 2 / 3600) * r, 5, Math.cos(Math.PI * 2 / 3600) * r
- 因为每个行星的公转速度不一,所以我们再加一个变量来控制速度,整理后如下
1 | function calcSpeed(rev) { |
- 文字的移动效果同行星,只是位置要在行星的上方,并且水平方向上减去文本大小的一半。
1 | const r = text && text.geometry && text.geometry.boundingSphere.radius || 0 |
行星添加光照效果
- 为了使行星看起来更加真实,我们使用另一种材质来创建行星。并给太阳一个光照效果。
1 | // 受光照影响的材质 |
- 至此,一个有点像模像样的太阳系微缩模型完成了。
- 接下来我们为这个模型加点动态交互效果。
场景添加鼠标事件
- 我们在这里做一个较为简易的交互,即鼠标移到相应的行星上就显示该行星的名字。
- 3d 空间中,鼠标的交互事件的本质是获取鼠标点的坐标,然后转化为 3d 坐标系里的坐标,然后判断位置是否在物体内,然后做一些处理。
- 这部分的重点就在于将鼠标坐标转化为 3d 坐标系里的坐标。即坐标换算。
- 首先我们要创建一个鼠标对象,然后获取鼠标的坐标。
1 | let mouse = new THREE.Vector2() |
- 3d 坐标系的不同在于他是以场景中心作为原点的。而鼠标坐标是以左上角为原点的。转换后要将坐标赋值给鼠标对象。所以这里的坐标换算计算公式为:
1 | function onDomMouseMove(event) { |
- 获取场景中的鼠标位置后。接下去就是判断位置是否在物体上。这里的逻辑是,由相机发往一个射线到鼠标位置处,期间相交的第一个物体就是鼠标点中的物体。即 raycatser 对象中。
1 | let raycaster = new THREE.Raycaster() |
- 现在我们已经能获取到鼠标位置所在的物体了。剩下是完成这个交互,先将文字默认隐藏,然后在鼠标滑过的时候再显示出来。
- 首先将文本存在同一个组里,便于查找。然后在鼠标滑过行星时,使用行星名字来找出对应的文本,然后显示。
1 | function initTexts() { |
- 到这儿一个差不多的太阳系模型就完成啦。
- 后续还可以给各行星的初始位置添加随机位置,给行星添加纹理效果,给土星加上土星环,给地球加上月亮,再加上些星星背景,那就挺好看了。
- 具体的代码在这里**threejs-learning**
最终效果
- gif 显示的较为简陋。当然,更清楚的效果还是直接跑项目吧~
评论