前言
通过着色器如何实现粒子之间动态切换
一、代码
script.js
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
import GUI from 'lil-gui'
import gsap from 'gsap'
import particlesVertexShader from './shaders/particles/vertex.glsl'
import particlesFragmentShader from './shaders/particles/fragment.glsl'/*** Base*/
// Debug
const gui = new GUI({ width: 340 })
const debugObject = {}// Canvas
const canvas = document.querySelector('canvas.webgl')// Scene
const scene = new THREE.Scene()// Loaders
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('./draco/')
const gltfLoader = new GLTFLoader()
gltfLoader.setDRACOLoader(dracoLoader)/*** Sizes*/
const sizes = {width: window.innerWidth,height: window.innerHeight,pixelRatio: Math.min(window.devicePixelRatio, 2)
}window.addEventListener('resize', () =>
{// Update sizessizes.width = window.innerWidthsizes.height = window.innerHeightsizes.pixelRatio = Math.min(window.devicePixelRatio, 2)// Materialsif(particles)particles.material.uniforms.uResolution.value.set(sizes.width * sizes.pixelRatio, sizes.height * sizes.pixelRatio)// Update cameracamera.aspect = sizes.width / sizes.heightcamera.updateProjectionMatrix()// Update rendererrenderer.setSize(sizes.width, sizes.height)renderer.setPixelRatio(sizes.pixelRatio)
})/*** Camera*/
// Base camera
const camera = new THREE.PerspectiveCamera(35, sizes.width / sizes.height, 0.1, 100)
camera.position.set(0, 0, 8 * 2)
scene.add(camera)// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true/*** Renderer*/
const renderer = new THREE.WebGLRenderer({canvas: canvas,antialias: true,
})renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(sizes.pixelRatio)debugObject.clearColor = '#160920'
gui.addColor(debugObject, 'clearColor').onChange(() => { renderer.setClearColor(debugObject.clearColor) })
renderer.setClearColor(debugObject.clearColor)/*** Particles*/
let particles = null// Load models
gltfLoader.load('./models.glb',(gltf)=>{particles = {}particles.index = 0;// Positions //需要知道为什么要位置,因为要拿到位置应用const positions = gltf.scene.children.map(child=>child.geometry.attributes.position)particles.maxCount = 0for(const position of positions){if(position.count > particles.maxCount){particles.maxCount = position.count // 获取最大计数}}particles.positions = []for(const position of positions){const originalArray = position.arrayconst newArray = new Float32Array(particles.maxCount * 3)for(let i=0;i<particles.maxCount;i++){const i3 = i * 3if(i3 < originalArray.length){newArray[i3 + 0] = originalArray[i3 + 0]newArray[i3 + 1] = originalArray[i3 + 1]newArray[i3 + 2] = originalArray[i3 + 2]}else{// 这里如果设置为0 可以看到集合体粒子 中心有白点const randomIndex = Math.floor(position.count * Math.random()) * 3newArray[i3 + 0] = originalArray[randomIndex + 0]newArray[i3 + 1] = originalArray[randomIndex + 1]newArray[i3 + 2] = originalArray[randomIndex + 2]}}particles.positions.push(new THREE.Float32BufferAttribute(newArray,3)) // 告诉gpu 是3 * 3取值}// Geometryconst sizesArray = new Float32Array(particles.maxCount) // 设置粒子的大小for(let i=0;i<particles.maxCount;i++){sizesArray[i] = Math.random()}particles.geometry = new THREE.BufferGeometry()particles.geometry.setAttribute('position',particles.positions[particles.index]) // 本次的模型particles.geometry.setAttribute('aPositionTarget',particles.positions[3]) // 目标到达的模型 particles.geometry.setAttribute('aSize',new THREE.BufferAttribute(sizesArray,1)) // 目标到达的模型 // particles.geometry.setIndex(null) // 发现圆形集合体 平面很亮 因为每个点都有六个顶点组成(多个三角形组成平面,平面交点由六个点组成一个顶点),应该停用// Materialparticles.colorA = '#ff7300'particles.colorB = '#0091ff'particles.material = new THREE.ShaderMaterial({vertexShader: particlesVertexShader,fragmentShader: particlesFragmentShader,uniforms:{uSize: new THREE.Uniform(0.4),uResolution: new THREE.Uniform(new THREE.Vector2(sizes.width * sizes.pixelRatio, sizes.height * sizes.pixelRatio)),uProgress:new THREE.Uniform(0),// 为什么这个从0-1就改变了模型,其实更改了particles中模型的顶点位置 变成aPositionTargetuColorA:new THREE.Uniform(new THREE.Color(particles.colorA)),uColorB:new THREE.Uniform(new THREE.Color(particles.colorB)),},blending: THREE.AdditiveBlending,depthWrite:false , // 深度缓冲器})// Pointsparticles.points = new THREE.Points(particles.geometry, particles.material)particles.points.frustumCulled = false // 判断边界scene.add(particles.points)/* 将粒子转换为不同形状需要3个步骤将位置属性设置为原始几何体将 aPositionTarget 属性设置为目标几何体将uProgress从0动画到1*/// Methods 保存粒子的方法particles.morph = (index) =>{ // 能够得到正确的索引// Update attributesparticles.geometry.attributes.position = particles.positions[particles.index] // particles.geometry.attributes.aPositionTarget = particles.positions[index]// Animate uProgressgsap.fromTo(particles.material.uniforms.uProgress,{value:0},{value:1, duration:3, ease:'linear'},)// save indexparticles.index = index}particles.morph0 = () => { particles.morph(0) }particles.morph1 = () => { particles.morph(1) }particles.morph2 = () => { particles.morph(2) }particles.morph3 = () => { particles.morph(3) }// Tweaks/* 能否做一些改进,因为每一个都是从头开始做线性运动然后结束这一次,我们将使用simplex Ncise。非常相似看起来更自然不那么网格状性能更高(特别是在较高维度上)同样由肯·佩林创作1.在不同时刻开始// https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83*/gui.addColor(particles,'colorA').onChange(()=>{particles.material.uniforms.uColorA.value.set(particles.colorA)})gui.addColor(particles,'colorB').onChange(()=>{particles.material.uniforms.uColorB.value.set(particles.colorB)})gui.add(particles.material.uniforms.uProgress,'value').min(0).max(1).step(0.001).name('uProgress').listen() // 监听变化gui.add(particles,'morph0')gui.add(particles,'morph1')gui.add(particles,'morph2')gui.add(particles,'morph3')})/*** Animate*/
const tick = () =>
{// Update controlscontrols.update()// Render normal scenerenderer.render(scene, camera)// Call tick again on the next framewindow.requestAnimationFrame(tick)
}tick()
fragment.glsl
varying vec3 vColor;void main()
{vec2 uv = gl_PointCoord;float distanceToCenter = length(uv - 0.5);float alpha = 0.05 / distanceToCenter - 0.1; // 为什么减去0.1 ,因为到正方形的边界没有变成0 ,而是无限接近gl_FragColor = vec4(vColor, alpha);#include <tonemapping_fragment>#include <colorspace_fragment>
}
vertex.glsl
uniform vec2 uResolution;
uniform float uSize;
uniform float uProgress;
uniform vec3 uColorA;
uniform vec3 uColorB; varying vec3 vColor;attribute vec3 aPositionTarget;
attribute float aSize;// 引入噪音 一种表现形式
#include ../includes/simplexNoise3d.glslvoid main()
{// Mixed positionfloat nosieOrigin = simplexNoise3d(position * 0.2);float nosieTarget = simplexNoise3d(aPositionTarget * 0.2);float nosie = mix(nosieOrigin,nosieTarget,uProgress);nosie = smoothstep(-1.0,1.0,nosie);// 设置噪音 0 - 1 float duration = 0.4; // 设置延长float delay = (1.0 - duration) * nosie; // 延长事件最大float end = delay + duration; // 结束float progress = smoothstep(delay, end, uProgress);vec3 mixedPosition = mix(position,aPositionTarget,progress);// Final positionvec4 modelPosition = modelMatrix * vec4(mixedPosition, 1.0);vec4 viewPosition = viewMatrix * modelPosition;vec4 projectedPosition = projectionMatrix * viewPosition;gl_Position = projectedPosition;// Point sizegl_PointSize = aSize * uSize * uResolution.y;gl_PointSize *= (1.0 / - viewPosition.z);// varyingvColor = mix(uColorA,uColorB,nosie);
}
index.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Particles Morphing</title><link rel="stylesheet" href="./style.css">
</head>
<body><canvas class="webgl"></canvas><script type="module" src="./script.js"></script>
</body>
</html>
style.css
*
{margin: 0;padding: 0;
}html,
body
{overflow: hidden;
}.webgl
{position: fixed;top: 0;left: 0;outline: none;
}
simplexNoise3d.glsl (噪音)
// Simplex 3D Noise
// by Ian McEwan, Ashima Arts
//
vec4 permute(vec4 x){ return mod(((x*34.0)+1.0)*x, 289.0); }
vec4 taylorInvSqrt(vec4 r){ return 1.79284291400159 - 0.85373472095314 * r; }float simplexNoise3d(vec3 v)
{const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);// First cornervec3 i = floor(v + dot(v, C.yyy) );vec3 x0 = v - i + dot(i, C.xxx) ;// Other cornersvec3 g = step(x0.yzx, x0.xyz);vec3 l = 1.0 - g;vec3 i1 = min( g.xyz, l.zxy );vec3 i2 = max( g.xyz, l.zxy );// x0 = x0 - 0. + 0.0 * C vec3 x1 = x0 - i1 + 1.0 * C.xxx;vec3 x2 = x0 - i2 + 2.0 * C.xxx;vec3 x3 = x0 - 1. + 3.0 * C.xxx;// Permutationsi = mod(i, 289.0 ); vec4 p = permute( permute( permute( i.z + vec4(0.0, i1.z, i2.z, 1.0 )) + i.y + vec4(0.0, i1.y, i2.y, 1.0 )) + i.x + vec4(0.0, i1.x, i2.x, 1.0 ));// Gradients// ( N*N points uniformly over a square, mapped onto an octahedron.)float n_ = 1.0/7.0; // N=7vec3 ns = n_ * D.wyz - D.xzx;vec4 j = p - 49.0 * floor(p * ns.z *ns.z); // mod(p,N*N)vec4 x_ = floor(j * ns.z);vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N)vec4 x = x_ *ns.x + ns.yyyy;vec4 y = y_ *ns.x + ns.yyyy;vec4 h = 1.0 - abs(x) - abs(y);vec4 b0 = vec4( x.xy, y.xy );vec4 b1 = vec4( x.zw, y.zw );vec4 s0 = floor(b0)*2.0 + 1.0;vec4 s1 = floor(b1)*2.0 + 1.0;vec4 sh = -step(h, vec4(0.0));vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;vec3 p0 = vec3(a0.xy,h.x);vec3 p1 = vec3(a0.zw,h.y);vec3 p2 = vec3(a1.xy,h.z);vec3 p3 = vec3(a1.zw,h.w);// Normalise gradientsvec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));p0 *= norm.x;p1 *= norm.y;p2 *= norm.z;p3 *= norm.w;// Mix final noise valuevec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);m = m * m;return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3) ) );
}
开始结束的线性运动
二,效果
three.js 关于着色器粒子应用
总结
属于着色器应该的一个小案例