- 本篇文章将要绘制一个具有动画效果的galaxy,关于galaxy的实现可以参考之前的这篇笔记 Galaxy,我们在此基础上做了部分修改
- We saw that animating each particles is a bad idea, animating the geometry buffer attribute on each frame is a bad idea, it must be have frame rate issue
- so we're going to animate each particles by using vertex shader
-
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)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
document.body.appendChild(renderer.domElement)
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})
/**
* control
*/
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
/**
* render
*/
const clock = new THREE.Clock()
function tick () {
let elapsedTime = clock.getElapsedTime()
controls.update()
requestAnimationFrame(tick)
renderer.render(scene, camera)
}
tick()
</script>
-
基于之前已实现的 Galaxy 修改后的 Galaxy,添加对应属性的gui
/**
* Galaxy
*/
// 参数
const parameters = {
count: 200000,
size: 0.005,
radius: 5, // 星系半径
branches: 3, // 星系分支,平分星系角度
spin: 1, // 旋转系数,geometry距离原点越远旋转角度越大
randomness: 0.5, // 随机性
randomnessPower: 3, // 随机性系数,可控制曲线变化
insideColor: '#ff6030',
outsideColor: '#1b3984',
}
let geometry = null
let material = null
let points = null
const generateGalaxy = () => {
if(points !== null) {
geometry.dispose()
material.dispose()
scene.remove(points)
}
// geometry
geometry = new THREE.BufferGeometry()
const positions = new Float32Array(parameters.count * 3)
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 i3 = i * 3
// Position
const radius = Math.random() * parameters.radius
const branchAngle = (i % parameters.branches) / parameters.branches * Math.PI * 2
const randomX = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : - 1) * parameters.randomness * radius
const randomY = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : - 1) * parameters.randomness * radius
const randomZ = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : - 1) * parameters.randomness * radius
positions[i3] = Math.cos(branchAngle) * radius + randomX
positions[i3 + 1] = randomY
positions[i3 + 2] = Math.sin(branchAngle) * radius + randomZ
// Color
const mixedColor = colorInside.clone()
mixedColor.lerp(colorOutside, radius / parameters.radius)
colors[i3] = mixedColor.r
colors[i3 + 1] = mixedColor.g
colors[i3 + 2] = mixedColor.b
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3))
// material
material = new THREE.PointsMaterial({
size: parameters.size,
sizeAttenuation: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true,
})
// Points
points = new THREE.Points(geometry, material)
scene.add(points)
}
generateGalaxy()
/**
* gui
*/
const gui = new dat.GUI()
gui.add(parameters, 'count')
.min(100)
.max(1000000)
.step(100)
.onFinishChange(generateGalaxy)
gui.add(parameters, 'radius')
.min(0.01)
.max(20)
.step(0.01)
.onFinishChange(generateGalaxy)
gui.add(parameters, 'branches')
.min(2)
.max(20)
.step(1)
.onFinishChange(generateGalaxy)
gui.add(parameters, 'randomness')
.min(0)
.max(2)
.step(0.001)
.onFinishChange(generateGalaxy)
gui.add(parameters, 'randomnessPower')
.min(1)
.max(10)
.step(0.001)
.onFinishChange(generateGalaxy)
gui.addColor(parameters, 'insideColor').onFinishChange(generateGalaxy)
gui.addColor(parameters, 'outsideColor').onFinishChange(generateGalaxy)
-
The first, we're going to create our own shader, we're going to replace PointsMaterial
by ShaderMaterial
- we get a warning telling us that the
ShaderMaterial
supports neither size
nor sizeAttenuation
, so remove these properties
// material
material = new THREE.ShaderMaterial({
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true,
})
import vertexShader from './animated-galaxy/vertex.glsl'
import fragmentShader from './animated-galaxy/fragment.glsl'
// material
material = new THREE.ShaderMaterial({
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true,
vertexShader,
fragmentShader,
})
// vertex.glsl
void main () {
// Position
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
// Size
gl_PointSize = 2.0; // 内置变量,fragment size
}
// fragment.glsl
void main () {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
-
Handle the Size
- because we're not use the
PointsMaterial
, we need to redo the color, the size, the size attenuation...
// material
material = new THREE.ShaderMaterial({
...
uniforms: {
uSize: {value: 8}
}
})
// vertex.glsl
uniform float uSize;
void main () {
// Position
...
// Size
gl_PointSize = uSize; // fragment size
}
- in real life, stars have different sizes, we can send a random value in the attributes
const generateGalaxy = () => {
...
...
const scales = new Float32Array(parameters.count)
for(let i = 0; i < parameters.count; i++) {
...
...
// Size
scales[i] = Math.random()
}
...
geometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1))
}
// vertex.glsl
uniform float uSize;
attribute float aScale;
void main () {
// Position
...
// Size
gl_PointSize = uSize * aScale; // fragment size
}
-
Fix the pixel ratio
- the
gl_PointSize
means the fragment
size(这里最好要理解下设备像素比的概念, 即物理像素/逻辑像素)
- if you have a screen with a pixel ratio of 1, the particles will look 2 times larger than if the screen with a pixel ratio of 2
- 浏览器获取设备像素比的方法是
window.devicePixelRatio
,但我们在构造render的部分已经设置过了renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
// material
material = new THREE.ShaderMaterial({
...
uniforms: {
uSize: {value: 8 * renderer.getPixelRatio()}
}
})
/**
* Renderer
*/
...
window.addEventListener('resize', () => {
...
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})
generateGalaxy() // 将调用移动至renderer后面,防止上面在获取renderer变量时报错
-
Size Attenuation
- 当我们只关注一颗particle时,会发现不管页面放大或是缩小,particle始终保持着相同大小
- 我们当前使用的
perspectiveCamera
,呈现效果应当是particle距离camera越远,尺寸越小,这就是size attenuation
- 原先使用
PointsMaterial
时,直接设置sizeAttenuation: true
就可以了,但在shader
中我们需要自己实现这一部分
- we're going to take the code from the three.js depedency,PointsMaterial的顶点着色器文件路径: /node_modules/three/src/renderers/shaders/ShaderLib/points.glsl.js,我们需要关注的就是以下这段
#ifdef USE_SIZEATTENUATION
// 只有在perspectiveCamera下才会工作
bool isPerspective = isPerspectiveMatrix( projectionMatrix );
// scale is a value related to the renderer hight
// particle的大小会根据屏幕的高度拉伸,这样不管是在大屏幕还是小屏幕,看到的particle都会差不多
// mvPosition correspond to the position of the vertex once the modelMatrix and the viewMatrix
// in our case, it's viewPosition
if ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z );
#endif
- 基于以上的代码来修改我们自己的,然后就会发现距离camare近的particle尺寸相对远的较大,这就是size attenuation,但我们在这部分并不想让particle随着屏幕大小的改变而拉伸
// vertex.glsl
...
void main () {
...
...
// Size
gl_PointSize = uSize * aScale; // fragment size
gl_PointSize *= ( 1.0 / - viewPosition.z );
}
// fragment.glsl
void main () {
// disc
float strength = distance(gl_PointCoord, vec2(0.5)); // 计算当前片段与圆心(0.5, 0.5)的距离
strength = step(0.5, strength);
strength = 1.0 - strength;
gl_FragColor = vec4(vec3(strength), 1.0);
}
- Diffuse Point Pattern,边缘扩散的particle
// fragment.glsl
void main () {
// diffuse point
float strength = distance(gl_PointCoord, vec2(0.5));
strength *= 2.0;
strength = 1.0 - strength;
gl_FragColor = vec4(vec3(strength), 1.0);
}
Diffuse Point Pattern.png
// fragment.glsl
void main () {
// light point
float strength = distance(gl_PointCoord, vec2(0.5));
strength = 1.0 - strength;
strength = pow(strength, 10.0);
gl_FragColor = vec4(vec3(strength), 1.0);
}
material = new THREE.ShaderMaterial({
...
...
uniforms: {
uSize: {value: 30 * renderer.getPixelRatio()}
}
})
// vertex.glsl
...
varying vec3 vColor;
void main () {
...
...
// Color
vColor = color;
}
// fragmrnt.glsl
varying vec3 vColor;
void main () {
// light point
float strength = distance(gl_PointCoord, vec2(0.5));
strength = 1.0 - strength;
strength = pow(strength, 10.0);
// Color
vec3 color = mix(vec3(0.0), vColor, strength); // 将黑色与color混合
gl_FragColor = vec4(color, 1.0);
}
-
Animate
- we only need to rotate the particles on
x
and z
- we calculate the particle angle and its distance to the center 我们计算particle和x轴之间的夹角以及particle到中心点的距离
- we increase that angle according to the
uTime
and distance
- we can use
atan()
to retrieve the angle 用于计算给定数值的反正切值
// material
material = new THREE.ShaderMaterial({
...
uniforms: {
uSize: {value: 30 * renderer.getPixelRatio()},
uTime: {value: 0}
}
})
/**
* render
*/
const clock = new THREE.Clock()
function tick () {
let elapsedTime = clock.getElapsedTime()
// update material
material.uniforms.uTime.value = elapsedTime
...
}
tick()
// vertex.glsl
uniform float uSize;
uniform float uTime;
...
void main () {
// Position
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
float angle = atan(modelPosition.x, modelPosition.z);
float distanceToCenter = length(modelPosition.xz);
float angleOffset = (1.0 / distanceToCenter) * uTime * 0.2;
angle += angleOffset;
modelPosition.x = cos(angle) * distanceToCenter;
modelPosition.z = sin(angle) * distanceToCenter;
...
...
}
-
增加旋转后的随机性
- 在创建galaxy时设置了一个参数
randomness
,只在设置初始position
时使用了,旋转过程中却没有使用
- 旋转过程中particle分布的随机性不好的话,就容易变得越来越线条,particle不能很好的分布在轴线附近,可以对比上下两张图,还是较为直观的
const generateGalaxy = () => {
...
...
const randomness = new Float32Array(parameters.count * 3)
...
for(let i = 0; i < parameters.count; i++) {
...
// Position
const radius = Math.random() * parameters.radius
const branchAngle = (i % parameters.branches) / parameters.branches * Math.PI * 2
positions[i3] = Math.cos(branchAngle) * radius
positions[i3 + 1] = 0
positions[i3 + 2] = Math.sin(branchAngle) * radius
// random
const randomX = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : - 1) * parameters.randomness * radius
const randomY = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : - 1) * parameters.randomness * radius
const randomZ = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : - 1) * parameters.randomness * radius
randomness[i3] = randomX
randomness[i3 + 1] = randomY
randomness[i3 + 2] = randomZ
...
...
}
...
geometry.setAttribute('aRandomness', new THREE.BufferAttribute(randomness, 3))
...
...
}
// vertex.glsl
...
attribute vec3 aRandomness;
...
void main () {
// Position
...
...
// random
modelPosition.xyz += aRandomness;
...
...
}