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 上搜尋大約五分鐘,您將看到一個表格,其中包含將行星加入到場景中所需的所有值。
星球 | 尺寸(直徑) | 轉速 | 旋轉方向 | 軌道速度 |
---|---|---|---|---|
水星 | 4,880 公里 | 10.83 公里/小時 | 逆時針 | 47.87 公里/秒 |
金星 | 12,104 公里 | 6.52 公里/小時 | 順時針 | 35.02 公里/秒 |
地球 | 12,742 公里 | 1674.4 公里/小時 | 逆時針 | 29.78 公里/秒 |
火星 | 6,779 公里 | 866.5 公里/小時 | 逆時針 | 24.07 公里/秒 |
木星 | 142,984 公里 | 45,300 公里/小時 | 逆時針 | 13.07 公里/秒 |
土星 | 120,536 公里 | 35,500 公里/小時 | 逆時針 | 9.69 公里/秒 |
天王星 | 51,118 公里 | 9,320 公里/小時 | 順時針 | 6.81公里/秒 |
海王星 | 49,528 公里 | 9,720 公里/小時 | 逆時針 | 5.43 公里/秒 |
現在,所有行星和太陽都可以加入到場景中。
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);
結果,所有太陽係都會看起來像這樣:
用於部署以在vite.config.js
中設定正確的基礎。
如果您要部署至https://<USERNAME>.github.io/
,或透過 GitHub Pages 部署至自訂網域,請將 base 設定為'/'
。或者,您可以從配置中刪除 base ,因為它預設為'/'
。
如果您要部署至https://<USERNAME>.github.io/<REPO>/
(例如,您的儲存庫位於https://github.com/<USERNAME>/<REPO>
),則將base 設定為'/<REPO>/'
。
前往儲存庫設定頁面中的 GitHub Pages 配置,然後選擇部署來源作為“GitHub Actions”,這將引導您建立建置和部署專案的工作流程,提供了使用 npm 安裝依賴項和建置的範例工作流程:
# 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
就是這樣。如果您的部署尚未自動啟動,您始終可以在儲存庫的「操作」標籤中手動啟動它。已部署專案的連結可以在下面找到。
今天就這樣!您可以在下面找到整個專案的連結。我希望你覺得這很有趣,並且不要仍然相信地球是平的。
再見!
原文出處:https://dev.to/cookiemonsterdev/solar-system-with-threejs-3fe0