-
這篇筆記的最終實現(xiàn)目標喷兼,是創(chuàng)建一個星系酒请,如下圖:
<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>
-
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 a size
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()
const generateGalaxy = () => {
/**
* geometry
*/
...
/**
* material
*/
...
/**
* points
*/
const points = new THREE.Points(geometry, material)
scene.add(points)
}
generateGalaxy()
- 從上面的圖中可以看出,我們這個粒子堆沒有居中(因為使用的
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()
-
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 the generateGalaxy
- 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 上
/**
* 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
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
by spin
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
}
-
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
}
-
觀察下面的橫截面圖溉贿,我們可以發(fā)現(xiàn)particles的分布都很規(guī)律枫吧,但其實我們想要的效果是更自然一些,例如靠近branch的particles更多宇色,距離branch越遠九杂,particle的數(shù)量也遞減
-
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)
...
...
}
-
We want a color for a inner particles and a color for the outer particles, so create a insideColor
and an outsideColor
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
}