WebGL three.js學習筆記 使用粒子系統模擬時空隧道(蟲洞)

nsytsqdtn發表於2019-04-26

WebGL three.js學習筆記 使用粒子系統模擬時空隧道

本例的執行結果如圖:

時空隧道

時空隧道demo演示

Demo地址:nsytsqdtn.github.io/demo/sprite…

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>
複製程式碼

相關文章