本次我们将创建一个星系并且能够使用GUI面板来调整各项属性参数
基本粒子
创建generateGalaxy函数
/**
* Galaxy
*/
const generateGalaxy = ()=>{
}
generateGalaxy()
创建parameters对象
对象里面将存放所有关于星系的参数,后面这些参数将被加到GUI面板中
const parameters = {}
// 粒子总数
parameters.count = 1000
// 粒子大小
parameters.size = 0.02
创建随机粒子
在generateGalaxy函数中
/**
* Geometry
*/
const geometry = new THREE.BufferGeometry()
const positions = new Float32Array(parameters.count * 3)
for (let i = 0; i < parameters.count; i++) {
const i3 = i * 3
positions[i3] = (Math.random() - 0.5) * 3 // x
positions[i3 + 1] = (Math.random() - 0.5) * 3 // y
positions[i3 + 2] = (Math.random() - 0.5) * 3 // z
}
geometry.setAttribute('position',new THREE.BufferAttribute(positions,3))
/***
* Material
*/
const material = new THREE.PointsMaterial({
//粒子大小
size:parameters.size,
//开启尺寸衰减
sizeAttenuation:true,
//停用depthWrite
depthWrite:false,
//激活混合
blending:THREE.AdditiveBlending
})
/**
* Points
*/
const points = new THREE.Points(geometry,material)
scene.add(points)
面板参数
gui.add(parameters,'count').min(100).max(10000).step(100).name('粒子总数').onFinishChange(generateGalaxy)
gui.add(parameters,'size').min(0.001).max(0.1).step(0.001).name('粒子大小').onFinishChange(generateGalaxy)
当我们把参数往大了的方向调整完后,再往小的方向调整回去,会发现粒子的总数还是不断增加,这是因为我们每次调整参数都会调用一次generateGalaxy函数,而没去把前面渲染出来的粒子从场景中移除。
很简单,几何体、材质和点我们在外部定义而不是在函数内部
let geometry = null
let material = null
let points = null
之后在generateGalaxy函数开头对点数进行判断。dispose
方法是从内存中销毁对象,每次调用函数生成新星系时,如果点数不为空,那么我们从内存中销毁几何体和材质,然后把点从场景里移除出去,接下去再去新生成点
/**
* 移除旧星系
*/
if(points !== null){
geometry.dispose()
material.dispose()
scene.remove(points)
}
星系形状
我们将创建一个螺旋星系,像下图这种
半径
添加半径参数并添加至面板中
parameters.radius = 5
......
gui.add(parameters,'radius').min(0.01).max(20).step(0.01).name('星系半径').onFinishChange(generateGalaxy)
首先先更改点的坐标,使其位于一条直线上,从中心开始直至半径距离
在循环体中修改点坐标
const radius = Math.random() * parameters.radius
positions[i3] = radius // x
positions[i3 + 1] = 0 // y
positions[i3 + 2] = 0 // z
之后创建多条半径分支
添加分支参数并添加至面板中
parameters.branches = 3
......
gui.add(parameters,'branches').min(2).max(20).step(1).name('星系分支').onFinishChange(generateGalaxy)
以三条分支为例,先确定分支角度,因为只有三条分支,所以分支的角度无非就是0°,120°和240°,可以借助i % 3
不断循环得到0、1、2三个值,然后将之除以3
,再乘2Π
,便可得到三分支的所需要的角度了。之后再通过三角函数设置每个顶点的x
和z
坐标,再乘以半径,便可得到下图效果
for (let i = 0; i < parameters.count; i++) {
const i3 = i * 3
// 粒子半径
const radius = Math.random() * parameters.radius
// 分支角度
const branchAngle = (i % parameters.branches) / parameters.branches * Math.PI * 2
//顶点位置
positions[i3] = Math.cos(branchAngle) * radius// x
positions[i3 + 1] = 0 // y
positions[i3 + 2] = Math.sin(branchAngle) * radius // z
}
螺旋
添加螺旋参数并添加至面板中
parameters.spin = 1
......
gui.add(parameters, 'spin').min(-5).max(5).step(0.001).name('螺旋').onFinishChange(generateGalaxy)
距离中心越远的点旋转角度越大
for (let i = 0; i < parameters.count; i++) {
const i3 = i * 3
// 粒子半径
const radius = Math.random() * parameters.radius
// 分支角度
const branchAngle = (i % parameters.branches) / parameters.branches * Math.PI * 2
// 螺旋角度
const spinAngle = radius * parameters.spin
//顶点位置
positions[i3] = Math.cos(branchAngle + spinAngle) * radius// x
positions[i3 + 1] = 0 // y
positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius // z
}
随机性
添加随机性参数并添加至面板中
parameters.randomness = 0.2
......
gui.add(parameters, 'randomness').min(0).max(2).step(0.001).name('随机性').onFinishChange(generateGalaxy)
我们希望粒子沿着分支方向向外随机扩散。首先要先为每个轴创建一个随机值,并将其乘以半径和随机性参数,然后设置给position
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[i3] = Math.cos(branchAngle + spinAngle) * radius + randomX // x
positions[i3 + 1] = 0 + randomY // y
positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius + randomZ // z
观察上图可以看出有点效果,但是还是达不到我们想要的那种高随机性的粒子扩散分布,这是因为我们的随机数总是处在一个固定范围内,分支上这些点的整体分布是接近线性增长曲线的分布形,而我们要的是类似指数增长曲线的分布形。
创建随机性幂参数并添加到面板中
parameters.randomnessPower = 3
......
gui.add(parameters, 'randomnessPower').min(1).max(10).step(0.001).name('随机性幂').onFinishChange(generateGalaxy)
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 )
颜色
我们想要里面的粒子是一种颜色,外面的粒子是一种颜色
创建内颜色和外颜色参数并添加到面板中
parameters.insideColor = '#ff6030'
parameters.outsideColor = '#1b3984'
gui
.addColor(parameters, 'insideColor')
.name('内颜色')
.onFinishChange(generateGalaxy)
gui
.addColor(parameters, 'randomnessPower')
.name('外颜色')
.onFinishChange(generateGalaxy)
设置材质的vertexColors
属性为true
开启顶点着色
//激活顶点着色
vertexColors:true
实例化内外颜色,并设置几何体的color
属性
const colorInside = new THREE.Color(parameters.insideColor)
const colorOutside = new THREE.Color(parameters.outsideColor)
......
geometry.setAttribute('color',new THREE.BufferAttribute(colors,3))
在循环体内,通过克隆内颜色colorInside
来创建第三个Color类实例mixedColor
,然后使用lerp()
方法将之与外颜色colorOutside
混合。之所以克隆内颜色是因为lerp方法会改变原有的值。
.lerp ( color : Color, alpha : Float ) : this
color
- 用于收敛的颜色。
alpha
- 介于0到1的数字。
将该颜色的RGB值线性插值到传入参数的RGB值。alpha参数可以被认为是两种颜色之间的比例值,其中设置为 0 时是当前颜色,设置为 1 时是第一个参数用于收敛的颜色。
我们想要的是越趋近于星系中心,粒子颜色越趋于内颜色colorInside
,越远离星系中心,粒子颜色越趋近于外颜色colorOutside
,因此lerp
方法的第二个参数alpha
值应该设置为粒子半径 / 星系半径
,比例越小越趋于0,颜色趋于内颜色,比例越大越趋于1,颜色越趋于外颜色
const mixedColor = colorInside.clone()
mixedColor.lerp(colorOutside,radius/parameters.radius)
//顶点颜色
colors[i3] = mixedColor.r
colors[i3+1] = mixedColor.g
colors[i3+2] = mixedColor.b