【Threejs进阶教程-着色器篇】10.粒子Shader入门与基础雪花效果
【Threejs进阶教程-着色器篇】10.粒子Shader入门与基础雪花效果
本系列教程第一篇地址,建议按顺序学习模板代码解析gl_PointSize让粒子大小随着深度变化而变化
修改粒子的样式gl_PointCoord简介给粒子贴图处理透明处叠加
使用顶点着色器操作粒子动态粒子大小动态粒子位置
小练习,模拟下雪效果生成随机的粒子让粒子循环下落
雪花效果完整源码如有不明白的,可以在下方留言或者加群
本系列教程第一篇地址,建议按顺序学习
本系列目前已累计第十篇,这里直接省略了2到9篇的地址,可以通过上方专栏来查阅前面的教程 【Threejs进阶教程-着色器篇】1. Shader入门(ShadertoyShader和ThreejsShader入门)
本篇使用到的模板代码,从这里自取粒子模板代码 【模板代码】用于编写Threejs Demo的模板代码
粒子效果入门教程 【Threejs基础教程-点线精灵篇】4.2 基本粒子效果Points
模板代码解析
首先,我们先分析模板代码,片元着色器与之前本教程讲的片元着色器变化不大,在后面编写代码时才会有变化,所以现在仅讲解顶点着色器代码
varying vec2 vUv;
void main(){
vUv = vec2(uv.x,uv.y);
vec3 u_position = position;
vec4 mvPosition = modelViewMatrix * vec4( u_position, 1.0 );
gl_PointSize = 300.0 / -mvPosition.z;
gl_Position = projectionMatrix * mvPosition;
}
第四行,u_position,主要用于保存当前的变量,而不在原有的变量上做计算,包括对uv的保存等
第五行,mvPosition,字面意思**[模型视图位置]**,修改此位置,可以影响最终渲染的位置,可以简单尝试修改一下这个vec4变量看看最终效果,这个计算,在后续WebGL教程中会详细讲解,第五行可以视为现阶段,threejs的粒子shader的固定写法
gl_PointSize
此属性主要用于调整粒子大小,可以先修改为下面的代码,然后随便改变下数字,我们运行下看下效果,加深对此属性的理解
gl_PointSize = 20.0;
让粒子大小随着深度变化而变化
如果让gl_PointSize 设置为一个固定的数字,那么我们无论如何调整视角,我们的粒子实际上都是固定的大小,类似于之前在SpriteMaterial的讲解中,提到的sizeAttenuation属性
所以,我们除以mvPosition的z,来让粒子产生深度效果
gl_PointSize = 300.0/ - mvPosition.z;
大家可以多次修改这里的公式以及常量,来感受一下粒子大小的变化 这里的公式,现阶段可以看作为固定公式,为什么mvPosition.z 要为负值这个问题,在后续的WebGL教程中会做讲解
300,是本人在使用过程中,对模板代码创建的粒子的一个大致的合适的大小的常量,此常量根据个人需求修改即可 做了这样的修改后,我们的粒子大小,就会随着你的视角拉近拉远而放大缩小了
最后面的代码,其实本质上和之前的代码是一致的,不过是把mvPosition单独拆分出来,给粒子效果使用了而已
修改粒子的样式
我们依然可以用片元着色器的gl_FragColor来控制最终颜色,但是,当我们想使用uv去控制粒子的效果的时候,发现uv并不能对粒子产生影响,这里我们就需要一个新的变量gl_PointCoord
gl_PointCoord简介
//片元着色器代码
varying vec2 vUv;
void main(){
vec2 gpc = gl_PointCoord;
float a = 1.0 - distance(gpc,vec2(0.5));
gl_FragColor = vec4(a,0.0,0.0,1.0);
}
gl_PointCoord,你可以理解为是粒子效果专用的uv,基本用法与uv相似
给粒子贴图
当然,我们也完全可以用 texture2D(texture,gpc); 这种方式,给粒子贴一张图,资源我们依然使用threejs开发包中的资源 three\examples\textures\sprites\circle.png csdn直接保存图片会有水印,但是仅在教学过程中使用,是完全没问题的
记得开启透明度
let uniforms = {
iTime:{value:0}
}
function addMesh() {
let textureLoader = new THREE.TextureLoader();
let map = textureLoader.load('./circle.png');
uniforms.pointMap = {value:map};
let geometry = new THREE.BoxGeometry(1,1,1);
let material = new THREE.ShaderMaterial({
uniforms,
vertexShader:document.getElementById('vertexShader').textContent,
fragmentShader:document.getElementById('fragmentShader').textContent,
transparent:true
})
let points = new THREE.Points(geometry,material);
scene.add(points);
}
varying vec2 vUv;
uniform sampler2D pointMap;
void main(){
vec2 gpc = gl_PointCoord;
vec4 color = texture2D(pointMap,gpc);
gl_FragColor = color;
}
处理透明处叠加
这里有人发现了,我们的图是贴到粒子上了,但是透明的地方有问题 但是,我们尝试给材质追加 alphaTest 的时候,没有任何作用,在ShaderMaterial中,我们要想处理这种透明度错误,一般用下面这种办法
alphaTest的代码非常简单,就是判断透明度的值,然后使用discard 关键字,丢弃掉这个片元
varying vec2 vUv;
uniform sampler2D pointMap;
void main(){
vec2 gpc = gl_PointCoord;
vec4 color = texture2D(pointMap,gpc);
//这里的0.0本质上与alphaTest功能一致,可以尝试修改此值,来达到最好的效果
if(color.a <= 0.0){
discard;
}
gl_FragColor = color;
}
可以看到,我们把透明度小于等于0的部分直接舍弃掉,这样,我们的透明边缘就得到了一定的改善,我们可以通过不断的调整此数值达到最佳效果,这里本人就不再做调整了,大家自行尝试即可
使用顶点着色器操作粒子
动态粒子大小
首先我们回顾一下之前讲到的动效,我们使用uniforms添加一个iTime来控制时间变化
let uniforms = {
iTime:{value:0}
}
function addMesh() {
let textureLoader = new THREE.TextureLoader();
let map = textureLoader.load('./circle.png');
uniforms.pointMap = {value:map};
let geometry = new THREE.BoxGeometry(1,1,1);
let material = new THREE.ShaderMaterial({
uniforms,
vertexShader:document.getElementById('vertexShader').textContent,
fragmentShader:document.getElementById('fragmentShader').textContent,
transparent:true,
})
let points = new THREE.Points(geometry,material);
scene.add(points);
}
function render() {
uniforms.iTime.value += 0.01;
renderer.render(scene,camera);
orbit.update();
requestAnimationFrame(render);
}
varying vec2 vUv;
uniform float iTime;
void main(){
vUv = vec2(uv.x,uv.y);
vec3 u_position = position;
vec4 mvPosition = modelViewMatrix * vec4( u_position, 1.0 );
gl_PointSize = (300.0 * abs(sin(iTime))) / -mvPosition.z;
gl_Position = projectionMatrix * mvPosition;
}
这里我们使用sin函数做周期变化,用绝对值,让计算消除负值,这样我们就可以制作出呼吸效果的粒子效果了
具体要怎么变化,看各位对下面的数学公式的加工了
动态粒子位置
当然我们也可以直接操作粒子的位置
varying vec2 vUv;
uniform float iTime;
void main(){
vUv = vec2(uv.x,uv.y);
vec3 u_position = position;
u_position.y = position.y - fract(iTime);
vec4 mvPosition = modelViewMatrix * vec4( u_position, 1.0 );
gl_PointSize = 300.0 / -mvPosition.z;
gl_Position = projectionMatrix * mvPosition;
}
小练习,模拟下雪效果
生成随机的粒子
首先我们先把上面修改完的顶点着色器改回来
我们上面一直在用BoxGeometry来生成粒子,所以生成的粒子,主要以Box的8个顶点为准
这里我们要更换成随机的粒子,来模仿下雪的感觉
function addMesh() {
let textureLoader = new THREE.TextureLoader();
let map = textureLoader.load('./circle.png');
uniforms.pointMap = {value:map};
//随机生成1000个粒子
let geometry = new THREE.BufferGeometry();
let pointsCount = 1000;
let vecArray = [];
for(let i = 0;i< pointsCount;i++){
let point = new THREE.Vector3();
point.x = Math.random() * 100 - 50;
point.y = Math.random() * 100 - 50;
point.z = Math.random() * 100 - 50;
vecArray.push(point);
}
geometry.setFromPoints(vecArray);
let material = new THREE.ShaderMaterial({
uniforms,
vertexShader:document.getElementById('vertexShader').textContent,
fragmentShader:document.getElementById('fragmentShader').textContent,
transparent:true,
})
let points = new THREE.Points(geometry,material);
scene.add(points);
}
因为我们的粒子贴图是很白很白的,所以这里我们也要把renderer的alpha属性去掉,黑色的背景看起来更清晰
renderer = new THREE.WebGLRenderer({
//alpha:true, 开启此属性后,背景变为透明,背景色变成html的背景色
antialias:true
});
让粒子循环下落
虽然我们可以用很简单的方法,直接操作u_position -= iTime来实现下落,但是我们还需要让下落到底的粒子,再回到最顶部
所以,我们要控制粒子的高度,始终在 最高高度到最低高度内,也就是 -50~50这个区间
我们首先第一步,要计算出我们的粒子所在的位置到整个高度轴空间的百分比,然后用这个百分比去加上一个周期变化的iTime,最后在百分比上做变化后,乘以总高度并计算偏移后,计算出粒子的最终位置,即可得到我们的循环下落效果
varying vec2 vUv;
uniform float iTime;
void main(){
vUv = vec2(uv.x,uv.y);
vec3 u_position = position;
//1. 当前的粒子位置在高度上的百分比 = (粒子高度 - 最低高度)/(最高高度 - 最低高度)
float p1 = (u_position.y - 50.0)/(50.0 - (-50.0));
//2. 下一帧的粒子位置在高度上的百分比 = 当前粒子高度百分比 - 时间 * 下落速度百分比
// 此百分比不能超过1,所以使用fract只取小数部分
float oy = fract(p1 - iTime * 0.01);
// 3.最终位置 = 下一帧的粒子高度百分比 * 总高度 - 最高高度
// 我们的粒子总高度为100,最高的位置在50
u_position.y = oy * 100.0 - 50.0;
vec4 mvPosition = modelViewMatrix * vec4( u_position, 1.0 );
gl_PointSize = 300.0 / -mvPosition.z;
gl_Position = projectionMatrix * mvPosition;
}
雪花效果完整源码
body{
width:100vw;
height: 100vh;
overflow: hidden;
margin: 0;
padding: 0;
border: 0;
}
{
"imports": {
"three": "../three/build/three.module.js",
"three/addons/": "../three/examples/jsm/"
}
}
varying vec2 vUv;
uniform float iTime;
void main(){
vUv = vec2(uv.x,uv.y);
vec3 u_position = position;
//1. 当前的粒子位置在高度上的百分比 = (粒子高度 - 最低高度)/(最高高度 - 最低高度)
float p1 = (u_position.y - 50.0)/(50.0 - (-50.0));
//2. 下一帧的粒子位置在高度上的百分比 = 当前粒子高度百分比 - 时间 * 下落速度百分比
// 此百分比不能超过1,所以使用fract只取小数部分
float oy = fract(p1 - iTime * 0.01);
// 3.最终位置 = 下一帧的粒子高度百分比 * 总高度 - 最高高度
// 我们的粒子总高度为100,最高的位置在50
u_position.y = oy * 100.0 - 50.0;
vec4 mvPosition = modelViewMatrix * vec4( u_position, 1.0 );
gl_PointSize = 300.0 / -mvPosition.z;
gl_Position = projectionMatrix * mvPosition;
}
varying vec2 vUv;
uniform sampler2D pointMap;
void main(){
vec2 gpc = gl_PointCoord;
vec4 color = texture2D(pointMap,gpc);
gl_FragColor = color;
}
import * as THREE from "../three/build/three.module.js";
import {OrbitControls} from "../three/examples/jsm/controls/OrbitControls.js";
window.addEventListener('load',e=>{
init();
addMesh();
render();
})
let scene,renderer,camera;
let orbit;
function init(){
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer({
antialias:true
});
renderer.setSize(window.innerWidth,window.innerHeight);
document.body.appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera(50,window.innerWidth/window.innerHeight,0.1,2000);
camera.add(new THREE.PointLight());
camera.position.set(10,10,10);
scene.add(camera);
orbit = new OrbitControls(camera,renderer.domElement);
orbit.enableDamping = true;
scene.add(new THREE.GridHelper(10,10));
}
let uniforms = {
iTime:{value:0}
}
function addMesh() {
let textureLoader = new THREE.TextureLoader();
let map = textureLoader.load('./circle.png');
uniforms.pointMap = {value:map};
let geometry = new THREE.BufferGeometry();
let pointsCount = 1000;
let vecArray = [];
for(let i = 0;i< pointsCount;i++){
let point = new THREE.Vector3();
point.x = Math.random() * 100 - 50;
point.y = Math.random() * 100 - 50;
point.z = Math.random() * 100 - 50;
vecArray.push(point);
}
geometry.setFromPoints(vecArray);
let material = new THREE.ShaderMaterial({
uniforms,
vertexShader:document.getElementById('vertexShader').textContent,
fragmentShader:document.getElementById('fragmentShader').textContent,
transparent:true,
})
let points = new THREE.Points(geometry,material);
scene.add(points);
}
function render() {
uniforms.iTime.value += 0.01;
renderer.render(scene,camera);
orbit.update();
requestAnimationFrame(render);
}
如有不明白的,可以在下方留言或者加群
如有其他不懂的问题,可以在下方留言,也可以加入qq群咨询, Web3D+GIS开源社区为新群,群内相对来说学习气氛良好,群号131995948 本人的群,群号867120877 欢迎大家来群里交流技术