整个宇宙将为你闪动 除了三体人前端也能够 哈哈

整个宇宙将为你闪动,这句话出自《三体》,过后就被吸引住了,而后就想着把它实现进去。我的项目次要应用 three.js,设计灵感及贴图来源于 Solar System Scope 。

视频展现: Bilibili 哔哩哔哩

我的项目源码: GitHub

前置项

在开始之前心愿你对 three.js 有肯定的理解,这里举荐一个教程,Discover threejs, three.js 主创之一编写的适宜入门的教程。

我的项目只用原生 js 并没有引入 Vue 或 React 等框架,因为我想让我的项目更纯正些,也心愿本人不要来到框架就不会写代码了。

基础设施

  1. 创立场景
// scene.jsimport { Scene } from 'three'function createScene() {    const scene = new Scene()    return scene}export { createScene }
  1. 创立相机
// 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 }
  1. 渲染场景
// 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 上的源码。

参考资料

  1. Discover threejs, three.js 主创之一编写的适宜入门的教程。
  2. Bloom, three.js 官网示例, 用于太阳发光的成果。
  3. Outline, three.js 官网示例,用于太阳的边缘。
  4. Sprite, three.js 官网示例,用于星体的标签
  5. Points, three.js 官网示例,用于闪动的星星。