整个宇宙将为你闪烁 除了三体人前端也可以 哈哈
整个宇宙将为你闪烁,这句话出自《三体》,当时就被吸引住了,然后就想着把它实现出来。项目主要使用 three.js,设计灵感及贴图来源于 Solar System Scope 。
视频展示: Bilibili 哔哩哔哩
项目源码: GitHub
前置项
在开始之前希望你对 three.js 有一定的了解,这里推荐一个教程,Discover threejs, three.js 主创之一编写的适合入门的教程。
项目只用原生 js 并没有引入 Vue 或 React 等框架,因为我想让项目更纯粹些,也希望自己不要离开框架就不会写代码了。
基础设施
- 创建场景
// scene.js
import { Scene } from 'three'
function createScene() {
const scene = new Scene()
return scene
}
export { createScene }
- 创建相机
// camera.js
import { PerspectiveCamera } from 'three'
function createCamera() {
const camera = new PerspectiveCamera(45, 1, 0.1, 2000)
camera.position.set(0, 0, 20)
return camera
}
export { createCamera }
- 渲染场景
// renderer.js
import { WebGLRenderer } from 'three'
function createRenderer() {
const renderer = new WebGLRenderer()
return renderer
}
export { createRenderer }
添加物体到场景中
完成上面的基本操作,我们就可以往场景中添加物体了。
银河系及行星
创建一个球体,并给球体的材质使用纹理贴图。别忘了添加到场景中。
// milkyWay.js
import { SphereGeometry, Mesh, MeshStandardMaterial, TextureLoader, BackSide } from 'three'
import * as THREE from 'three'
function createMaterial() {
const textureLoader = new TextureLoader()
const texture = textureLoader.load('/assets/textures/8k_stars_milky_way.jpg')
const material = new MeshStandardMaterial({ map: texture, side: THREE.DoubleSide })
return material
}
function createMilkyWay() {
const geometry = new SphereGeometry(1000)
const material = createMaterial()
const milkyWay = new Mesh(geometry, material)
return milkyWay
}
export { createMilkyWay }
行星的创建与银河系类似,因为都是创建球体。
行星的轨迹
这里主要使用了 CatmullRomCurve3 创建一条平滑的三维样条曲线。
// orbit.js
import { CatmullRomCurve3, BufferGeometry, LineBasicMaterial, LineLoop } from 'three'
function createOrbit(sphereVal) {
let sphererObitalInclination = sphereVal.orbitalInclination || 0
let sphereOrbit = sphereVal.orbit || 0
let x = Math.cos((Math.PI / 180) * sphererObitalInclination) * sphereOrbit
let y = Math.sin((Math.PI / 180) * sphererObitalInclination) * sphereOrbit
const initialPoints = [
{
x: x,
y: y,
z: 0
},
{ x: 0, y: 0, z: -sphereOrbit },
{
x: -x,
y: -y,
z: 0
},
{ x: 0, y: 0, z: sphereOrbit }
]
const curve = new CatmullRomCurve3(initialPoints)
curve.curveType = 'catmullrom'
curve.tension = 0.84
curve.closed = true
const points = curve.getPoints(50)
const geometry = new BufferGeometry().setFromPoints(points)
const material = new LineBasicMaterial({ color: 0x718799, transparent: true, opacity: 0.8 })
const orbit = new LineLoop(geometry, material)
if (sphereVal.id === 'moon') {
orbit.position.x = 10
}
return { curve, orbit }
}
export { createOrbit }
创建圆环还有更简单的方法,就是直接使用 EllipseCurve 创建一个曲线,再将其进行旋转,也就是修改 orbit 的 rotation。 但是最后在行星的运动环节,使用 getPointAt 获取不到准确的值,只能拿到旋转前的值。
import { EllipseCurve, BufferGeometry, LineBasicMaterial, Line } from 'three'
function createOrbit(sphereVal) {
const curve = new EllipseCurve(0, 0, sphereVal.orbit, sphereVal.orbit, 0, 2 * Math.PI, false, 0)
const points = curve.getPoints(100)
const geometry = new BufferGeometry().setFromPoints(points)
const material = new LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.8 })
const orbit = new Line(geometry, material)
orbit.rotation.x = Math.PI / 2
orbit.rotation.y = (sphereVal.orbitalInclination * Math.PI) / 180
return orbit
}
export { createOrbit }
太阳的发光效果
使用 EffectComposer 实现,具体的例子可以参考 three.js 官方示例 Bloom,用于太阳的辉光;
three.js 官方示例 Outline,用于太阳的边缘。
import { Vector2, ShaderMaterial } from 'three'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js'
function createEffectComposer(scene, camera, renderer) {
const renderScene = new RenderPass(scene, camera)
const bloomPass = new UnrealBloomPass(new Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85)
const outlinePass = new OutlinePass(new Vector2(window.innerWidth, window.innerHeight), scene, camera)
bloomPass.threshold = 0.2
bloomPass.strength = 0.2
bloomPass.radius = 0
const bloomComposer = new EffectComposer(renderer)
bloomComposer.renderToScreen = false
bloomComposer.addPass(renderScene)
bloomComposer.addPass(bloomPass)
bloomComposer.addPass(outlinePass)
outlinePass.edgeStrength = 10
outlinePass.edgeGlow = 0.5
outlinePass.edgeThickness = 8
outlinePass.visibleEdgeColor.set(0xffc607)
outlinePass.hiddenEdgeColor.set(0x000)
const finalPass = new ShaderPass(
new ShaderMaterial({
uniforms: {
baseTexture: { value: null },
bloomTexture: { value: bloomComposer.renderTarget2.texture }
},
vertexShader: document.getElementById('vertexshader').textContent,
fragmentShader: document.getElementById('fragmentshader').textContent,
defines: {}
}),
'baseTexture'
)
finalPass.needsSwap = true
const finalComposer = new EffectComposer(renderer)
finalComposer.addPass(renderScene)
finalComposer.addPass(finalPass)
return { bloomPass, bloomComposer, finalComposer, outlinePass }
}
export { createEffectComposer }
美颜前 | 美颜后 |
![]() |
![]() |
行星的标签
使用 canvas 创建一张图片,再赋值给 Sprite 的材质,这个方式是参考了网上的一个例子。
import { Sprite, SpriteMaterial, TextureLoader } from 'three'
function createSprite(sphereVal) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = 48
canvas.height = 30
ctx.fillStyle = '#fff'
ctx.font = 'normal 12pt 黑体'
ctx.textAlign = 'center'
ctx.fillText(sphereVal.name, 24, 25)
let url = canvas.toDataURL('image/png')
const spriteMaterial = new SpriteMaterial({
map: new TextureLoader().load(url),
sizeAttenuation: false
})
const sprite = new Sprite(spriteMaterial)
sprite.scale.set(0.05, 0.03)
return sprite
}
export { createSprite }
除此以外还有另外一种实现的方式,可以参考 three.js 官方示例 Label。
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'
// 用 CSS2DObject 创建标签
const div = document.createElement('div')
div.className = 'label'
div.textContent = sphereVal.name
const label = new CSS2DObject(div)
label.position.set(1.5 * sphereVal.radius, 0, 0)
label.center.set(0, 1)
sphere.add(label)
label.layers.set(0)
group.add(sprite)
这样看起来更简单一些,但是,那个标签的层级会比较高,我的行星根本没有办法挡住它!
行星的运动
这里逻辑就是获取轨迹线 curve 的点,将其所在位置赋值给行星组 group,因为月亮比较特殊所以要单独设置,这样就完成了公转。那么自转就更容易了只需要改变 group 的 rotation 就好了。
其实我觉得月亮和地球之间的处理方式有待优化,但我已经尽力了。
// sphere.js
group.tick = (delta) => {
if (sphereVal.orbitalPeriod) {
const point = curve.getPointAt(progress)
const pointBox = curve.getPointAt(progress + sphereVal.orbitalPeriod)
if (sphereVal.id === 'earth') {
earth = point
}
if (sphereVal.id === 'moon') {
orbit.position.set(earth.x, earth.y, earth.z)
group.position.set(earth.x + point.x, earth.y + point.y, earth.z + point.z)
} else {
group.position.set(point.x, point.y, point.z)
group.lookAt(pointBox.x, pointBox.y, pointBox.z)
}
sprite.position.set(group.position.x, group.position.y - (sphereVal.radius + 0.2), group.position.z)
progress = progress >= 1 - sphereVal.orbitalPeriod * 2 ? 0 : (progress += sphereVal.orbitalPeriod)
}
if (sphereVal.rotationPeriod) {
group.rotation.y += sphereVal.rotationPeriod
}
}
至此,就可以得到这样的场景。
✨ 星光闪闪
没有一个例子能逃过我的手掌心,three.js 官方示例 Points, 用于闪烁的星星。
import {
Vector3,
Color,
BufferGeometry,
BufferAttribute,
ShaderMaterial,
Points,
TextureLoader,
AdditiveBlending
} from 'three'
function createStar(sphereVal, type) {
const amount = 5000
const radius = 400
const positions = new Float32Array(amount * 3)
const colors = new Float32Array(amount * 3)
const sizes = new Float32Array(amount)
const vertex = new Vector3()
const color = new Color(0xffffff)
for (let i = 0; i < amount; i++) {
vertex.x = (Math.random() * 2 - 1) * radius
vertex.y = (Math.random() * 2 - 1) * radius
vertex.z = (Math.random() * 2 - 1) * radius
vertex.toArray(positions, i * 3)
color.setHSL(0.0 + 0.1 * (i / amount), 0.98, 0.38)
color.toArray(colors, i * 3)
sizes[i] = 1
}
const geometry = new BufferGeometry()
geometry.setAttribute('position', new BufferAttribute(positions, 3))
geometry.setAttribute('customColor', new BufferAttribute(colors, 3))
geometry.setAttribute('size', new BufferAttribute(sizes, 1))
const material = new ShaderMaterial({
uniforms: {
color: { value: new Color(0xffffff) },
pointTexture: { value: new TextureLoader().load('assets/textures/spark1.png') }
},
vertexShader: document.getElementById('pointVertexshader').textContent,
fragmentShader: document.getElementById('pointFragmentshader').textContent,
blending: AdditiveBlending,
depthTest: false,
transparent: true
})
const stars = new Points(geometry, material)
const starsGeometry = stars.geometry
const attributes = starsGeometry.attributes
stars.tick = (delta) => {
const time = Date.now() * 0.005
for (let i = 0; i < attributes.size.array.length; i++) {
attributes.size.array[i] = 1 + 10 * Math.sin(1 * i + time)
}
attributes.size.needsUpdate = true
}
return stars
}
export { createStar }
宇宙闪烁
这里是往场景中添加红色的雾,但是 fog 并没有继承 Object3D 的 visible 属性,所以修改 density 的值去实现忽明忽暗的效果。我真是个平平无奇的大聪明,哈哈。
import { FogExp2 } from 'three'
let fog
function createFog(scene) {
fog = fog ? fog : new FogExp2(0xbd0302, 0.00025)
scene.fog = fog
fog.tick = (delta) => {
const time = Date.now() * 0.005
scene.fog && (scene.fog.density = Math.sin(time) * 0.001 + 0.0005)
}
return fog
}
export { createFog }
结语
至此,你学废了吗?感兴趣的朋友可以看下 GitHub 上的源码。
参考资料
- Discover threejs, three.js 主创之一编写的适合入门的教程。
- Bloom, three.js 官方示例, 用于太阳发光的效果。
- Outline, three.js 官方示例,用于太阳的边缘。
- Sprite, three.js 官方示例,用于星体的标签
- Points, three.js 官方示例,用于闪烁的星星。