前言
今年過年由於疫情又沒回家,就抽空看了看WebGL程式設計指南和Three.js開發指南,為了練手,就簡單實現了一下冬奧開幕式上的迎客松煙花效果,純小白,這篇文章主要是記錄這次實踐過程,涉及的也都是threejs最基礎的內容,那我們就開始吧...
效果
效果圖如下,也可以點選連結預覽 https://awebgl.vercel.app/ 先是大門開啟的一個動作,接著機器人是threejs官網的一個例子,最後就是煙花飛上天空爆炸成迎客鬆的一個效果。
環境
用的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,完全可以把自己當成一個導演(偷笑),相機放在哪個位置、燈光在哪兒等等,不然就容易遇到滿屏黑,下面是我這個例子的簡圖,大家按照自己的習慣來構建就好。
場景和相機
首先需要建立一個場景,有了場景才能新增光照、圖元。
然後這個例子使用的是透視攝像機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條直線,實現效果和上面類似,大家可以直接看原始碼,我就不再介紹了。
//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...
好久沒寫文章了,拖拖拉拉了半個月,終於寫完了,碼字不易,還請大家多多點贊,也歡迎討論區交流,謝謝~