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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章