안녕하세요! 오늘은 Three.js를 이용하여 태양광 발전 시스템을 구축해 보겠습니다. 하지만 시작하기 전에, 이 기사의 영감은 제가 현재 진행 중인 프로젝트의 클라이언트 담당자에게서 나왔다는 점을 알아야 합니다. 네, 바로 당신입니다. 지구가 평평하다고 믿는 사람입니다.
JavaScript/Node에는 개발을 단순화하는 엄청난 양의 기능을 포함하는 가장 큰 라이브러리 생태계가 있으므로 항상 귀하의 목적에 더 적합한 것을 선택할 수 있습니다. 그러나 3D 그래픽에 관해 이야기하면 멋진 옵션이 그리 많지 않으며 three.js가 아마도 그 중에서 가장 좋고 가장 큰 커뮤니티를 가지고 있습니다.
이제 Three.js를 살펴보고 이를 사용하여 태양계를 구축해 보겠습니다. 이 기사에서 다룰 내용은 다음과 같습니다.
먼저 프로젝트를 초기화하기 위해 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; } }
Scene 클래스의 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에서 약 5분 동안 검색하면 장면에 행성을 추가하는 데 필요한 모든 값이 포함된 표를 찾을 수 있습니다.
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 중국어 웹사이트의 기타 관련 기사를 참조하세요!