WebGL three.js学习笔记 使用粒子系统模拟时空隧道

本例的运行结果如图:

时空隧道demo演示

Demo地址:https://nsytsqdtn.github.io/demo/sprite/tunnel

three.js的粒子系统

three.js的粒子系统主要是依靠精灵体来创建的,要实现three.js中的粒子系统创建,一般有两种方式。

第一种是在场景中使用很多歌THREE.Sprite创建单个的精灵,这样创建的每一个精灵体,我们都可以单独对它们进行操作,同时我们也可以用一个THREE.Group把他们放在一起,整合起来一起操作。具有很高的自主性。但同时也是需要大量的性能支持与开发上的不便利性,所以这里我选择了第二种方式。

第二种创建粒子系统是依靠点云的方式,点云就是很多很多点组成的一个东西,点云里面的每一个顶点都可以看做一个粒子,而这个粒子我们就可以使用纹理去对它美化,或者是使用坐标变化来变化出好看的粒子系统,这种创建方式的缺点是不能对每一个粒子单独进行操作,但是相比第一种却给我们提供了更多的方便。

搭建场景

点云的创建方法和普通的几何体差不多,首先需要一个材质THREE.PointsMaterial,可以设置每个粒子的大小size,颜色color,透明transparent等等属性。然后再用THREE.Points(geometry, material)这个方法就可以创建出点云了。

let cloud = new THREE.Points(geom, material);//创建点云

如果我们给了Points(),geometry这个参数,这个点云会按照我们定义好的几何体的顶点去创建粒子。
,比如geometry是一个Box,那么这个点云就会有8粒子,分别分布在正方体的8个顶点上。如果我们不用geometry,我们就需要手动给点云创建很多的顶点,包括定义它们的坐标,这里我们也是用一个定义好的几何体去创建粒子。

//创建点云
    function createPointCloud(geom,color) {
        let material = new THREE.PointsMaterial({
            color: color,
            size: 3,
            transparent: true,
            blending: THREE.AdditiveBlending,//混合的模式,可以让很多的粒子的背景得到很好的融合,而不是互相干扰
            map: generateSprite()//取得渐变的canvas纹理
        });
        let cloud = new THREE.Points(geom, material);//创建点云
        cloud.sortParticles = true;//可以让所有粒子的Z轴得到正确摆放,不会互相遮挡
        return cloud;
    }

函数形参传过来的geom,我们使用的一个类似于管道的几何体TorusGeometry
TorusGeometry的构造函数如下:
THREE.TorusGeometry(radius, tube, radialSegments, tubularSegments, arc)
    radius:圆环半径
    tube:管道半径
    radialSegments:径向的分段数
    tubularSegments:管的分段数
    arc:圆环面的弧度,缺省值为Math.PI * 2

    

let geom = new THREE.TorusGeometry(
controls.radius, controls.tube,
 Math.round(controls.radialSegments),
 Math.round(controls.tubularSegments)
 );//TorusGeometry几何体,管道状的几何体,里面的参数设置都是菜单面板上面的参数

这里的参数主要就是我们要在菜单面板中去更改的值,

controls = new function () {
            this.radius = 100;//整个大圆隧道的半径
            this.tube = 10;//管道的半径
            this.radialSegments = 40;//管道的段数,值越大,创造的物体更精细,也更消耗性能
            this.tubularSegments = 200;//整个大圆隧道的段数,值越大,创造的物体更精细,也更消耗性能
            this.useParticle = true;//是否使用粒子系统创造几何体
            this.rotationSpeed = 0.003;//摄像机的速度
            this.color = 0xffffff;//此颜色会与材质中纹理本身的颜色做乘法,最后的结果就是渲染出来的颜色
            }

如果我们要想创建一个好看的时空隧道还需要它的map属性,去赋给它一个纹理,这样每一个粒子都会比纯色更美观。纹理的话使用图片也是可以的,在这里我选择了制作一个渐变的画布来当做纹理,即generateSprite()这个函数的返回值。
generateSprite函数代码(主要用到的是canvas的绘图函数,js的基础部分):

function generateSprite() {
        let canvas = document.createElement("canvas");
        canvas.width = 16;
        canvas.height = 16;
        let context = canvas.getContext("2d");//得到canvas的绘图上下文
        let gradient = context.createRadialGradient(canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2);//颜色渐变图形
        gradient.addColorStop(0, 'rgba(255,255,255,1)');//从内向外的第一渐变颜色,设置为白色
        gradient.addColorStop(0.2, 'rgba(0,125,125,1)');//从内向外的第二渐变颜色,设置为浅蓝色
        gradient.addColorStop(0.5, 'rgba(0,64,0,1)');//从内向外的第三渐变颜色,设置为绿色
        gradient.addColorStop(1, 'rgba(0,0,0,0.1)');//最外层的渐变颜色,为背景色
        context.fillStyle = gradient;
        context.fillRect(0, 0, canvas.width, canvas.height);

        let texture = new THREE.Texture(canvas);//将得到的画好的canvas作为纹理图片
        texture.needsUpdate = true;//需要设置更新,否则会没有效果
        return texture;
    }

注意texture.needsUpdate = true这句话,否则是渲染不出来的。
到此,我们就可以开始绘制场景

this.draw = function () {
                cameraInit = true;//调用此函数后,对摄像机进行一次初始化
                if (obj) scene.remove(obj);//如果场景的隧道已经存在,先移除
                let geom = new THREE.TorusGeometry(controls.radius, controls.tube, Math.round(controls.radialSegments), Math.round(controls.tubularSegments));//TorusGeometry几何体,管道状的几何体,里面的参数设置都是菜单面板上面的参数
                //使用粒子系统渲染几何体
                if (controls.useParticle) {
                    obj = createPointCloud(geom,controls.color);
                    obj.rotation.x = Math.PI/2;//旋转90度以后,更加方便观测
                } else {//使用普通材质系统渲染几何体
                    obj = createMesh(geom);
                    obj.rotation.x = Math.PI/2;
                }
                scene.add(obj);
            }

场景有了以后,摄像机还是不会动,没有一种在时空隧道的感觉,所以这里想办法让摄像机在这个隧道的中间,沿着这个几何体的形状去移动。

因为管道不看y轴的话,其实还是一个圆形,所以可以使用圆形的参数方程来让摄像机沿着这个函数去运动。让y轴始终不变就可以。

let angle = 0;//初始角度
angle = angle + controls.rotationSpeed;//相机移动的速度
camera.position.set(controls.radius*Math.sin(angle),0,
controls.radius*Math.cos(angle));//让相机按照一个圆形轨迹运动
//可以理解为圆形的参数方程x=rsinα,y=rcosα,

即设置相机的x为rsinα,z为rcosα,y轴是一直都为0的。这里的r为整个隧道的半径,α就是当前移动的角度。
虽然这样可以让相机开始移动了,但是相机的目标我们还没有设置,我们需要让相机在移动的过程中,始终看向前方,这样才有一种在时空隧道中漫游的感觉。但是three.js的相机运动轨迹插件似乎在这里不好用,所以就想到了用其他方式实现。

我们既然已经用相机运动的圆的轨迹方程,也能很容易想到相机lookAt的方向其实就是沿着圆运动的切线方向。所以只需要求摄像机运动的当前位置的切线就可以了。

这里用到的是向量的点乘,坐标的点乘公式x1y2+x2y1,如果结果为0,就可以得到这个向量的垂直向量,我们要求的切线肯定就是垂直于半径的。因为我们的y轴一直不变的,所以点乘公式的y我们变为z。我们首先是让相机的位置减去隧道的中心(0,0,0),得到指向中心的向量,也就是半径,然后再用一个向量与它点乘为0,这个向量方向就是垂直于半径的了,也就是切线的方向。

function look(){
        let view = new THREE.Vector3(camera.position.x,
        camera.position.y,
        camera.position.z);//计算当前摄像机位置点到世界中心点的向量
        let vertical = (new THREE.Vector3(view.z, 0,
        -1.0 * view.x)).normalize();
        //两个向量的点积如果为0,则两个向量垂直,公式为x1*y2+x2*y1=0,
        //这里的Y轴用Z轴代替。计算出垂直向量以后用normalize()化成单位向量
        camera.lookAt(camera.position.x+vertical.x,0,
        camera.position.z+vertical.z);//camera.lookAt的值设置为 刚刚的单位向量加在当前摄像机的位置
        //这样就实现了在摄像机在旋转时,一直朝前看。

    }

最后得到的这个单位向量我们再加上当前相机的位置,就可以设置为相机lookAt的值。
注意我们在每次渲染的时候都要去改变这个值,因为相机的位置一直都在变化的,所以我们要把它封装成一个函数,方便在渲染的时候调用。

其他的,相机,场景的初始化代码:

function initThree() {
        //渲染器初始化
        renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0x000000);
        document.getElementById("WebGL-output").appendChild(renderer.domElement);//将渲染添加到div中
        //初始化摄像机,这里使用透视投影摄像机
        camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.up.x = 0;//设置摄像机的上方向为哪个方向,这里定义摄像的上方为Y轴正方向
        camera.up.y = 1;
        camera.up.z = 0;
        look();//计算摄像机在当前位置应该对准的目标点,即camera.lookAt的设置

        //初始化场景
        scene = new THREE.Scene();

    }

至此,场景基本已经构建完成了。

完整的代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Sprite Tunnel</title>
    <script src="../../import/three.js"></script>
    <script src="../../import/stats.js"></script>
    <script src="../../import/Setting.js"></script>
    <script src="../../import/dat.gui.min.js"></script>
    <style type="text/css">
        div#WebGL-output {
            border: none;
            cursor: pointer;
            width: 100%;
            height: 850px;
            background-color: #000000;
        }
    </style>
</head>
<body onload="Start()">
<div id="WebGL-output"></div>
<script>
    let camera, renderer, scene;
    let controls;

    function initThree() {
        //渲染器初始化
        renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0x000000);
        document.getElementById("WebGL-output").appendChild(renderer.domElement);//将渲染添加到div中
        //初始化摄像机,这里使用透视投影摄像机
        camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.up.x = 0;//设置摄像机的上方向为哪个方向,这里定义摄像的上方为Y轴正方向
        camera.up.y = 1;
        camera.up.z = 0;
        look();//计算摄像机在当前位置应该对准的目标点,即camera.lookAt的设置

        //初始化场景
        scene = new THREE.Scene();

    }
    //计算摄像机在当前位置应该对准的目标点
    function look(){
        let view = new THREE.Vector3(camera.position.x, camera.position.y, camera.position.z);//计算当前摄像机位置点到世界中心点的向量
        let vertical = (new THREE.Vector3(view.z, 0, -1.0 * view.x)).normalize();//两个向量的点积如果为0,则两个向量垂直,公式为x1*y2+x2*y1=0,这里的Y轴用Z轴代替。计算出垂直向量以后用normalize()化成单位向量
        camera.lookAt(camera.position.x+vertical.x,0,camera.position.z+vertical.z);//camera.lookAt的值设置为 刚刚的单位向量加在当前摄像机的位置,这样就实现了在摄像机在旋转时,一直朝前看。

    }
    //
    let obj;
    let cameraInit = false;//改动隧道的半径后,需要让摄像机重新初始化,当cameraInit为true时进行初始化,先定义为false
    //初始化菜单面板
    function initDatGUI() {
        //设置菜单中需要的参数
        controls = new function () {
            this.radius = 100;//整个大圆隧道的半径
            this.tube = 10;//管道的半径
            this.radialSegments = 40;//管道的段数,值越大,创造的物体更精细,也更消耗性能
            this.tubularSegments = 200;//整个大圆隧道的段数,值越大,创造的物体更精细,也更消耗性能
            this.useParticle = true;//是否使用粒子系统创造几何体
            this.rotationSpeed = 0.003;//摄像机的速度
            this.color = 0xffffff;//此颜色会与材质中纹理本身的颜色做乘法,最后的结果就是渲染出来的颜色
            //初始化渲染场景中的隧道以及粒子系统的函数
            this.draw = function () {
                cameraInit = true;//调用此函数后,对摄像机进行一次初始化
                if (obj) scene.remove(obj);//如果场景的隧道已经存在,先移除
                let geom = new THREE.TorusGeometry(controls.radius, controls.tube, Math.round(controls.radialSegments), Math.round(controls.tubularSegments));//TorusGeometry几何体,管道状的几何体,里面的参数设置都是菜单面板上面的参数
                //使用粒子系统渲染几何体
                if (controls.useParticle) {
                    obj = createPointCloud(geom,controls.color);
                    obj.rotation.x = Math.PI/2;//旋转90度以后,更加方便观测
                } else {//使用普通材质系统渲染几何体
                    obj = createMesh(geom);
                    obj.rotation.x = Math.PI/2;
                }
                scene.add(obj);
            }
        };
        let gui = new dat.GUI();
        //将刚刚设置的参数添加到菜单中
        gui.add(controls, "radius", 50, 200).onChange(controls.draw);
        gui.add(controls, "rotationSpeed", 0, 0.02);
        gui.add(controls, "tube", 5, 30).onChange(controls.draw);
        gui.add(controls, "radialSegments", 20, 100).step(1).onChange(controls.draw);
        gui.add(controls, "tubularSegments", 50, 300).step(1).onChange(controls.draw);
        gui.addColor(controls, "color").onChange(controls.draw);
        gui.add(controls, "useParticle").onChange(controls.draw);

        //这里需要先调用一次draw()函数,否则刚开始的时候会没有东西背渲染出来
        controls.draw();
    }
    //精灵贴图的制作,场景的粒子系统的每一个粒子都用这里制作的贴图来模拟
    function generateSprite() {
        let canvas = document.createElement("canvas");
        canvas.width = 16;
        canvas.height = 16;
        let context = canvas.getContext("2d");//得到canvas的绘图上下文
        let gradient = context.createRadialGradient(canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2);//颜色渐变图形
        gradient.addColorStop(0, 'rgba(255,255,255,1)');//从内向外的第一渐变颜色,设置为白色
        gradient.addColorStop(0.2, 'rgba(0,125,125,1)');//从内向外的第二渐变颜色,设置为浅蓝色
        gradient.addColorStop(0.5, 'rgba(0,64,0,1)');//从内向外的第三渐变颜色,设置为绿色
        gradient.addColorStop(1, 'rgba(0,0,0,0.1)');//最外层的渐变颜色,为背景色
        context.fillStyle = gradient;
        context.fillRect(0, 0, canvas.width, canvas.height);

        let texture = new THREE.Texture(canvas);//将得到的画好的canvas作为纹理图片
        texture.needsUpdate = true;//需要设置更新,否则会没有效果
        return texture;
    }
    //创建点云
    function createPointCloud(geom,color) {
        let material = new THREE.PointsMaterial({
            color: color,
            size: 3,
            transparent: true,
            blending: THREE.AdditiveBlending,//混合的模式,可以让很多的粒子的背景得到很好的融合,而不是互相干扰
            map: generateSprite()//取得渐变的canvas纹理
        });
        let cloud = new THREE.Points(geom, material);//创建点云
        cloud.sortParticles = true;//可以让所有粒子的Z轴得到正确摆放,不会互相遮挡
        return cloud;
    }
    //创建普通的管道几何体
    function createMesh(geom) {
        let material = new THREE.MeshNormalMaterial();
        material.side = THREE.DoubleSide;//双边渲染
        let mesh = new THREE.Mesh(geom, material);
        return mesh;
    }

    let angle = 0;//初始角度
    //渲染函数
    function render() {
        if(cameraInit){//每次重新渲染场景的时候,重新设置相机的位置与角度
            angle = 0;
            camera.position.set(controls.radius,0,0);
            cameraInit=false;
        }
        angle = angle + controls.rotationSpeed;//相机移动的速度
        camera.position.set(controls.radius*Math.sin(angle),0,controls.radius*Math.cos(angle));//让相机按照一个圆形轨迹运动,可以理解为圆形的参数方程x=rsinα,y=rcosα,
        look();
        stats.update();
        renderer.clear();
        requestAnimationFrame(render);
        renderer.render(scene, camera);
    }

    //功能函数
    function setting() {
        loadFullScreen();
        loadAutoScreen(camera, renderer);
        loadStats();
    }

    //运行主函数
    function Start() {
        initThree();
        initDatGUI();
        setting();
        render();
    }
</script>
</body>
</html>

WebGL three.js学习笔记 使用粒子系统模拟时空隧道(虫洞)的更多相关文章

  1. WebGL three.js学习笔记 加载外部模型以及Tween.js动画

    WebGL three.js学习笔记 加载外部模型以及Tween.js动画 本文的程序实现了加载外部stl格式的模型,以及学习了如何把加载的模型变为一个粒子系统,并使用Tween.js对该粒子系统进行 ...

  2. WebGL three.js学习笔记 6种类型的纹理介绍及应用

    WebGL three.js学习笔记 6种类型的纹理介绍及应用 本文所使用到的demo演示: 高光贴图Demo演示 反光效果Demo演示(因为是加载的模型,所以速度会慢) (一)普通纹理 计算机图形学 ...

  3. WebGL three.js学习笔记 法向量网格材质MeshNormalMaterial的介绍和创建360度全景天空盒的方法

    WebGL学习----Three.js学习笔记(5) 点击查看demo演示 Demo地址:https://nsytsqdtn.github.io/demo/360/360 简单网格材质 MeshNor ...

  4. WebGL three.js学习笔记 创建three.js代码的基本框架

    WebGL学习----Three.js学习笔记(1) webgl介绍 WebGL是一种3D绘图协议,它把JavaScript和OpenGL ES 2.0结合在一起,通过增加OpenGL ES 2.0的 ...

  5. WebGL three.js学习笔记 自定义顶点建立几何体

    自定义顶点建立几何体与克隆 Three.js本身已经有很多的网格模型,基本已经够我们的使用,但是如果我们还是想自己根据顶点坐标来建立几何模型的话,Three.js也是可以的. 基本效果如图: 点击查看 ...

  6. WebGL three.js学习笔记 纹理贴图模拟太阳系运转

    纹理贴图的应用以及实现一个太阳系的自转公转 点击查看demo演示 demo地址:https://nsytsqdtn.github.io/demo/solar/solar three.js中的纹理 纹理 ...

  7. WebGL three.js学习笔记 阴影与实现物体的动画

    实现物体的旋转.跳动以及场景阴影的开启与优化 本程序将创建一个场景,并实现物体的动画效果 运行的结果如图: 完整代码如下: <!DOCTYPE html> <html lang=&q ...

  8. js学习笔记:webpack基础入门(一)

    之前听说过webpack,今天想正式的接触一下,先跟着webpack的官方用户指南走: 在这里有: 如何安装webpack 如何使用webpack 如何使用loader 如何使用webpack的开发者 ...

  9. Vue.js学习笔记(2)vue-router

    vue中vue-router的使用:

随机推荐

  1. Django升级1.8的一些问题

    1.最明显的问题当然是Settings设置中关于模板的设置数据结构发生变化,这个就不细说了,你开个Django的1.8的新项目就知道怎么改了 2.migrations问题,这个问题是1.8最主要的修改 ...

  2. Ocelot中文文档-不支持

    Ocelot不支持一下几点... 分块编码 - Ocelot将始终获取body大小并返回Content-Length头. 如果这不适合你的场景,只能表示抱歉! 转发host头 - 您发给Ocelot的 ...

  3. JS实现数组去重的6种方法总结

    方法一: 双层循环,外层循环元素,内层循环时比较值,如果有相同的值则跳过,不相同则push进数组. Array.prototype.distinct = function(){ var arr = t ...

  4. JS中$含义和用法

    原博客:https://www.cnblogs.com/jokerjason/p/7404649.html$在JS中本身只是一个符号而异,在JS里什么也不是.但在JS应用库JQUERY的作者将之做为一 ...

  5. 遍历php数组的几种方法

    第一.foreach() foreach()是一个用来遍历数组中数据的最简单有效的方法. <?php $urls= array('aaa','bbb','ccc','ddd'); foreach ...

  6. Maven手动添加jar包

    有的jar在Maven中找不到则需要手动添加(如ojdbc14.jar) 方法如下: 一.将你要添加的jar包放到指定目录(在该目录下打开命令窗口) 二.输入指令:mvn install:instal ...

  7. 二叉树,AVL树和红黑树

    为了接下来能更好的学习TreeMap和TreeSet,讲解一下二叉树,AVL树和红黑树. 1. 二叉查找树 2. AVL树 2.1. 树旋转 2.1.1. 左旋和右旋 2.1.2. 左左,右右,左右, ...

  8. Oracle数据库表分区

    一.Oracle数据库表分区概念和理解         1.1.已经存在的表没有方法可以直接转化为分区表.         1.2.不在分区字段上建立分区索引,在别的字段上建立索引相当于全局索引.效率 ...

  9. python_code list_3

    >>> seq=['foo','x41','?','***']>>> def func(x): return x.isalnum() >>> li ...

  10. element-ui bug及解决方案

    1.element-ui 使用MessageBox后弹窗显示异常 解决方案:去掉Vue.use(MessageBox); 2.element-ui 分页切换后若改变总数会导致请求两次 解决方案:< ...