基于vue3+threejs实现太阳系与奥尔特云层(结尾附源码)
先看效果,附源码地址,看完觉得还不错的还望不吝一个小小的star
- 在线预览:预览
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()
})
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,渲染下一帧
}
最终效果
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()
}
最终效果图
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 参数是公转角度,不同行星尽量设置不同的公转角度,相当于公转速度。
依照同样的方法,在其余七大行星中设置公转,最终的结果
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()
}
最终效果,感觉有点样子了
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 );
}
最终效果
竟然是正方体形状的云层,是因为上面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));
是不是很简单,随便来个初中生就会做了,不知道大学生的你会不会做?话不多说,来看下效果
搞定收工!
开玩笑的,没有,后面的星空背景太空旷了,没有星空的感觉,那么最后加个星空背景吧。
用的是跟奥尔特云层同样的方法,不过星体不是放在球面了,而是分布到球体里面散开。
// 背景星体
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 );
}
最终效果,这下是真搞定了!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。