基于vue3+threejs实现太阳系与奥尔特云层(结尾附源码)

先看效果,附源码地址,看完觉得还不错的还望不吝一个小小的star

image.png

image.png

1 快速上手
1.1 在项目中使用 npm 包引入
Step 1: 使用命令行在项目目录下执行以下命令

npm install three@0.148.0 --save

Step 2: 在需要用到 three 的 JS 文件中导入

import * as THREE from 'three'
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

配置

  • node 版本 18.17.0
  • 调用three方法生创建three基础功能

Step 1: 新建场景

// 首先定义之后需要用到的参数
let scene, mesh, camera, stats, renderer, geometry, material, width, height;
// 场景
const initScene = () => {
  width = webGlRef.value.offsetWidth; //宽度
  height = webGlRef.value.offsetHeight; //高度
  scene = new THREE.Scene()
  renderer = new THREE.WebGLRenderer();
  renderer.setSize(width, height);
  document.getElementById('webgl').appendChild(renderer.domElement);
}

Step 2: 新建相机

  • 相机有多个参数,最后一个参数是相机拍摄距离
    cemear.position.set可以设置相机位置

    // 相机
    const initCamera = () => {
    // 实例化一个透视投影相机对象
    camera = new THREE.PerspectiveCamera(30, width / height, 1, 50000000);
    //相机在Three.js三维坐标系中的位置
    // 根据需要设置相机位置具体值
    camera.position.set(3500, 1000, 100000);
    camera.lookAt(0, 10, 0);  //y轴上位置10
    // camera.lookAt(mesh.position);//指向mesh对应的位置
    renderer.render(scene, camera);
    }

    Step 3: 创建点光源
    在坐标原点创建点光源,之后用太阳覆盖,模拟太阳光照

    // 光源
    const initPointLight = () => {
    const pointLight = new THREE.PointLight('#ffeedb', 2.0);
    pointLight.intensity = 2;//光照强度
    pointLight.decay = 2;//设置光源不随距离衰减
    pointLight.position.set(0, 0, 0);
    scene.add(pointLight); //点光源添加到场景中
    
    // 光源辅助观察
    const pointLightHelper = new THREE.PointLightHelper(pointLight, 10);
    scene.add(pointLightHelper);
    // pointLight.position.set(100, 200, 150);
    }

最终的结果
由于相机位置拉的比较远,若页面未显示光源,滑动鼠标滚轮即可显示。

const initThree = () => {
  initScene() // 场景
  initCamera() // 相机
  initPointLight() // 光源
  initRender()
}
// 监听性能
const initRender = () => {
  const controls = new OrbitControls(camera, renderer.domElement);
  controls.addEventListener('change', function () {
    renderer.render(scene, camera); //执行渲染操作
  });//监听鼠标、键盘事件
}
nextTick(() => {
  initThree()
})

image.png

2 生成太阳系(太阳和八大行星贴图全部会放在最后面)
Step 1: 生成太阳
设置太阳的形状SphereGeometry(球体),材质MeshBasicMaterial(不受光照影响)。导入sun.jpg为太阳贴图,让太阳看起来更真实。

// sun
const initSun = () => {
  geometry = new THREE.SphereGeometry(300, 32, 16);
  // 添加纹理加载器
  const texLoader = new THREE.TextureLoader();
  const texture = texLoader.load('./public/sun.jpg');
  const material = new THREE.MeshBasicMaterial({
    // color:0x0000FF,
    map: texture,
    side: THREE.DoubleSide  //默认只渲染正面,这里设置双面渲染
  });

  mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
  mesh.position.set(0, 0, 0)
  scene.add(mesh);
}

Step 2: 引入性能监视器stats

//引入性能监视器stats.js
import Stats from 'three/addons/libs/stats.module.js';
// stats对象
const initStats = () => {
  stats = new Stats();
  stats.setMode(1);
  //stats.domElement:web页面上输出计算结果,一个div元素,
  document.body.appendChild(stats.domElement);
}

Step 3: 太阳自转

// 自转
const initSunRotate = () => {
  stats.update();
  renderer.render(scene, camera); //执行渲染操作
  mesh.rotateY(0.01);//每次绕y轴旋转0.01弧度
  requestAnimationFrame(initSunRotate);//请求再次执行渲染函数render,渲染下一帧
}

最终效果
image.png

3 生成八大行星

八大行星按照与太阳之间的距离分为:水星, 金星, 地球, 火星, 木星, 土星, 天王星, 海王星

  • 水星
  • 金星
  • 地球
  • 火星
  • 木星
  • 土星
  • 天王星
  • 海王星

Step 1: 创建八大行星,实现行星自转
依照太阳的创建方法,依次创建八大行星,并实现行星自转

// 水星
const initMercury = () => {
  const geometrys = new THREE.SphereGeometry(5, 32, 16);
  const texLoader = new THREE.TextureLoader();
  const texture = texLoader.load('./public/mercury.jpg');
  const materials = new THREE.MeshPhysicalMaterial({
    map: texture,
    side: THREE.DoubleSide  //默认只渲染正面,这里设置双面渲染
  });

  const meshs = new THREE.Mesh(geometrys, materials); //网格模型对象Mesh
  meshs.position.set(-500, 0, 0)
  scene.add(meshs);

  function renderTemp() {
    renderer.render(scene, camera); //执行渲染操作
    meshs.rotateY(0.05);//每次绕y轴旋转0.01弧度
    requestAnimationFrame(renderTemp);//请求再次执行渲染函数render,渲染下一帧
  }

  renderTemp()
  initPlanet(500)
  // 公转
  let angle = 0, speed = 0.025, distance = 500;

  function rotationMesh() {
    renderer.render(scene, camera); //执行渲染操作
    angle += speed;
    if (angle > Math.PI * 2) {
      angle -= Math.PI * 2;
    }

    meshs.position.set(distance * Math.sin(angle), 0, distance * Math.cos(angle));

    requestAnimationFrame(rotationMesh);//请求再次执行渲染函数render,渲染下一帧
  }

  rotationMesh()
}
// 金星
const initVenus = () => {
  const geometrys = new THREE.SphereGeometry(20, 32, 16);
  const texLoader = new THREE.TextureLoader();
  const texture = texLoader.load('./public/venus.jpg');
  const materials = new THREE.MeshPhysicalMaterial({
    map: texture,
    side: THREE.DoubleSide  //默认只渲染正面,这里设置双面渲染
  });

  const meshs = new THREE.Mesh(geometrys, materials); //网格模型对象Mesh
  meshs.position.set(600, 0, 0)
  scene.add(meshs);

  function renderTemp() {
    renderer.render(scene, camera); //执行渲染操作
    meshs.rotateY(0.05);//每次绕y轴旋转0.01弧度
    requestAnimationFrame(renderTemp);//请求再次执行渲染函数render,渲染下一帧
  }

  renderTemp()
}

// 地球
const initEarth = () => {
  const geometrys = new THREE.SphereGeometry(21, 32, 16);
  const texLoader = new THREE.TextureLoader();
  const texture = texLoader.load('./public/earth.jpg');
  const materials = new THREE.MeshPhysicalMaterial({
    map: texture,
    side: THREE.DoubleSide  //默认只渲染正面,这里设置双面渲染
  });

  const meshs = new THREE.Mesh(geometrys, materials); //网格模型对象Mesh
  meshs.position.set(-850, 0, 0)
  scene.add(meshs);

  function renderTemp() {
    renderer.render(scene, camera); //执行渲染操作
    meshs.rotateY(0.05);//每次绕y轴旋转0.01弧度
    requestAnimationFrame(renderTemp);//请求再次执行渲染函数render,渲染下一帧
  }

  renderTemp()
}

// 火星
const initMars = () => {
  const geometrys = new THREE.SphereGeometry(11, 32, 16);
  const texLoader = new THREE.TextureLoader();
  const texture = texLoader.load('./public/mars.jpg');
  const materials = new THREE.MeshPhysicalMaterial({
    map: texture,
    side: THREE.DoubleSide  //默认只渲染正面,这里设置双面渲染
  });

  const meshs = new THREE.Mesh(geometrys, materials); //网格模型对象Mesh
  meshs.position.set(1150, 0, 0)
  scene.add(meshs);

  function renderTemp() {
    renderer.render(scene, camera); //执行渲染操作
    meshs.rotateY(0.05);//每次绕y轴旋转0.01弧度
    requestAnimationFrame(renderTemp);//请求再次执行渲染函数render,渲染下一帧
  }

  renderTemp()
}

// 木星
const initJupiter = () => {
  const geometrys = new THREE.SphereGeometry(100, 32, 16);
  const texLoader = new THREE.TextureLoader();
  const texture = texLoader.load('./public/jupiter.jpg');
  const materials = new THREE.MeshPhysicalMaterial({
    map: texture,
    side: THREE.DoubleSide  //默认只渲染正面,这里设置双面渲染
  });

  const meshs = new THREE.Mesh(geometrys, materials); //网格模型对象Mesh
  meshs.position.set(-1450, 0, 0)
  scene.add(meshs);

  function renderTemp() {
    renderer.render(scene, camera); //执行渲染操作
    meshs.rotateY(0.05);//每次绕y轴旋转0.01弧度
    requestAnimationFrame(renderTemp);//请求再次执行渲染函数render,渲染下一帧
  }

  renderTemp()
}

// 土星
const initSaturn = () => {
  const geometrys = new THREE.SphereGeometry(80, 32, 16);
  const texLoader = new THREE.TextureLoader();
  const texture = texLoader.load('./public/saturn.jpg');
  const materials = new THREE.MeshPhysicalMaterial({
    map: texture,
    side: THREE.DoubleSide  //默认只渲染正面,这里设置双面渲染
  });

  const meshs = new THREE.Mesh(geometrys, materials); //网格模型对象Mesh
  meshs.position.set(1700, 0, 0)
  scene.add(meshs);

  function renderTemp() {
    renderer.render(scene, camera); //执行渲染操作
    meshs.rotateY(0.05);//每次绕y轴旋转0.01弧度
    requestAnimationFrame(renderTemp);//请求再次执行渲染函数render,渲染下一帧
  }

  renderTemp()
}

// 天王星
const initUranus = () => {
  const geometrys = new THREE.SphereGeometry(45, 32, 16);
  const texLoader = new THREE.TextureLoader();
  const texture = texLoader.load('./public/uranus.jpg');
  const materials = new THREE.MeshPhysicalMaterial({
    map: texture,
    side: THREE.DoubleSide  //默认只渲染正面,这里设置双面渲染
  });

  const meshs = new THREE.Mesh(geometrys, materials); //网格模型对象Mesh
  meshs.position.set(-2000, 0, 0)
  scene.add(meshs);

  function renderTemp() {
    renderer.render(scene, camera); //执行渲染操作
    meshs.rotateY(0.05);//每次绕y轴旋转0.01弧度
    requestAnimationFrame(renderTemp);//请求再次执行渲染函数render,渲染下一帧
  }

  renderTemp()
}

// 海王星
const initNeptune = () => {
  const geometrys = new THREE.SphereGeometry(45, 32, 16);
  const texLoader = new THREE.TextureLoader();
  const texture = texLoader.load('./public/neptune.jpg');
  const materials = new THREE.MeshPhysicalMaterial({
    map: texture,
    side: THREE.DoubleSide  //默认只渲染正面,这里设置双面渲染
  });

  const meshs = new THREE.Mesh(geometrys, materials); //网格模型对象Mesh
  meshs.position.set(2300, 0, 0)
  scene.add(meshs);
  meshs.rotation.y = 100;//每次绕y轴旋转0.01弧度

  function renderTemp() {
    renderer.render(scene, camera); //执行渲染操作
    meshs.rotateY(0.05);//每次绕y轴旋转0.01弧度
    requestAnimationFrame(renderTemp);//请求再次执行渲染函数render,渲染下一帧
  }

  renderTemp()
}

最终效果图
image.png

Step 2: 实现行星公转
自转都有了,那么公转能少了吗?
以水星为例子,在initMercury方法中添加

let angle = 0, speed = 0.025, distance = 500;

  function rotationMesh() {
    renderer.render(scene, camera); //执行渲染操作
    angle += speed;
    if (angle > Math.PI * 2) {
      angle -= Math.PI * 2;
    }

    meshs.position.set(distance * Math.sin(angle), 0, distance * Math.cos(angle));

    requestAnimationFrame(rotationMesh);//请求再次执行渲染函数render,渲染下一帧
  }

  rotationMesh()

其中需要注意的是distance 参数,与上面方法中的meshs.position.set(600, 0, 0)必须相等,相当于公转半径。speed 参数是公转角度,不同行星尽量设置不同的公转角度,相当于公转速度。
依照同样的方法,在其余七大行星中设置公转,最终的结果
image.png

Step 3: 公转轨迹
虽然行星都转起来了,但是是不是感觉少了点什么,于是加上公转轨迹看下效果

// 公转轨迹
const initPlanet = (distance) => {
  /*轨道*/
  let track = new THREE.Mesh(new THREE.RingGeometry(distance - 0.2, distance + 0.2, 64, 1),
      new THREE.MeshBasicMaterial({color: 0xffffff, side: THREE.DoubleSide})
  );
  track.rotation.x = -Math.PI / 2;
  scene.add(track);
}

在每个创建行星的方法中调用,依旧以水星为例

// 水星
const initMercury = () => {
  const geometrys = new THREE.SphereGeometry(5, 32, 16);
  const texLoader = new THREE.TextureLoader();
  const texture = texLoader.load('./public/mercury.jpg');
  const materials = new THREE.MeshPhysicalMaterial({
    map: texture,
    side: THREE.DoubleSide  //默认只渲染正面,这里设置双面渲染
  });

  const meshs = new THREE.Mesh(geometrys, materials); //网格模型对象Mesh
  meshs.position.set(-500, 0, 0)
  scene.add(meshs);

  function renderTemp() {
    renderer.render(scene, camera); //执行渲染操作
    meshs.rotateY(0.05);//每次绕y轴旋转0.01弧度
    requestAnimationFrame(renderTemp);//请求再次执行渲染函数render,渲染下一帧
  }

  renderTemp()
  initPlanet(500)
  // 公转
  let angle = 0, speed = 0.025, distance = 500;

  function rotationMesh() {
    renderer.render(scene, camera); //执行渲染操作
    angle += speed;
    if (angle > Math.PI * 2) {
      angle -= Math.PI * 2;
    }

    meshs.position.set(distance * Math.sin(angle), 0, distance * Math.cos(angle));

    requestAnimationFrame(rotationMesh);//请求再次执行渲染函数render,渲染下一帧
  }

  rotationMesh()
}

最终效果,感觉有点样子了

image.png

4.奥尔特云层

// 奥尔特云层
const atStars = () => {
  /*背景星星*/
  const particles = 30000;  //星星数量
  /*buffer做星星*/
  const bufferGeometry = new THREE.BufferGeometry();

  /*32位浮点整形数组*/
  let positions = new Float32Array( particles * 3 );
  let colors = new Float32Array( particles * 3 );

  let color = new THREE.Color();

  const gap = 80000; // 定义星星的最近出现位置
  for ( let i = 0; i < positions.length; i += 3 ) {
    // positions

    /*-gap < x < gap */
    let x = ( Math.random() * gap )* (Math.random()<.5? -1 : 1);
    let y = ( Math.random() * gap )* (Math.random()<.5? -1 : 1);
    let z = ( Math.random() * gap )* (Math.random()<.5? -1 : 1);

    /*找出x,y,z中绝对值最大的一个数*/
    let biggest = Math.abs(x) > Math.abs(y) ? Math.abs(x) > Math.abs(z) ? 'x' : 'z' :
        Math.abs(y) > Math.abs(z) ? 'y' : 'z';

    let pos = { x, y, z};

    /*如果最大值比n要小(因为要在一个距离之外才出现星星)则赋值为n(-n)*/
    if(Math.abs(pos[biggest]) < gap) pos[biggest] = pos[biggest] < 0 ? -gap : gap;

    x = pos['x'];
    y = pos['y'];
    z = pos['z'];

    positions[ i ]     = x;
    positions[ i + 1 ] = y;
    positions[ i + 2 ] = z;

    // colors
    /*70%星星有颜色*/
    let hasColor = Math.random() > 0.3;
    let vx, vy, vz;

    if(hasColor){
      vx = (Math.random()+1) / 2 ;
      vy = (Math.random()+1) / 2 ;
      vz = (Math.random()+1) / 2 ;
    }else{
      vx = 1 ;
      vy = 1 ;
      vz = 1 ;
    }

    color.setRGB( vx, vy, vz );

    colors[ i ]     = color.r;
    colors[ i + 1 ] = color.g;
    colors[ i + 2 ] = color.b;
  }
  // console.log(positions, "positions >>>>>>>>>>>>>>>")
  bufferGeometry.setAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );
  bufferGeometry.setAttribute( 'color', new THREE.BufferAttribute( colors, 3 ) );
  bufferGeometry.computeBoundingSphere();

  /*星星的material*/
  let material = new THREE.PointsMaterial( { size: 6, vertexColors: THREE.VertexColors } );
  const particleSystem = new THREE.Points( bufferGeometry, material );
  scene.add( particleSystem );

}

最终效果
image.png
image.png

竟然是正方体形状的云层,是因为上面x,y,z坐标缘故。因此下面就要把正方体表面的行星坐标变换成球面坐标。正方体表面坐标(x, y, z)和球面半径R的坐标都是已知的,那么将之全部转换为球面坐标(a, b, c)。

  • x = rsinθcosΦ
  • y = rsinθsinΦ
  • z = r*cosθcos
    其中r = gap,θ = Math.acos(Math.abs(z) / gap),Φ = Math.atan(Math.abs(y) / Math.abs(x))。因此可得出:
  • a = gap Math.sin(Math.acos(Math.abs(z) / gap)) Math.cos(Math.atan(Math.abs(y) / Math.abs(x)))
  • b = gap Math.sin(Math.acos(Math.abs(z) / gap)) Math.sin(Math.atan(Math.abs(y) / Math.abs(x)))
  • c = gap * Math.cos(Math.acos(Math.abs(z) / gap))
    带入公式,此时渲染的结果并不理想,是因为空间坐标分为八个象限,a, b, c的正负值出错。最后公式改为:
  • a = gap Math.sin(Math.acos(Math.abs(z) / gap)) Math.cos(Math.atan(Math.abs(y) / Math.abs(x))) * (x/Math.abs(x));
  • b = gap Math.sin(Math.acos(Math.abs(z) / gap)) Math.sin(Math.atan(Math.abs(y) / Math.abs(x))) * (y/Math.abs(y));
  • c = gap Math.cos(Math.acos(Math.abs(z) / gap)) (z/Math.abs(z));
    将的出来的结果带入上面position数组替换x, y, z的值

    positions[ i ]     = gap * Math.sin(Math.acos(Math.abs(z) / gap)) * Math.cos(Math.atan(Math.abs(y) / Math.abs(x))) * (x/Math.abs(x));
    positions[ i + 1 ] = gap * Math.sin(Math.acos(Math.abs(z) / gap)) * Math.sin(Math.atan(Math.abs(y) / Math.abs(x))) * (y/Math.abs(y));
    positions[ i + 2 ] = gap * Math.cos(Math.acos(Math.abs(z) / gap)) * (z/Math.abs(z));

    是不是很简单,随便来个初中生就会做了,不知道大学生的你会不会做?话不多说,来看下效果

image.png

搞定收工!
开玩笑的,没有,后面的星空背景太空旷了,没有星空的感觉,那么最后加个星空背景吧。
用的是跟奥尔特云层同样的方法,不过星体不是放在球面了,而是分布到球体里面散开。

// 背景星体
const backStars = () => {
  /*背景星星*/
  const particles = 50000;  //星星数量
  /*buffer做星星*/
  const bufferGeometry = new THREE.BufferGeometry();

  /*32位浮点整形数组*/
  let positions = new Float32Array( particles * 3 );
  let colors = new Float32Array( particles * 3 );

  let color = new THREE.Color();

  const gap = 10000000; // 定义星星的最近出现位置
  for ( let i = 0; i < positions.length; i += 3 ) {
    // positions

    /*-gap < x < gap */
    let x = ( Math.random() * 6 * gap ) * (Math.random()<.5? -1 : 1);
    let y = ( Math.random() * 6 * gap ) * (Math.random()<.5? -1 : 1);
    let z = ( Math.random() * 6 * gap ) * (Math.random()<.5? -1 : 1);

    /*找出x,y,z中绝对值最大的一个数*/
    let biggest = Math.abs(x) > Math.abs(y) ? Math.abs(x) > Math.abs(z) ? 'x' : 'z' :
        Math.abs(y) > Math.abs(z) ? 'y' : 'z';

    let pos = { x, y, z};

    /*如果最大值比n要小(因为要在一个距离之外才出现星星)则赋值为n(-n)*/
    if(Math.abs(pos[biggest]) <  (gap)) pos[biggest] = pos[biggest] < 0 ? - gap :  gap;

    x = pos['x'];
    y = pos['y'];
    z = pos['z'];

    // positions[ i ]     = x;
    // positions[ i + 1 ] = y;
    // positions[ i + 2 ] = z;
    let tempGap = Math.sqrt(x * x + y * y + z * z) >  6 * gap ?  6 * gap : Math.sqrt(x * x + y * y + z * z)

    positions[ i ]     = tempGap * Math.sin(Math.acos(Math.abs(z) / tempGap)) * Math.cos(Math.atan(Math.abs(y) / Math.abs(x))) * (x/Math.abs(x));
    positions[ i + 1 ] = tempGap * Math.sin(Math.acos(Math.abs(z) / tempGap)) * Math.sin(Math.atan(Math.abs(y) / Math.abs(x))) * (y/Math.abs(y));
    positions[ i + 2 ] = tempGap * Math.cos(Math.acos(Math.abs(z) / tempGap)) * (z/Math.abs(z));

    // positions[ i ]     = Math.atan(y/x) * (x/Math.abs(x));
    // positions[ i + 1 ] = Math.sqrt(gap * gap - (Math.atan(y/x)) * Math.atan(y/x) + gap * Math.sin(Math.atan((y / x))) * gap * Math.sin(Math.atan((y / x)))) * (z/Math.abs(z));
    // positions[ i + 2 ] = gap * Math.sin(Math.atan((y / x))) * (y/Math.abs(y));

    // colors
    // colors
    /*70%星星有颜色*/
    let hasColor = Math.random() > 0.3;
    let vx, vy, vz;

    if(hasColor){
      vx = (Math.random()+1) / 2 ;
      vy = (Math.random()+1) / 2 ;
      vz = (Math.random()+1) / 2 ;
    }else{
      vx = 1 ;
      vy = 1 ;
      vz = 1 ;
    }

    color.setRGB( vx, vy, vz );

    colors[ i ]     = color.r;
    colors[ i + 1 ] = color.g;
    colors[ i + 2 ] = color.b;
  }
  // console.log(positions, "positions >>>>>>>>>>>>>>>")
  bufferGeometry.setAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );
  bufferGeometry.setAttribute( 'color', new THREE.BufferAttribute( colors, 3 ) );
  bufferGeometry.computeBoundingSphere();

  /*星星的material*/
  let material = new THREE.PointsMaterial( { size: 6, vertexColors: THREE.VertexColors } );
  const particleSystem = new THREE.Points( bufferGeometry, material );
  scene.add( particleSystem );

}

最终效果,这下是真搞定了!
image.png

  • 仓库地址:Github(点击跳转Github源码地址)
  • 仓库地址:Gitee(点击跳转Gitee源码地址)

紫槐
4 声望0 粉丝

初学者