WebGL three.JS 学习笔记 使用粒子系统模拟时空隧道
本例的运行结果如图:
时空隧道 demo 演示 https://nsytsqdtn.github.io/demo/sprite/tunnel
Demo 地址: https://nsytsqdtn.github.io/demo/360/360
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(Windows.innerWidth, Windows.innerHeight);
- renderer.setClearColor(0x000000);
- document.getElementById("WebGL-output").appendChild(renderer.domElement);// 将渲染添加到 div 中
- // 初始化摄像机, 这里使用透视投影摄像机
- camera = new THREE.PerspectiveCamera(50, Windows.innerWidth / Windows.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(Windows.innerWidth, Windows.innerHeight);
- renderer.setClearColor(0x000000);
- document.getElementById("WebGL-output").appendChild(renderer.domElement); // 将渲染添加到 div 中
- // 初始化摄像机, 这里使用透视投影摄像机
- camera = new THREE.PerspectiveCamera(50, Windows.innerWidth / Windows.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>
来源: https://www.cnblogs.com/nsytsqdtn/p/10777195.html