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

Posted

    tags:

    篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用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支持。

    以上是关于用Three.js绘制一个3D天体系统的主要内容,如果未能解决你的问题,请参考以下文章

    如何使用 three.js 绘制自定义 3d 表面?

    有哪些值得推荐的绘制3D的js库

    Three.js教程:第一个3D场景

    three.js 相关概念

    如何使用three.js在运行时绘制线段

    Three.js地球开发—3D经纬度等比地图,3D飞行航线最终效果