嗨!今天,我將使用 Three.js 建造一個太陽能係統。但在我們開始之前,您應該知道本文的靈感來自於我目前正在從事的專案的客戶代表。是的,那就是你──相信地球是平的。
JavaScript/Node 擁有最大的庫生態系統,涵蓋大量可簡化開發的功能,因此我始終可以選擇哪個更適合您的目的。然而,如果我們談論 3D 圖形,沒有那麼多酷的選擇,而 Three.js 可能是其中最好的,並且擁有最大的社區。
所以讓我們深入研究 Three.js 並使用它來建構 Solar 系統。在這篇文章中我將介紹:
首先要做的事情:為了初始化項目,我使用 Vite 並安裝 Three.js 依賴項。現在的問題是如何設定 Three.js。為此,您需要三樣東西:場景、相機和渲染器。我還使用內建插件 OrbitControls,它允許我在場景中導航。啟動應用程式後,應該會出現黑屏。
import { Scene, WebGLRenderer, PerspectiveCamera } from "three"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; const w = window.innerWidth; const h = window.innerHeight; const scene = new Scene(); const camera = new PerspectiveCamera(75, w / h, 0.1, 100); const renderer = new WebGLRenderer(); const controls = new OrbitControls(camera, renderer.domElement); controls.minDistance = 10; controls.maxDistance = 60; camera.position.set(30 * Math.cos(Math.PI / 6), 30 * Math.sin(Math.PI / 6), 40); renderer.setSize(w, h); document.body.appendChild(renderer.domElement); renderer.render(scene, camera); window.addEventListener("resize", () => { const w = window.innerWidth; const h = window.innerHeight; renderer.setSize(w, h); camera.aspect = w / h; camera.updateProjectionMatrix(); }); const animate = () => { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); }; animate();
您可能會注意到,我透過控制限制了縮放,並且還更改了相機的預設角度。這將有助於後續步驟中正確顯示場景。
現在是時候添加一個簡單的星空了,因為我們的太陽系應該被恆星包圍。為了簡化說明,假設您有一個球體,並且在該球體上隨機選取 1,000 個點。然後,透過將星形紋理映射到這些點上來創建星形。最後,我添加動畫以使所有這些點都繞著 y 軸旋轉。這樣,星空就可以加入場景了。
import { Group, Color, Points, Vector3, TextureLoader, PointsMaterial, BufferGeometry, AdditiveBlending, Float32BufferAttribute, } from "three"; export class Starfield { group; loader; animate; constructor({ numStars = 1000 } = {}) { this.numStars = numStars; this.group = new Group(); this.loader = new TextureLoader(); this.createStarfield(); this.animate = this.createAnimateFunction(); this.animate(); } createStarfield() { let col; const verts = []; const colors = []; const positions = []; for (let i = 0; i < this.numStars; i += 1) { let p = this.getRandomSpherePoint(); const { pos, hue } = p; positions.push(p); col = new Color().setHSL(hue, 0.2, Math.random()); verts.push(pos.x, pos.y, pos.z); colors.push(col.r, col.g, col.b); } const geo = new BufferGeometry(); geo.setAttribute("position", new Float32BufferAttribute(verts, 3)); geo.setAttribute("color", new Float32BufferAttribute(colors, 3)); const mat = new PointsMaterial({ size: 0.2, alphaTest: 0.5, transparent: true, vertexColors: true, blending: AdditiveBlending, map: this.loader.load("/solar-system-threejs/assets/circle.png"), }); const points = new Points(geo, mat); this.group.add(points); } getRandomSpherePoint() { const radius = Math.random() * 25 + 25; const u = Math.random(); const v = Math.random(); const theta = 2 * Math.PI * u; const phi = Math.acos(2 * v - 1); let x = radius * Math.sin(phi) * Math.cos(theta); let y = radius * Math.sin(phi) * Math.sin(theta); let z = radius * Math.cos(phi); return { pos: new Vector3(x, y, z), hue: 0.6, minDist: radius, }; } createAnimateFunction() { return () => { requestAnimationFrame(this.animate); this.group.rotation.y += 0.00005; }; } getStarfield() { return this.group; } }
新增星空很簡單,只要使用場景類別中的 add 方法即可
const starfield = new Starfield().getStarfield(); scene.add(starfield);
至於紋理,您可以在存儲庫中找到此項目中使用的所有紋理,該存儲庫鏈接在文章末尾。大多數紋理均取自此站點,但恆星和行星環紋理除外。
對於太陽,我使用了二十面體幾何體並在其上映射了紋理。使用改進的噪聲,我實現了太陽脈衝的效果,模擬真實恆星向太空發射能量流的方式。太陽不僅僅是一個帶有映射紋理的圖形;它也是一個圖形。它還需要成為場景中的光源,所以我使用 PointLight 來模擬它。
import { Mesh, Group, Color, Vector3, BackSide, PointLight, TextureLoader, ShaderMaterial, AdditiveBlending, DynamicDrawUsage, MeshBasicMaterial, IcosahedronGeometry, } from "three"; import { ImprovedNoise } from "three/addons/math/ImprovedNoise.js"; export class Sun { group; loader; animate; corona; sunRim; glow; constructor() { this.sunTexture = "/solar-system-threejs/assets/sun-map.jpg"; this.group = new Group(); this.loader = new TextureLoader(); this.createCorona(); this.createRim(); this.addLighting(); this.createGlow(); this.createSun(); this.animate = this.createAnimateFunction(); this.animate(); } createSun() { const map = this.loader.load(this.sunTexture); const sunGeometry = new IcosahedronGeometry(5, 12); const sunMaterial = new MeshBasicMaterial({ map, emissive: new Color(0xffff99), emissiveIntensity: 1.5, }); const sunMesh = new Mesh(sunGeometry, sunMaterial); this.group.add(sunMesh); this.group.add(this.sunRim); this.group.add(this.corona); this.group.add(this.glow); this.group.userData.update = (t) => { this.group.rotation.y = -t / 5; this.corona.userData.update(t); }; } createCorona() { const coronaGeometry = new IcosahedronGeometry(4.9, 12); const coronaMaterial = new MeshBasicMaterial({ color: 0xff0000, side: BackSide, }); const coronaMesh = new Mesh(coronaGeometry, coronaMaterial); const coronaNoise = new ImprovedNoise(); let v3 = new Vector3(); let p = new Vector3(); let pos = coronaGeometry.attributes.position; pos.usage = DynamicDrawUsage; const len = pos.count; const update = (t) => { for (let i = 0; i < len; i += 1) { p.fromBufferAttribute(pos, i).normalize(); v3.copy(p).multiplyScalar(5); let ns = coronaNoise.noise( v3.x + Math.cos(t), v3.y + Math.sin(t), v3.z + t ); v3.copy(p) .setLength(5) .addScaledVector(p, ns * 0.4); pos.setXYZ(i, v3.x, v3.y, v3.z); } pos.needsUpdate = true; }; coronaMesh.userData.update = update; this.corona = coronaMesh; } createGlow() { const uniforms = { color1: { value: new Color(0x000000) }, color2: { value: new Color(0xff0000) }, fresnelBias: { value: 0.2 }, fresnelScale: { value: 1.5 }, fresnelPower: { value: 4.0 }, }; const vertexShader = ` uniform float fresnelBias; uniform float fresnelScale; uniform float fresnelPower; varying float vReflectionFactor; void main() { vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); vec4 worldPosition = modelMatrix * vec4( position, 1.0 ); vec3 worldNormal = normalize( mat3( modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz ) * normal ); vec3 I = worldPosition.xyz - cameraPosition; vReflectionFactor = fresnelBias + fresnelScale * pow( 1.0 + dot( normalize( I ), worldNormal ), fresnelPower ); gl_Position = projectionMatrix * mvPosition; } `; const fragmentShader = ` uniform vec3 color1; uniform vec3 color2; varying float vReflectionFactor; void main() { float f = clamp( vReflectionFactor, 0.0, 1.0 ); gl_FragColor = vec4(mix(color2, color1, vec3(f)), f); } `; const sunGlowMaterial = new ShaderMaterial({ uniforms, vertexShader, fragmentShader, transparent: true, blending: AdditiveBlending, }); const sunGlowGeometry = new IcosahedronGeometry(5, 12); const sunGlowMesh = new Mesh(sunGlowGeometry, sunGlowMaterial); sunGlowMesh.scale.setScalar(1.1); this.glow = sunGlowMesh; } createRim() { const uniforms = { color1: { value: new Color(0xffff99) }, color2: { value: new Color(0x000000) }, fresnelBias: { value: 0.2 }, fresnelScale: { value: 1.5 }, fresnelPower: { value: 4.0 }, }; const vertexShader = ` uniform float fresnelBias; uniform float fresnelScale; uniform float fresnelPower; varying float vReflectionFactor; void main() { vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); vec4 worldPosition = modelMatrix * vec4( position, 1.0 ); vec3 worldNormal = normalize( mat3( modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz ) * normal ); vec3 I = worldPosition.xyz - cameraPosition; vReflectionFactor = fresnelBias + fresnelScale * pow( 1.0 + dot( normalize( I ), worldNormal ), fresnelPower ); gl_Position = projectionMatrix * mvPosition; } `; const fragmentShader = ` uniform vec3 color1; uniform vec3 color2; varying float vReflectionFactor; void main() { float f = clamp( vReflectionFactor, 0.0, 1.0 ); gl_FragColor = vec4(mix(color2, color1, vec3(f)), f); } `; const sunRimMaterial = new ShaderMaterial({ uniforms, vertexShader, fragmentShader, transparent: true, blending: AdditiveBlending, }); const sunRimGeometry = new IcosahedronGeometry(5, 12); const sunRimMesh = new Mesh(sunRimGeometry, sunRimMaterial); sunRimMesh.scale.setScalar(1.01); this.sunRim = sunRimMesh; } addLighting() { const sunLight = new PointLight(0xffff99, 1000); sunLight.position.set(0, 0, 0); this.group.add(sunLight); } createAnimateFunction() { return (t = 0) => { const time = t * 0.00051; requestAnimationFrame(this.animate); this.group.userData.update(time); }; } getSun() { return this.group; } }
所有行星都是使用類似的邏輯建構的:每個行星都需要一個軌道、紋理、軌道速度和自轉速度。對於需要它們的行星,也應該添加環。
import { Mesh, Color, Group, DoubleSide, RingGeometry, TorusGeometry, TextureLoader, ShaderMaterial, SRGBColorSpace, AdditiveBlending, MeshPhongMaterial, MeshBasicMaterial, IcosahedronGeometry, } from "three"; export class Planet { group; loader; animate; planetGroup; planetGeometry; constructor({ orbitSpeed = 1, orbitRadius = 1, orbitRotationDirection = "clockwise", planetSize = 1, planetAngle = 0, planetRotationSpeed = 1, planetRotationDirection = "clockwise", planetTexture = "/solar-system-threejs/assets/mercury-map.jpg", rimHex = 0x0088ff, facingHex = 0x000000, rings = null, } = {}) { this.orbitSpeed = orbitSpeed; this.orbitRadius = orbitRadius; this.orbitRotationDirection = orbitRotationDirection; this.planetSize = planetSize; this.planetAngle = planetAngle; this.planetTexture = planetTexture; this.planetRotationSpeed = planetRotationSpeed; this.planetRotationDirection = planetRotationDirection; this.rings = rings; this.group = new Group(); this.planetGroup = new Group(); this.loader = new TextureLoader(); this.planetGeometry = new IcosahedronGeometry(this.planetSize, 12); this.createOrbit(); this.createRings(); this.createPlanet(); this.createGlow(rimHex, facingHex); this.animate = this.createAnimateFunction(); this.animate(); } createOrbit() { const orbitGeometry = new TorusGeometry(this.orbitRadius, 0.01, 100); const orbitMaterial = new MeshBasicMaterial({ color: 0xadd8e6, side: DoubleSide, }); const orbitMesh = new Mesh(orbitGeometry, orbitMaterial); orbitMesh.rotation.x = Math.PI / 2; this.group.add(orbitMesh); } createPlanet() { const map = this.loader.load(this.planetTexture); const planetMaterial = new MeshPhongMaterial({ map }); planetMaterial.map.colorSpace = SRGBColorSpace; const planetMesh = new Mesh(this.planetGeometry, planetMaterial); this.planetGroup.add(planetMesh); this.planetGroup.position.x = this.orbitRadius - this.planetSize / 9; this.planetGroup.rotation.z = this.planetAngle; this.group.add(this.planetGroup); } createGlow(rimHex, facingHex) { const uniforms = { color1: { value: new Color(rimHex) }, color2: { value: new Color(facingHex) }, fresnelBias: { value: 0.2 }, fresnelScale: { value: 1.5 }, fresnelPower: { value: 4.0 }, }; const vertexShader = ` uniform float fresnelBias; uniform float fresnelScale; uniform float fresnelPower; varying float vReflectionFactor; void main() { vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); vec4 worldPosition = modelMatrix * vec4( position, 1.0 ); vec3 worldNormal = normalize( mat3( modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz ) * normal ); vec3 I = worldPosition.xyz - cameraPosition; vReflectionFactor = fresnelBias + fresnelScale * pow( 1.0 + dot( normalize( I ), worldNormal ), fresnelPower ); gl_Position = projectionMatrix * mvPosition; } `; const fragmentShader = ` uniform vec3 color1; uniform vec3 color2; varying float vReflectionFactor; void main() { float f = clamp( vReflectionFactor, 0.0, 1.0 ); gl_FragColor = vec4(mix(color2, color1, vec3(f)), f); } `; const planetGlowMaterial = new ShaderMaterial({ uniforms, vertexShader, fragmentShader, transparent: true, blending: AdditiveBlending, }); const planetGlowMesh = new Mesh(this.planetGeometry, planetGlowMaterial); planetGlowMesh.scale.setScalar(1.1); this.planetGroup.add(planetGlowMesh); } createRings() { if (!this.rings) return; const innerRadius = this.planetSize + 0.1; const outerRadius = innerRadius + this.rings.ringsSize; const ringsGeometry = new RingGeometry(innerRadius, outerRadius, 32); const ringsMaterial = new MeshBasicMaterial({ side: DoubleSide, transparent: true, map: this.loader.load(this.rings.ringsTexture), }); const ringMeshs = new Mesh(ringsGeometry, ringsMaterial); ringMeshs.rotation.x = Math.PI / 2; this.planetGroup.add(ringMeshs); } createAnimateFunction() { return () => { requestAnimationFrame(this.animate); this.updateOrbitRotation(); this.updatePlanetRotation(); }; } updateOrbitRotation() { if (this.orbitRotationDirection === "clockwise") { this.group.rotation.y -= this.orbitSpeed; } else if (this.orbitRotationDirection === "counterclockwise") { this.group.rotation.y += this.orbitSpeed; } } updatePlanetRotation() { if (this.planetRotationDirection === "clockwise") { this.planetGroup.rotation.y -= this.planetRotationSpeed; } else if (this.planetRotationDirection === "counterclockwise") { this.planetGroup.rotation.y += this.planetRotationSpeed; } } getPlanet() { return this.group; } }
對於地球,我擴展了 Planet 類別以添加額外的紋理,例如雲和行星夜面的夜間紋理。
import { Mesh, AdditiveBlending, MeshBasicMaterial, MeshStandardMaterial, } from "three"; import { Planet } from "./planet"; export class Earth extends Planet { constructor(props) { super(props); this.createPlanetLights(); this.createPlanetClouds(); } createPlanetLights() { const planetLightsMaterial = new MeshBasicMaterial({ map: this.loader.load("/solar-system-threejs/assets/earth-map-2.jpg"), blending: AdditiveBlending, }); const planetLightsMesh = new Mesh( this.planetGeometry, planetLightsMaterial ); this.planetGroup.add(planetLightsMesh); this.group.add(this.planetGroup); } createPlanetClouds() { const planetCloudsMaterial = new MeshStandardMaterial({ map: this.loader.load("/solar-system-threejs/assets/earth-map-3.jpg"), transparent: true, opacity: 0.8, blending: AdditiveBlending, alphaMap: this.loader.load( "/solar-system-threejs/assets/earth-map-4.jpg" ), }); const planetCloudsMesh = new Mesh( this.planetGeometry, planetCloudsMaterial ); planetCloudsMesh.scale.setScalar(1.003); this.planetGroup.add(planetCloudsMesh); this.group.add(this.planetGroup); } }
透過在 Google 上搜尋大約五分鐘,您將看到一個表格,其中包含將行星添加到場景中所需的所有值。
Planet | Size (diameter) | Rotation speed | Rotation direction | Orbit speed |
---|---|---|---|---|
Mercury | 4,880 km | 10.83 km/h | Counterclockwise | 47.87 km/s |
Venus | 12,104 km | 6.52 km/h | Clockwise | 35.02 km/s |
Earth | 12,742 km | 1674.4 km/h | Counterclockwise | 29.78 km/s |
Mars | 6,779 km | 866.5 km/h | Counterclockwise | 24.07 km/s |
Jupiter | 142,984 km | 45,300 km/h | Counterclockwise | 13.07 km/s |
Saturn | 120,536 km | 35,500 km/h | Counterclockwise | 9.69 km/s |
Uranus | 51,118 km | 9,320 km/h | Clockwise | 6.81 km/s |
Neptune | 49,528 km | 9,720 km/h | Counterclockwise | 5.43 km/s |
Now, all the planets and the sun can be added to the scene.
const planets = [ { orbitSpeed: 0.00048, orbitRadius: 10, orbitRotationDirection: "clockwise", planetSize: 0.2, planetRotationSpeed: 0.005, planetRotationDirection: "counterclockwise", planetTexture: "/solar-system-threejs/assets/mercury-map.jpg", rimHex: 0xf9cf9f, }, { orbitSpeed: 0.00035, orbitRadius: 13, orbitRotationDirection: "clockwise", planetSize: 0.5, planetRotationSpeed: 0.0005, planetRotationDirection: "clockwise", planetTexture: "/solar-system-threejs/assets/venus-map.jpg", rimHex: 0xb66f1f, }, { orbitSpeed: 0.00024, orbitRadius: 19, orbitRotationDirection: "clockwise", planetSize: 0.3, planetRotationSpeed: 0.01, planetRotationDirection: "counterclockwise", planetTexture: "/solar-system-threejs/assets/mars-map.jpg", rimHex: 0xbc6434, }, { orbitSpeed: 0.00013, orbitRadius: 22, orbitRotationDirection: "clockwise", planetSize: 1, planetRotationSpeed: 0.06, planetRotationDirection: "counterclockwise", planetTexture: "/solar-system-threejs/assets/jupiter-map.jpg", rimHex: 0xf3d6b6, }, { orbitSpeed: 0.0001, orbitRadius: 25, orbitRotationDirection: "clockwise", planetSize: 0.8, planetRotationSpeed: 0.05, planetRotationDirection: "counterclockwise", planetTexture: "/solar-system-threejs/assets/saturn-map.jpg", rimHex: 0xd6b892, rings: { ringsSize: 0.5, ringsTexture: "/solar-system-threejs/assets/saturn-rings.jpg", }, }, { orbitSpeed: 0.00007, orbitRadius: 28, orbitRotationDirection: "clockwise", planetSize: 0.5, planetRotationSpeed: 0.02, planetRotationDirection: "clockwise", planetTexture: "/solar-system-threejs/assets/uranus-map.jpg", rimHex: 0x9ab6c2, rings: { ringsSize: 0.4, ringsTexture: "/solar-system-threejs/assets/uranus-rings.jpg", }, }, { orbitSpeed: 0.000054, orbitRadius: 31, orbitRotationDirection: "clockwise", planetSize: 0.5, planetRotationSpeed: 0.02, planetRotationDirection: "counterclockwise", planetTexture: "/solar-system-threejs/assets/neptune-map.jpg", rimHex: 0x5c7ed7, }, ]; planets.forEach((item) => { const planet = new Planet(item).getPlanet(); scene.add(planet); }); const earth = new Earth({ orbitSpeed: 0.00029, orbitRadius: 16, orbitRotationDirection: "clockwise", planetSize: 0.5, planetAngle: (-23.4 * Math.PI) / 180, planetRotationSpeed: 0.01, planetRotationDirection: "counterclockwise", planetTexture: "/solar-system-threejs/assets/earth-map-1.jpg", }).getPlanet(); scene.add(earth);
In result all solar system will look sth like:
For deploying to set the correct base in vite.config.js.
If you are deploying to https://
If you are deploying to https://
Go to your GitHub Pages configuration in the repository settings page and choose the source of deployment as "GitHub Actions", this will lead you to create a workflow that builds and deploys your project, a sample workflow that installs dependencies and builds using npm is provided:
# Simple workflow for deploying static content to GitHub Pages name: Deploy static content to Pages on: # Runs on pushes targeting the default branch push: branches: ['main'] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write # Allow one concurrent deployment concurrency: group: 'pages' cancel-in-progress: true jobs: # Single deploy job since we're just deploying deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Node uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Setup Pages uses: actions/configure-pages@v4 - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: # Upload dist folder path: './dist' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4
That is it. If your deployment has not started automatically you can always start it manually in Actions tab in your repo. Link with deployed project can be found below.
That’s it for today! You can find the link to the entire project below. I hope you found this entertaining and don’t still believe the Earth is flat.
See ya!
Repository link
Deployment link
以上是使用 Three.js 的太陽能係統的詳細內容。更多資訊請關注PHP中文網其他相關文章!