共计 8388 个字符,预计需要花费 21 分钟才能阅读完成。
整个宇宙将为你闪动 除了三体人前端也能够 哈哈
整个宇宙将为你闪动,这句话出自《三体》,过后就被吸引住了,而后就想着把它实现进去。我的项目次要应用 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 官网示例,用于闪动的星星。