-
這篇筆記的最終實現(xiàn)目標喷兼,是創(chuàng)建一個星系酒请,如下圖:
galaxy.png
-
Set up
<script setup>
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import * as dat from 'dat.gui'
/**
* Scene
*/
const scene = new THREE.Scene()
/**
* Camera
*/
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
100
)
camera.position.set(3, 3, 3)
/**
* Renderer
*/
const renderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement) // 在body上添加渲染器,domElement指向canvas
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})
/**
* 坐標軸
*/
const axesHelper = new THREE.AxesHelper(5) // 坐標軸線段長度
scene.add(axesHelper)
/**
* 控制器
*/
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
/**
* animate
*/
function animate () {
controls.update()
requestAnimationFrame(animate)
renderer.render(scene, camera)
}
animate()
/**
* gui
*/
const gui = new dat.GUI()
</script>
setup.png
-
Create a
generateGalaxy
function and call it, 我們用這個方法生成一個默認參數(shù)的星系
/**
* Camera
*/
...
...
/**
* Galaxy
*/
const generateGalaxy = () => {}
generateGalaxy()
-
Create a
parameters
object that will contain all the parameters of our galaxy
/**
* Galaxy
*/
const parameters = {}
const generateGalaxy = () => {}
generateGalaxy()
-
Create random particles based on
count
parameter
const parameters = {
count: 10000, // particles的數(shù)量
}
const generateGalaxy = () => {
/**
* geometry
*/
const geometry = new THREE.BufferGeometry()
// 創(chuàng)建一個空數(shù)組,長度為 count*3逆济,每個點都有 x、y磺箕、z 3個坐標值
const positions = new Float32Array(parameters.count * 3)
for(let i = 0; i < parameters.count; i++) {
const i3 = i * 3 // 0 3 6 9 12 ... 數(shù)組中的每3個值為一組(即一個頂點的坐標)
// 填充positions數(shù)組
positions[i3] = Math.random()
positions[i3 + 1] = Math.random()
positions[i3 + 2] = Math.random()
}
generateGalaxy()
-
Create the
PointMaterial
class and add asize
parameter
const parameters = {
count: 10000, // particles的數(shù)量
size: 0.02,
}
const generateGalaxy = () => {
/**
* geometry
*/
...
/**
* material
*/
const material = new THREE.PointsMaterial({
size: parameters.size,
sizeAttenuation: true, // 使用PerspectiveCamera時奖慌,particle的size隨相機深度衰減
depthWrite: false,
blending: THREE.AdditiveBlending
})
}
generateGalaxy()
-
Create the
Points
const generateGalaxy = () => {
/**
* geometry
*/
...
/**
* material
*/
...
/**
* points
*/
const points = new THREE.Points(geometry, material)
scene.add(points)
}
generateGalaxy()
points.png
- 從上面的圖中可以看出,我們這個粒子堆沒有居中(因為使用的
Math.random()
, 坐標范圍被限制在 [0, 1])松靡,并且比較集中不夠分散简僧,那么修改一下positions
的設置
const generateGalaxy = () => {
/**
* geometry
*/
...
for(let i = 0; i < parameters.count; i++) {
...
// 填充positions數(shù)組
positions[i3] = (Math.random() - 0.5) * 3
positions[i3 + 1] = (Math.random() - 0.5) * 3
positions[i3 + 2] = (Math.random() - 0.5) * 3
}
...
...
}
generateGalaxy()
居中后的points.png
-
tweaks
- 添加
gui
并設置調試參數(shù) - to know when to generate a new galaxy, you need to listen to the change event, use
onFinishChange()
- 當我們在監(jiān)聽回調后創(chuàng)建了新的galaxy之后,但沒有刪除之前已創(chuàng)建的雕欺,so we need to destroy the previous galaxy, and then move the
geometry
,material
,points
variables outside thegenerateGalaxy
- before assigning these variables, we can test if they already exist and used the
dispose()
,remove()
/** * Galaxy */ ... let geometry = null let material = null let points = null const generateGalaxy = () => { // destroy old galaxy, 每次操作完gui后都會再次執(zhí)行generateGalaxy函數(shù)岛马,避免反復創(chuàng)建新對象 if(points !== null) { geometry.dispose() // 從內存中銷毀對象 material.dispose() scene.remove(points) // mesh不存在占用內存的問題棉姐,因此只需要remove } /** * geometry */ geometry = new THREE.BufferGeometry() ... ... /** * material */ material = new THREE.PointsMaterial({ ... }) /** * points */ points = new THREE.Points(geometry, material) ... }
/* * gui */ const gui = new dat.GUI() gui.add(parameters, 'count') .min(100) .max(1000000) .step(100) .onFinishChange(generateGalaxy) gui.add(parameters, 'size') .min(0.001) .max(0.1) .step(0.001) .onFinishChange(generateGalaxy)
- 添加
-
Create a
radius
parameter
/**
* Galaxy
*/
const parameters = {
count: 100000, // particles的數(shù)量
size: 0.01,
radius: 5, // 星系半徑
}
...
...
/*
* gui
*/
...
...
gui.add(parameters, 'radius')
.min(0.01)
.max(20)
.step(0.01)
.onFinishChange(generateGalaxy)
-
Position the vertices in a straight line from the center and going as far as the radius, 這一步我們將所有的
points
隨機分布在x軸上(可以注釋掉axeshelper方便觀察)
const generateGalaxy = () => {
...
/**
* geometry
*/
...
for(let i = 0; i < parameters.count; i++) {
...
const radius = Math.random() * parameters.radius // 星系半徑
// 填充positions數(shù)組
positions[i3] = radius
positions[i3 + 1] = 0
positions[i3 + 2] = 0
}
...
...
}
-
Creat branches, 默認創(chuàng)建3個分支,分支與分支間的夾角相等啦逆,首先谅海,創(chuàng)建
parameter
const parameters = {
count: 100000, // particles的數(shù)量
size: 0.01,
radius: 5, // 星系半徑
branches: 3, // 星系分支,平分星系角度
}
gui.add(parameters, 'branches')
.min(2)
.max(20)
.step(1)
.onFinishChange(generateGalaxy)
-
Position the particles on those branches
- 在我們當前3個 branches 的基礎上蹦浦,通過取模的方式將
points
順時針依次分布在3個 branches 上
- 在我們當前3個 branches 的基礎上蹦浦,通過取模的方式將
/**
* geometry
*/
...
for(let i = 0; i < parameters.count; i++) {
const i3 = i * 3 // 0 3 6 9 12 ... 數(shù)組中的每3個值為一組(即一個頂點的坐標)
const radius = Math.random() * parameters.radius // 星系半徑
// branch之間的夾角:i % parameters.branches 從0開始計算扭吁,分布在第幾個branch
const branchAngle = (i % parameters.branches) / parameters.branches * Math.PI * 2 // 2π的占比
// 填充positions數(shù)組
positions[i3] = Math.cos(branchAngle) * radius
positions[i3 + 1] = 0
positions[i3 + 2] = Math.sin(branchAngle) * radius
}
...
...
}
generateGalaxy()
在當前branches上依次分布particles.png
-
Creat a
spin
parameter
const parameters = {
count: 100000, // particles的數(shù)量
size: 0.01,
radius: 5, // 星系半徑
branches: 3, // 星系分支,平分星系角度
spin: 1, // 旋轉系數(shù)盲镶,geometry距離原點越遠旋轉角度越大
}
gui.add(parameters, 'spin')
.min(-5)
.max(5)
.step(0.001)
.onFinishChange(generateGalaxy)
-
Multiply the
spinAngle
byspin
parameter
/**
* geometry
*/
...
for(let i = 0; i < parameters.count; i++) {
...
const radius = Math.random() * parameters.radius // 星系半徑
const spinAngle = radius * parameters.spin
...
// 填充positions數(shù)組
positions[i3] = Math.cos(branchAngle + spinAngle) * radius
positions[i3 + 1] = 0
positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius
}
spinAngle.png
-
Create
randomness
parameter
const parameters = {
count: 100000, // particles的數(shù)量
size: 0.01,
radius: 5, // 星系半徑
branches: 3, // 星系分支侥袜,平分星系角度
spin: 1, // 旋轉系數(shù),geometry距離原點越遠旋轉角度越大
randomness: 0.2, // 隨機性
}
gui.add(parameters, 'randomness')
.min(0)
.max(2)
.step(0.001)
.onFinishChange(generateGalaxy)
-
We want the particles to spread more outside, so we use the
randomness
for each branch
for(let i = 0; i < parameters.count; i++) {
...
...
...
// 每個軸不同的隨機值
const randomX = (Math.random() - 0.5) * parameters.randomness
const randomY = (Math.random() - 0.5) * parameters.randomness
const randomZ = (Math.random() - 0.5) * parameters.randomness
// 填充positions數(shù)組
positions[i3] = Math.cos(branchAngle + spinAngle) * radius + randomX
positions[i3 + 1] = randomY
positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius + randomZ
}
spread.png
-
觀察下面的橫截面圖溉贿,我們可以發(fā)現(xiàn)particles的分布都很規(guī)律枫吧,但其實我們想要的效果是更自然一些,例如靠近branch的particles更多宇色,距離branch越遠九杂,particle的數(shù)量也遞減
spread后的橫截面.png
-
Create the
randomnessPower
parameter
const parameters = {
count: 100000, // particles的數(shù)量
size: 0.01,
radius: 5, // 星系半徑
branches: 3, // 星系分支,平分星系角度
spin: 1, // 旋轉系數(shù)宣蠕,geometry距離原點越遠旋轉角度越大
randomness: 0.2, // 隨機性
randomnessPower: 3, // 隨機性系數(shù)例隆,可控制曲線變化
}
gui.add(parameters, 'randomnessPower')
.min(1)
.max(10)
.step(0.001)
.onFinishChange(generateGalaxy)
-
Apply the power with
Math.pow()
and multiply by-1
randomly to have negative values too
for(let i = 0; i < parameters.count; i++) {
...
...
const randomX = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : -1)
const randomY = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : -1)
const randomZ = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : -1)
...
...
}
randomnessPower.png
-
We want a color for a inner particles and a color for the outer particles, so create a
insideColor
and anoutsideColor
const parameters = {
count: 100000, // particles的數(shù)量
size: 0.01,
radius: 5, // 星系半徑
branches: 3, // 星系分支,平分星系角度
spin: 1, // 旋轉系數(shù)抢蚀,geometry距離原點越遠旋轉角度越大
randomness: 0.2, // 隨機性
randomnessPower: 3, // 隨機性系數(shù)镀层,可控制曲線變化
insideColor: '#ff6030',
outsideColor: '#1b3984',
}
gui.addColor(parameters, 'insideColor').onFinishChange(generateGalaxy)
gui.addColor(parameters, 'outsideColor').onFinishChange(generateGalaxy)
-
Create a third color and use the
lerp()
method to mix color
/**
* geometry
*/
...
const colors = new Float32Array(parameters.count * 3)
const colorInside = new THREE.Color(parameters.insideColor)
const colorOutside = new THREE.Color(parameters.outsideColor)
for(let i = 0; i < parameters.count; i++) {
...
...
...
const mixedColor = colorInside.clone() // 這里使用clone()的原因是,lerp()輸出的時候會改變原始值
mixedColor.lerp(colorOutside, radius / parameters.radius) // 根據(jù)半徑計算混合程度
colors[i3] = mixedColor.r
colors[i3 +1] = mixedColor.g
colors[i3 + 2] = mixedColor.b
}
galaxy.png