threejs+vue3實現煙花效果

onedaynobug發表於2022-03-11

前言

今年過年由於疫情又沒回家,就抽空看了看WebGL程式設計指南Three.js開發指南,為了練手,就簡單實現了一下冬奧開幕式上的迎客松煙花效果,純小白,這篇文章主要是記錄這次實踐過程,涉及的也都是threejs最基礎的內容,那我們就開始吧...

效果

效果圖如下,也可以點選連結預覽 https://awebgl.vercel.app/ 先是大門開啟的一個動作,接著機器人是threejs官網的一個例子,最後就是煙花飛上天空爆炸成迎客鬆的一個效果。
WX20220311-112418@2x.png

環境

用的vite+vue3,步驟如下,threejs的api比較長,目前我是記不住(捂臉),npm安裝了@types/three,vscode就有程式碼提示功能了,還安裝了一個動畫庫tween.js。

npm create vite@latest three3d --template vue
cd three3d
npm install @tweenjs/tween.js @types/three three -S

實現

個人感覺做3d,完全可以把自己當成一個導演(偷笑),相機放在哪個位置、燈光在哪兒等等,不然就容易遇到滿屏黑,下面是我這個例子的簡圖,大家按照自己的習慣來構建就好。

IMG_0101.png

場景和相機

首先需要建立一個場景,有了場景才能新增光照、圖元。
然後這個例子使用的是透視攝像機PerspectiveCamera(fov,aspect,near,far), 它可以提供一個近大遠小的3D視覺效果,aspect通常設為畫布的長寬比,這裡我選取的是整個頁面,所以就是window.innerWidth/window.innerHeight,最後把相機指向中心(0,0,0)。

//App.vue
const createScene = ()=>{
    scene = new THREE.Scene(); //建立場景
}
const createCamera = ()=>{
    const scale = window.innerWidth/window.innerHeight;
    camera = new THREE.PerspectiveCamera(60,scale,0.1,1000);
    camera.position.set(0,0,20)
    camera.lookAt(new THREE.Vector3(0,0,0))
}

光照

首先新增的是環境光,AmbientLight只是簡單地將材質的顏色與光照顏色進行疊加,再乘以光照強度,可以參考下面程式碼的註釋部分,由於是夜晚,我選取了較暗的顏色。同樣由於是夜晚,沒有選用太陽光照那樣的平行光,而是在大門的左右新增了一個類似路燈效果的聚光燈光源。

//App.vue
const createLight = ()=>{
  // 環境光
    // 這裡的顏色計算是 RBG 通道上的值分別對應相乘
    // 例: rgb(0.64,0.64,0.64) = rgb(0.8,0.8,0.8) * rgb(0.8,0.8,0.8) * 1
    // color = materialColor * light.color * light.intensity;
    const ambientLight = new THREE.AmbientLight(0x1c1c1c);
    ambientLight.intensity = 1;
    scene.add(ambientLight);
    // 聚光燈
    const spotLight1 = new THREE.SpotLight(0xffffff, 1);
    spotLight1.position.set(-1.5,1.5,10); 
    spotLight1.target.position.set(-1.5, -2, 8);
    spotLight1.castShadow = true;
    spotLight1.shadow.mapSize.width = 2048;
    spotLight1.shadow.mapSize.height = 2048;
    spotLight1.shadow.camera.near = 1;
    spotLight1.shadow.camera.far = 100;
    spotLight1.shadow.camera.fov = 30;
    spotLight1.penumbra = 1;
    scene.add(spotLight1);
    scene.add(spotLight1.target);
    //同理新增右側燈光...
}

圖元模型

相機光照舞臺都搭好了,接著就該是我們的主角們出場了。

小樹枝:可見下面的示意圖,選取圓的一部分,移到原點,然後弧線繞z軸旋轉,左右各生成4條,旋轉後點的計算公式,可以直接套用數學公式,這裡需要提到的一點,threejs使用的是右手座標系,z軸垂直螢幕指向外,所以示意圖是繞z軸逆時針旋轉了θ',右邊四條弧線我傳的也是負數。最後把這個小樹枝的scale設定為0,然後放大到1,就簡單實現了一個爆炸效果。

迎客鬆整個樹枝:首先把上面的小樹枝克隆60個,然後在一個16 4 2的立方體區域裡,隨機選取其中60個位置設定給小樹枝,最後再分3批依次綻放。

迎客松樹幹:斜著5條和豎著10條直線,實現效果和上面類似,大家可以直接看原始碼,我就不再介紹了。

IMG_0100.png

//App.vue
  const pointsMaterial = new THREE.PointsMaterial({
    size: 0.15,
    sizeAttenuation: true,
    transparent: true,
    opacity: 0.8,
    color: 0xffffff,
    depthWrite: false,
    blending: THREE.AdditiveBlending,
    vertexColors: true
  });
  const circleNum = 8; //8條弧線
  const circlePointNum = circlePoint*3*circleNum;
  const circleColors = getColors(circlePointNum);
  let circleArr = [];
  //右4條弧線左4條弧線
  for(let i=0;i<4;i++){
    circleArr = circleArr.concat(getRightPosition(-1*i/12)).concat(getLeftPosition(i/12));
  }
  const circleGeometry = new THREE.BufferGeometry();
  circleGeometry.setAttribute("color",new THREE.BufferAttribute(circleColors,3))
  circleGeometry.setAttribute("position",new THREE.BufferAttribute(new Float32Array(circleArr),3))
  circleGeometry.attributes.position.needsUpdate = true;
  const circlepoints = new THREE.Points(circleGeometry,pointsMaterial);
  const circlegroup = new THREE.Group()
  circlegroup.add(circlepoints);
  circlegroup.visible = false
  circlegroup.scale.set(0,0,0)
  const flowerGroup = new THREE.Group();
  //60個隨機小煙花
  for(let i=0;i<60;i++){
    let tgroup = circlegroup.clone();
    tgroup.position.set(16*Math.random()-8,4*Math.random()+5,-2*Math.random())
    flowerGroup.add(tgroup);
  }
  flowerGroup.position.set(0,0,-2)
  scene.add(flowerGroup);

機器人:這個是threejs官網的一個例子,連結https://threejs.org/examples/... ,加上這個是因為開門放煙花感覺有點單調,就把這個機器人用上了,用了揮手、跑、跳三個動作。需要提到的是,在載入機器人模型時,如果不等待載入完,就進行下一步操作,會出現黑屏,感覺跟vue框架也有關係,最後就用Promise統一封裝了。

//App.vue
  let people = await loadMesh(peopleModel);
  people.scene.traverse((child)=>{
    if (child.isMesh) {
      child.castShadow = true;
    }
  })
  people.scene.position.set(0.5, -5, 4);
  people.scene.scale.set(0.6, 0.6, 0.6);
  //載入動畫
  mixer = new THREE.AnimationMixer(people.scene);
  let animations = people.animations;
  let jumpClip = mixer.clipAction(animations[3])
  let runClip = mixer.clipAction(animations[6])
  let waveClip = mixer.clipAction(animations[12])
  waveClip.play()
  scene.add(people.scene)

大門:這塊需要提到的是,圖元在旋轉的時候都是繞著中心點的,我用了組合Group,左邊的門往右偏移半個門的寬度,同理右邊的門往左偏移,然後再把Group分別向左、右各平移一整個門的寬度,最後Group繞y軸旋轉就達到開門的效果了,大門的前面貼了紋理,其它面都是紅色,在官網沒找到立方體面的對應紋理順序,我試了一下應該是[右,左,上,下,前,後]。

//App.vue
  const doorWidth = 3;
  const doorHeight = 6;
  const cubeGeometry = new THREE.BoxGeometry(doorWidth,doorHeight,0.5);
  const leftdoorTexture = await loadTexture(leftDoorPic);
  const rightdoorTexture = await loadTexture(rightDoorPic);
  const cubeMaterial1 = new THREE.MeshPhongMaterial({color:0x5C0400});
  const cubeMaterial2 = new THREE.MeshPhongMaterial({map:leftdoorTexture});
  const cubeMaterial3 = new THREE.MeshPhongMaterial({map:rightdoorTexture});
  const doorGroup1 = new THREE.Group()
  const doorCube1 = new THREE.Mesh(cubeGeometry,[cubeMaterial1,cubeMaterial1,cubeMaterial1,cubeMaterial1,cubeMaterial2,cubeMaterial1]);
  doorCube1.castShadow = true;
  doorCube1.position.x = doorWidth/2;
  doorGroup1.position.set(-doorWidth,doorHeight/2-5,8);
  doorGroup1.add(doorCube1)
  scene.add(doorGroup1);
  //同理新增右側大門...

最後簡單整理一下整個動畫過程吧,大概就是大門開啟-》機器人揮手-》跑向炮竹-》炮竹起飛-》煙花綻放。動畫用的是tweenjs庫,api挺簡單的,我就不多說了。

渲染

首先建立WebGLRenderer渲染器,設定裝置畫素比、開啟陰影等操作,最後把domElement也就是一個canvas新增到頁面裡。
然後又建立了一個update方法,呼叫requestAnimationFrame來更新動畫,為了方便檢視,建立了OrbitControls軌道控制器,可以使相機圍繞目標(0,0,0)進行軌道運動。

//App.vue
  const createRender = ()=>{
    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth,window.innerHeight);
    renderer.shadowMap.enabled = true;
    container.value.appendChild(renderer.domElement);
  }
  const updateRender = ()=>{
    requestAnimationFrame(updateRender)
    renderer.render(scene, camera);
    orbitControls&&orbitControls.update();
    TWEEN.update();
    let time = clock.getDelta()
    mixer&&mixer.update(time)
  }

onMounted

上面的方法都建立好了,最後在onMounted呼叫就可以了,展示一個三維場景,基本就是下面幾步。

//App.vue
onMounted(()=>{
  createScene(); //建立場景
  createCamera(); //建立相機
  createLight(); //建立光照
  createMesh(); //載入模型
  createRender(); //建立渲染
  createControl(); 
  updateRender(); //更新渲染
  window.addEventListener('resize', onWindowResize, false);
})

最後

附原始碼地址:https://github.com/chencld/th...

好久沒寫文章了,拖拖拉拉了半個月,終於寫完了,碼字不易,還請大家多多點贊,也歡迎討論區交流,謝謝~

相關文章