整个宇宙将为你闪动 除了三体人前端也能够 哈哈
整个宇宙将为你闪动,这句话出自《三体》,过后就被吸引住了,而后就想着把它实现进去。我的项目次要应用 three.js,设计灵感及贴图来源于 Solar System Scope 。
视频展现: Bilibili 哔哩哔哩
我的项目源码: GitHub
前置项
在开始之前心愿你对 three.js 有肯定的理解,这里举荐一个教程,Discover threejs, three.js 主创之一编写的适宜入门的教程。
我的项目只用原生 js 并没有引入 Vue 或 React 等框架,因为我想让我的项目更纯正些,也心愿本人不要来到框架就不会写代码了。
基础设施
- 创立场景
// scene.jsimport { Scene } from 'three'function createScene() { const scene = new Scene() return scene}export { createScene }
- 创立相机
// camera.jsimport { 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.jsimport { WebGLRenderer } from 'three'function createRenderer() { const renderer = new WebGLRenderer() return renderer}export { createRenderer }
增加物体到场景中
实现下面的基本操作,咱们就能够往场景中增加物体了。
银河系及行星
创立一个球体,并给球体的材质应用纹理贴图。别忘了增加到场景中。
// milkyWay.jsimport { 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.jsimport { 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.nameconst 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.jsgroup.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 fogfunction 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 官网示例,用于闪动的星星。