第一次尝试使用GLSL来对页面效果进行处理,所以把实现的过程记录下来,方便自己下次查阅。如果你在这篇文章中发现错误,请帮助我纠正,谢谢。
场景构建
在页面中放置将近10w个particle,最开始的考虑将particle分为两个部分,一部分承载用户信息,一部分做装饰。对于在移动端渲染10w个particle性能方面是值得重视的地方,我采取的是承载用户信息的只有3000个左右,使用cpu操作。剩下的装饰particle都是用gpu做动画处理,这样页面会流畅很多。(最好的我觉的都用gpu去绘制,然后通过一种方式去判断这个particle是否可以点击,然后将对应承载信息放入点击的particle)
场景视觉效果&动画实现
-
particle 绘制
particle的生成,我这里直接是用threejs提供的
THREE.Points()
方法去创建。代码如下:
let geometry = createGeometry(); let shaderMaterial = createMaterial(); let particles = new THREE.Points(geometry, shaderMaterial);
其中在创建Geometry的时候,我是创建了一个
THREE.BufferGeometry()
,因为我需要对particles的位置、颜色、大小进行动画操作,创建一个BufferGeometry会方便我们最后续的操作。另外创建Material时,没有使用threejs提供的默认Material,而是使用
THREE.ShaderMaterial()
,这样我们就可以自己用GLSL写shader,这样对particles的控制会更加灵活。 -
particle 颜色
定义两个基础颜色,然后对两个进行mix,来生成不同的颜色。(其中我将一个颜色的g值,再进行随机)
代码如下:
attribute vec3 a_randoms; v_color = mix(vec3(194.0, 236.0 * random, 250.0), vec3(249.0, 178.0, 116.0), a_randoms.x) / 255.0;
-
noise
在讲particle动画的前,先介绍一个非常牛b的东西noise(噪点)。
-
什么是noise?
其实简单理解就是相当于javascript中
Math.random()
。但是在GLSL中没有Math.random()
方法,所以我们要自己去实现一个随机方法。在网上有一个神奇的公式,大多数GLSL程序都会使用它。公式如下:
float rand(vec2 co){ return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); }
简单来理解下这个公式是干嘛的:‘传入一个坐标值co.xy,然后与向量vec2(12.9898,78.233)点乘,避免某个位置分布密集,然后在利用sin or cos打乱点的位置,使其分布更加均匀,接着与一个超级大的数相乘后取小数部分,这样就得到一个(0.0-1.0)的数’。 然后就实现了随机数效果。
渲染效果图:
图1
但是这个简单的随机效果并不是我们想要的,我们需要的是更加平滑的效果。我们可以先来了解下强大的Perlin Noise。具体描述可以点击链接来查看,我们来看渲染出来的效果。
图2
但在这个项目中我使用的是Simplex Noise,Simplex Noise是Ken Perlin创造Perlin Noise后,对算法进行优化的到结果。
这里有一个GLSL的版本https://github.com/ashima/webgl-noise
-
noise能干什么?
我们可以用nosie轻松的做出灼烧,溶解,云雾,模糊,扭曲,随机地形等效果。如下图:
图3
图4
图3引用: https://www.shadertoy.com/view/lsf3RH
图4引用:https://www.shadertoy.com/view/MdXyzX
在项目中我就用noise做了一个表层的起伏效果。静态效果如下:
图5
-
-
particle 组成的形状
刚开始我把所有的particle组成了一个正方体:
但有几点不好:
1、carmea拉到远处的时候,画面很生硬。
2、页面进入的时候有个爆炸效果,当particle炸开的时候,开始有一瞬间会看到一个正方体出现。
基于这两点就换了一种,将particle组成一个球体,在实现球体的过程中出现了一些问题,当时不知道怎么将particle拼成一个球体,最后在网上找了一个计算方法,然后改了一下。先给定particle的初始位置,代码如下:
// 初始位置 球体排列 let phi = rand() * 2 * Math.PI; let cosTheta = rand() * 2 - 1; let u = rand(); let theta = Math.acos(cosTheta); let r = Math.pow(u, 0.4) * particleFieldRadius; defaultPos.push({ x: r * Math.sin(theta) * Math.cos(phi), y: r * Math.sin(theta) * Math.sin(phi), z: r * Math.cos(theta) });
正方体与球体对比:
图6
-
particle 动画
particle的绘制与排列都做完了,然后对particle进行动画&效果的操作。
-
particle 发光效果
为了让particle能看的更有感觉点,然后我用GLSL对particle进行效果的处理。测试阶段的时候绘制出了很多效果,但是总有不满意的地方,效果如下:
图7
最后采用了用一张贴图去做发光的效果,贴图如下:
图8
实现代码如下:
uniform sampler2D u_alphaMap; float alpha = pow(texture2D(u_alphaMap, vec2(distanceToCenter, 0.5)).r * v_alpha * 1.5, 2.0 - v_alpha); vec4 color = vec4(v_color, alpha);
-
particle 噪点效果
因为particle上承载着图片,所以在图片上加了一个简单的噪点效果,让图片看起来不会很死板。实现如下:
vec4 color = vec4(v_color, alpha); color.r *= .9 + randNoise * .2; color.g *= .9 + randNoise * .2; color.b *= .9 + randNoise * .2; gl_FragColor = color;
tips 其中randNoise的值就是上述noise中的简单随机。rand()
-
particle 爆炸效果
爆炸效果实现的比较简单,大致思路就是,因为是个球体,所以设置一个爆开的半径,然后实时去改变particle的position。
伪代码如下:(其中easeOutBack是为了让爆炸看起来更流畅)
if (sharedProperties.prevExplosionRatio < sharedProperties.explosionRatio) { let particlesAttr = particles.geometry.attributes; let particleRatio; for (let i = 0, i3 = 0; i < particleCount; i++, i3 += 3) { let rand = (i * 12.56162) % 1; particleRatio = ease.easeOutBack(math.clampRange(rand * 0.1, 1 - rand * 0.1, sharedProperties.explosionRatio)); particlesAttr.position.array[i3] = defaultPos[i].x * particleRatio; particlesAttr.position.array[i3 + 1] = defaultPos[i].y * particleRatio; particlesAttr.position.array[i3 + 2] = defaultPos[i].z * particleRatio; } particles.geometry.attributes.position.needsUpdate = true; }
tips: 其实应该还有更好的动画实现方案吧?
-
外层 particle 波浪起伏效果
在做动态效果的时候,觉得球体外侧太平了,所以想增加一些效果,让它看起来更有感觉点。说起webgl中的动态效果,最好的就是用niose做了,上面已经讲过noise了。最后想着做一个波浪起伏的效果。大致实现思路:通过noise来控制粒子的起伏效果。设置振幅的高低,然后改变time,使用noise改变particle的位置。实现效果如图5所示。
实现如下:
float lowFeqNoise = snoise4(vec4(position * 2.0, u_noiseTimes.x)); float highFeqNoise = snoise4(vec4(position * 10.0 + 10.0, u_noiseTimes.y)); offset += lowFeqNoise; offset += highFeqNoise; vec3 pos = position; pos *= offset; vec4 mvPosition = modelViewMatrix * vec4( pos, 1.0 ); gl_Position = projectionMatrix * mvPosition;
-
-
camera 平滑运动
然后考虑camera的运动,为了让相机能平滑的过度,这里我直接使用了
TweenLite
,具体使用文档可参考:https://greensock.com/docs/TweenLite -
postprocessing
为了让页面的边界地方看起来平滑点,所以对整个页面进行了处理,让它看起来有点虚的效果。如图所示:
图9改变rgb是为了让元素周围会有色彩的围绕的感觉。
实现大致如下:
uniforms: { 'u_texture': new THREE.WebGLRenderTarget(1, 1), .... }
vec2 rgbShiftDelta = pow(0.05 + distToCenter, 0.1) * toCenter * 0.0025; vec4 rgbShiftRed = texture2D(u_texture, v_uv - rgbShiftDelta); vec4 rgbShiftGreen = texture2D(u_texture, v_uv); vec4 rgbShiftBlue = texture2D(u_texture, v_uv + rgbShiftDelta); gl_FragColor = vec4(...);
场景功能实现
-
particle的点击选中
在3d空间中要做事件的操作,必定少不了射线检测,threejs提供了检测的方法
THREE.Raycaster()
let raycaster = new THREE.Raycaster() touch.x = (e.clientX / window.innerWidth) * 2 - 1; touch.y = -(e.clientY / window.innerHeight) * 2 + 1; let vector = new THREE.Vector3(touch.x, touch.y, 0.5).unproject(camera); raycaster.set(camera.position, vector.sub(camera.position).normalize()); raycaster.params.Points.threshold = 200; raycaster.setFromCamera(touch, camera); let intersects = raycaster.intersectObject(particle); if (intersects.length > 0) { //.. }
tips: 我之前在做点击操作的时候,有时候会点不中particle,这里调整一下
Points.threshold
的值就好了。 -
camera轨迹计算
当用户从一个particle切换到另一个particle的时候,camera有个平滑的过度的效果。当时我想了一种方案,就是将两个particle的信息位置直接交换,然后设定一段固定的camera动画。想象是很美好的,但是实现出来后,发现动画有点生硬,而且移动效果太low,有时会出现,两个particle挨的比较近,当点击的时候,camera会移动很久。所以就放弃了这个low low的办法。
后来想了一种计算两个particle到相机的距离,然后在做camera的运动,通过raycaster我们知道了,点击particle的位置和index,也知道当前显示particle的位置和index,剩下的就是数学问题了。实现如下:
function goToPoint (distance, pointIndex, time, cb) { let fromLookAt = camera.target.clone(); let toLookAt = (new THREE.Vector3()).fromArray(particles.geometry.attributes.position.array, pointIndex * 3); let length = distance; // 最终距离相机的距离 let dir = fromLookAt.sub(toLookAt).normalize().multiplyScalar(length); let c = toLookAt.clone().add(dir); TweenLite.to(camera.position, time, { x: c.x, y: c.y, z: c.z, ease: Power3.easeOut, onComplete: function () { cb && cb(); } }); }
未实现的优化方案
-
内层 particle gpu 绘制
在这个项目中,我承载信息的particle的动画都是直接在cpu中完成的,假设有要求承载的信息的particle数量为5000或者更多,我觉得我现在的方案可能在手机上会炸吧。我将尝试改进,将承载信息的particle的动画用glsl完成。
-
particle的检测方式
在这个项目中我是直接对3000个点进行射线检测,性能其实不是太好,我想了一个简单的思路具体还未实现,就是对屏幕可视区域的粒子做动态检测,将那些离camera远的粒子忽略,也就是区域划分吧。这样我想可能比现在的检测的方案要好一点。
-
文案3d particle 切换
在这个项目中文案的切换是直接利用的图片,然后动画的形式比较少,只有简单的唯一和淡入淡出,可以尝试将文案粒子化,然后用noise去做一个高级的动画切换。文字的3d切换可以看一下我另一篇文章:3d文字动态效果