使用three.js实现太阳系,参考文章 ThreeJS 轻松实现主视觉太阳系漫游 。 效果如下:

html部分
html部分主要定义了canvas。引入了一些必须的js文件:
- three.js: 一个浏览器中的3D引擎,我们主要使用的js库。
- OrbitControls.js: three.js实现的轨道控制器,用来控制场景变换。
- stats.min.js: three.js中用来统计FPS的小插件
- main.js: 我们要实现的部分。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chorme=1">
<title>Solar System</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<canvas id="main"></canvas>
<script src="js/three.js"></script>
<script src="js/OrbitControls.js"></script>
<script src='js/stats.min.js'></script>
<script src="js/main.js"></script>
</body>
</html>
JS部分
准备变量
首先准备本部分要使用到的变量。
// 场景,渲染器,镜头,背景星星,帧率器,第一人称控制
let scene, renderer, camera, particleSystem, stat, control;
// 初始化星体
let sun, // 太阳
mercury, // 水星
venus, //金星
earth, //地球
mars, //火星
jupiter, // 木星
saturn, // 土星
uranus, //天王星
neptune, // 海王星
stars = [];
const cameraFar = 3000; // 镜头视距
let starNames = {}; // 指向显示的星星名字对象
let dispplayName; // 当前显示名字
let clock = new THREE.Clock(); // 第一人称控制需要,用于计算时间间隔
let raycaster = new THREE.Raycaster(); // 指向镭射
let mouse = new THREE.Vector2(); // 鼠标屏幕向量
let displayName; // 当前显示名字
初始化
初始化three.js中的必备要素: render、scene和camera。
// 画布
const canvas = document.getElementById("main");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// 渲染器
renderer = new THREE.WebGLRenderer({canvas});
renderer.shadowMap.enabled = true;
renderer.shadowMapSoft = true;
renderer.setClearColor(0xffffff, 0);
// 场景
scene = new THREE.Scene();
// 镜头
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, cameraFar);
camera.position.set(-200, 50, 0)
camera.lookAt(new THREE.Vector3(0, 0, 0));
scene.add(camera);
添加行星
首先添加最重要的太阳。
// 太阳
let sunSkinPic = new THREE.TextureLoader().load('img/sun_bg.jpg', {}, function() {
renderer.render(scene, camera);
});
sun = new THREE.Mesh( new THREE.SphereGeometry( 12 ,16 ,16 ),
new THREE.MeshLambertMaterial({
color: 0xffff00,
emissive: 0xdd4422,
map: sunSkinPic
})
);
sun.name='Sun';
sun.castShadow = true;
sun.receiveShadow = true;
scene.add(sun);
然后批量添加其它行星。
// 星球
mercury = this.initPlanet('Mercury', 0.02, 0, 'rgb(124, 131, 203)', 20, 2);
stars.push(mercury);
venus = this.initPlanet("venus", 0.012, 0,'rgb(190, 130, 44)', 30, 4);
stars.push(venus);
earth = this.initPlanet('Earth', 0.010, 0,'rgb(46,69,119)',40,5);
stars.push(earth);
mars = this.initPlanet('Mars', 0.008, 0,'rgb(210,81,16)',50,4);
stars.push(mars);
jupiter = this.initPlanet('Jupiter', 0.006, 0,'rgb(254,208,101)',70,9);
stars.push(jupiter);
saturn = this.initPlanet('Saturn', 0.005, 0,'rgb(210,140,39)',100,7, {
color: 'rgb(136, 75, 30)',
innerRadius: 9,
outerRadius:11
});
stars.push(saturn);
uranus = this.initPlanet('Uranus', 0.003, 0,'rgb(49,168,218)',120,4);
stars.push(uranus);
neptune = this.initPlanet('Neptune', 0.002, 0,'rgb(84,125,204)',150,3);
stars.push(neptune);
// 环境光
let ambient = new THREE.AmbientLight(0x999999);
scene.add(ambient);
// 太阳光
let sunLight = new THREE.PointLight(0xddddaa, 1.5, 500);
scene.add(sunLight);
// 初始化行星
function initPlanet(name, speed, angle, color, distance, volume, ringMsg) {
let mesh = new THREE.Mesh(new THREE.SphereGeometry( volume, 16, 16),
new THREE.MeshLambertMaterial( { color } )
);
mesh.position.x = -distance
mesh.receiveShadow = true
mesh.castShadow = true;
mesh.name = name;
let star = {
name,
speed,
angle,
distance,
volume,
Mesh: mesh
}
// 如果有碎星带
if(ringMsg) {
let ring = new THREE.Mesh( new THREE.RingGeometry(ringMsg.innerRadius, ringMsg.outerRadius, 32, 6),
new THREE.MeshBasicMaterial( { color: ringMsg.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;
}
// 轨道
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);
scene.add(mesh);
return star
}
还要添加一个让星星动起来的方法。
// 运动
function moveEachStar(star) {
star.angle += star.speed;
if(star.angle > Math.PI * 2) {
star.angle -= Math.PI * 2;
}
star.Mesh.position.set(star.distance * Math.sin(star.angle), 0, star.distance * Math.cos(star.angle));
// 碎星带
if(star.ring) {
star.ring.position.set(star.distance * Math.sin(star.angle), 0,
star.distance * Math.cos(star.angle));
}
}
镜头控制
借助OrbitControls.js实现镜头控制
control = new THREE.OrbitControls(camera, canvas);
control.target = new THREE.Vector3(0, 0, 0);
control.autoRatate = false; // 关闭自动旋转
帧率监测
引入一个帧率监视的工具,以便我们对整个动画的效率的掌握。
// 帧率监视
stat = new Stats();
stat.domElement.style.position = 'absolute';
stat.domElement.style.right = '0px';
stat.domElement.style.top = '0px';
document.body.appendChild(stat.domElement);
添加鼠标移动事件
camera.lookAt(new THREE.Vector3(0, 0, 0));
window.addEventListener('mousemove', onMouseMove, false);
/*鼠标指针指向响应*/
function onMouseMove(event) {
// calculate mouse position in normalized device coordinates
// (-1 to +1) for both components
mouse.x = event.clientX / window.innerWidth * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
添加背景星星
为了让夜空更好看,添加一些星星。
// 背景星星
const particles = 20000; // 数量
let bufferGeometry = new THREE.BufferGeometry();
let positions = new Float32Array(particles * 3);
let colors = new Float32Array(particles * 3);
let color = new THREE.Color();
const gap = 1000; // 星星最近出现位置
for(let i = 0; i < positions.length; i +=3) {
let x = (Math.random() * gap * 2) * (Math.random() < 0.5 ? -1 : 1);
let y = (Math.random() * gap * 2) * (Math.random() < 0.5 ? -1 : 1);
let z = (Math.random() * gap * 2) * (Math.random() < 0.5 ? -1 : 1);
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}
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 hasColor = Math.random() > 0.3;
let vs, 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;
}
bufferGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
bufferGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
bufferGeometry.computeBoundingSphere();
let material = new THREE.PointsMaterial( {size:6, vertexColors: THREE.vertexColors});
particleSystem = new THREE.Points( bufferGeometry, material);
scene.add(particleSystem);
显示行星名称
添加鼠标移动到行星显示名称的功能。
// 显示行星名称
function displayPlanetName() {
var loader = new THREE.FontLoader();
for(var i = 0; i < stars.length; i++) {
nameConstructor(stars[i].name, stars[i].volume);
}
nameConstructor('Sun', 12);
function nameConstructor(name, volume) {
loader.load('fonts/helvetiker_regular.typeface.json', function (font) {
let planetName = new THREE.Mesh(
new THREE.TextGeometry(
name, {
font: font,
size: 4,
height: 4
}
),
new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.DoubleSide})
);
planetName.volume = volume;
planetName.visible = false;
starNames[name] = planetName;
scene.add(planetName);
});
}
}
displayPlanetName();
动画
最后,使用requestAnimationFrame()实现动画。
// 更新物品需要重新渲染
renderer.render(scene, camera);
requestAnimationFrame( ()=>move());
// 运动
function move() {
for(var i = 0; i < stars.length; i++) {
moveEachStar(stars[i]);
}
// 太阳自转
sun.rotation.y = (sun.rotation.y == 2 * Math.PI ? 0.0008 * Math.PI : sun.rotation.y + 0.0008 * Math.PI);
control.update( clock.getDelta() ); // 鼠标控制视角
// 限制镜头
camera.position.x = THREE.Math.clamp(camera.position.x, -400, 400);
camera.position.y = THREE.Math.clamp(camera.position.y, -400, 400);
camera.position.z = THREE.Math.clamp(camera.position.z, -400, 400);
stat.update();
/*鼠标指向行星显示名字*/
raycaster.setFromCamera( mouse, camera );
// 交汇点对象
let intersects = raycaster.intersectObjects( scene.children );
if(intersects.length > 0) {
let obj = intersects[0].object;
let name = obj.name;
// 把上一个显示隐藏
displayName && (displayName.visible = false)
if(starNames[name]) {
starNames[name].visible = true;
displayName = starNames[name];
displayName.position.copy(obj.position);
displayName.geometry.center();
displayName.position.y = starNames[name].volume + 4;
displayName.lookAt(camera.position);
}
} else {
displayName && displayName.visible && (displayName.visible = false);
}
// 更新物品需要重新渲染
renderer.render(scene, camera);
requestAnimationFrame(()=> move());
}
网友评论