用Three.js绘制一个3D天体系统

stcaolei 2020-03-06

年前就一直研究了下WebGL相关的东西,看了很多资料和文档,这里做了一些小实践,记录分享一下。

代码:链接
预览:链接

demo:
用Three.js绘制一个3D天体系统

前置知识

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中采用的是右手坐标系
用Three.js绘制一个3D天体系统
行星和恒星处于同一平面,所以y轴坐标为0,差别是x轴,以太阳为中心当做原点的话,初始化行星的distance参数就是离原点恒星的距离。通过计算三角函数,可以算出坐标系中的xy轴值。

运动和动画

运动主要是动态计算设置每个行星的x,y轴。
用Three.js绘制一个3D天体系统
这里的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官方还有其他控制相关的库。
用Three.js绘制一个3D天体系统

其他一些细节

还有很多其他一些细节,太阳的外燃烧蒙层,限定视角范围,行星环,鼠标移动到行星显示文字,星星背景等,都可以在源码里看到或者待完善。

tip:在vscode里没有好用的three的Snippets,可以npm i three,利用npm three包的ts智能提示。three中的loader加载物体的纹理皮肤或者字体,3d模型等在本地会被cors block,需要本地工程化,起个node服务或者webpack server支持。

相关推荐