three.js - Galaxy Generator
2024-03-05 本文已影响0人
闪电西兰花
-
这篇笔记的最终实现目标,是创建一个星系,如下图:

-
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>

-
Create a
generateGalaxy
function and call it, 我们用这个方法生成一个默认参数的星系
/**
* 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的数量
}
const generateGalaxy = () => {
/**
* geometry
*/
const geometry = new THREE.BufferGeometry()
// 创建一个空数组,长度为 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 ... 数组中的每3个值为一组(即一个顶点的坐标)
// 填充positions数组
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的数量
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()

- 从上面的图中可以看出,我们这个粒子堆没有居中(因为使用的
Math.random()
, 坐标范围被限制在 [0, 1]),并且比较集中不够分散,那么修改一下positions
的设置
const generateGalaxy = () => {
/**
* geometry
*/
...
for(let i = 0; i < parameters.count; i++) {
...
// 填充positions数组
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
并设置调试参数 - to know when to generate a new galaxy, you need to listen to the change event, use
onFinishChange()
- 当我们在监听回调后创建了新的galaxy之后,但没有删除之前已创建的,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后都会再次执行generateGalaxy函数,避免反复创建新对象 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的数量
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数组
positions[i3] = radius
positions[i3 + 1] = 0
positions[i3 + 2] = 0
}
...
...
}
-
Creat branches, 默认创建3个分支,分支与分支间的夹角相等,首先,创建
parameter
const parameters = {
count: 100000, // particles的数量
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 ... 数组中的每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数组
positions[i3] = Math.cos(branchAngle) * radius
positions[i3 + 1] = 0
positions[i3 + 2] = Math.sin(branchAngle) * radius
}
...
...
}
generateGalaxy()

-
Creat a
spin
parameter
const parameters = {
count: 100000, // particles的数量
size: 0.01,
radius: 5, // 星系半径
branches: 3, // 星系分支,平分星系角度
spin: 1, // 旋转系数,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数组
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的数量
size: 0.01,
radius: 5, // 星系半径
branches: 3, // 星系分支,平分星系角度
spin: 1, // 旋转系数,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数组
positions[i3] = Math.cos(branchAngle + spinAngle) * radius + randomX
positions[i3 + 1] = randomY
positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius + randomZ
}

-
观察下面的横截面图,我们可以发现particles的分布都很规律,但其实我们想要的效果是更自然一些,例如靠近branch的particles更多,距离branch越远,particle的数量也递减

-
Create the
randomnessPower
parameter
const parameters = {
count: 100000, // particles的数量
size: 0.01,
radius: 5, // 星系半径
branches: 3, // 星系分支,平分星系角度
spin: 1, // 旋转系数,geometry距离原点越远旋转角度越大
randomness: 0.2, // 随机性
randomnessPower: 3, // 随机性系数,可控制曲线变化
}
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 anoutsideColor
const parameters = {
count: 100000, // particles的数量
size: 0.01,
radius: 5, // 星系半径
branches: 3, // 星系分支,平分星系角度
spin: 1, // 旋转系数,geometry距离原点越远旋转角度越大
randomness: 0.2, // 随机性
randomnessPower: 3, // 随机性系数,可控制曲线变化
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) // 根据半径计算混合程度
colors[i3] = mixedColor.r
colors[i3 +1] = mixedColor.g
colors[i3 + 2] = mixedColor.b
}
