【Threejs进阶教程-着色器篇】10.粒子Shader入门与基础雪花效果

本系列教程第一篇地址,建议按顺序学习

本系列目前已累计第十篇,这里直接省略了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);
    }
<!--片元着色器代码-->
<script type="x-shader/x-fragment" id="fragmentShader">

    varying vec2 vUv;
    uniform sampler2D pointMap;
    void main(){
        vec2 gpc = gl_PointCoord;
        vec4 color = texture2D(pointMap,gpc);
        gl_FragColor = color;
    }
</script>

在这里插入图片描述

处理透明处叠加

这里有人发现了,我们的图是贴到粒子上了,但是透明的地方有问题
但是,我们尝试给材质追加 alphaTest 的时候,没有任何作用,在ShaderMaterial中,我们要想处理这种透明度错误,一般用下面这种办法

alphaTest的代码非常简单,就是判断透明度的值,然后使用discard 关键字,丢弃掉这个片元

<!--片元着色器代码-->
<script type="x-shader/x-fragment" id="fragmentShader">

    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;
    }
</script>

在这里插入图片描述
可以看到,我们把透明度小于等于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);
    }
<!-- 顶点着色器 -->
<script type="x-shader/x-vertex" id="vertexShader">
    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;
    }
</script>

这里我们使用sin函数做周期变化,用绝对值,让计算消除负值,这样我们就可以制作出呼吸效果的粒子效果了

具体要怎么变化,看各位对下面的数学公式的加工了
在这里插入图片描述

在这里插入图片描述

动态粒子位置

当然我们也可以直接操作粒子的位置

<script type="x-shader/x-vertex" id="vertexShader">
    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;
    }
</script>

在这里插入图片描述

小练习,模拟下雪效果

生成随机的粒子

首先我们先把上面修改完的顶点着色器改回来

我们上面一直在用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,最后在百分比上做变化后,乘以总高度并计算偏移后,计算出粒子的最终位置,即可得到我们的循环下落效果

<script type="x-shader/x-vertex" id="vertexShader">
    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;
    }
</script>

在这里插入图片描述

雪花效果完整源码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        body{
            width:100vw;
            height: 100vh;
            overflow: hidden;
            margin: 0;
            padding: 0;
            border: 0;
        }
    </style>
</head>
<body>

<script type="importmap">
			{
				"imports": {
					"three": "../three/build/three.module.js",
					"three/addons/": "../three/examples/jsm/"
				}
			}
		</script>

<script type="x-shader/x-vertex" id="vertexShader">
    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;
    }
</script>

<!--片元着色器代码-->
<script type="x-shader/x-fragment" id="fragmentShader">

    varying vec2 vUv;
    uniform sampler2D pointMap;
    void main(){
        vec2 gpc = gl_PointCoord;
        vec4 color = texture2D(pointMap,gpc);
        gl_FragColor = color;
    }
</script>

<script type="module">

    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);
    }

</script>
</body>
</html>

如有不明白的,可以在下方留言或者加群

如有其他不懂的问题,可以在下方留言,也可以加入qq群咨询,
Web3D+GIS开源社区为新群,群内相对来说学习气氛良好,群号131995948
本人的群,群号867120877
欢迎大家来群里交流技术
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值