偶然翻滴滴官網看到首頁下翻第六欄(大概)有個絢麗的地球的三維動畫,試著用there.js實現了下,基本實現了動畫效果,不過還是有些問題;這個動畫看似簡單,但也用到好的繪製方法和計算,這裡先寫一下主要功能的實現;
先看示例:http://39.106.166.212:8080/webgl/t4(由於是寫dome的一個專案,內容較多沒做優化,第一次載入會會比較慢,需多等待幾秒)
(lice這個截圖工具也是很不好用,每次都截到一半 ╮(╯﹏╰)╭)
一、 3d繪製場景的構建
繪製一個3d程式首先需要新增 渲染器,場景,照相機 這些元素,這裡補充一個燈光;
1、渲染器
首先建立一個渲染器,引數為頁面中的canvas元素,
渲染器的作用就是把3d場景的內容結合照相機渲染到頁面中,
最後將畫布背景設為白色。
const renderer = new Three.WebGLRenderer({canvas: this.$refs.thr});
renderer.setClearColor(0x000000);
2、場景
場景顧名思義,就是新增一個你發揮(繪製)的場地,後面所有繪製的元素都要新增到場景中;
cosnt scene = new Three.Scene();
3、照相機
照相機就像人的視覺或說從什麼角度去看場景,看場景的位置和視線的方向決定了渲染到頁面的內容。故一般需要設定兩個引數相機位置position、視線方向lookAt,,在webgl其實是需要三組引數視點,觀察點,和上方向。thresjs中好像是預設Y軸為上方向了,這裡使用透視相機。
const camera = new THREE.PerspectiveCamera(45, 500 / 500, 1, 1500);
camera.position.set(100, 100, 1000);
camera.lookAt(new THREE.Vector3(0, 0, 0));
scene.add(this.camera);
4、燈光
這裡使用THREE.HemisphereLight光,可以更加貼近自然的戶外光照效;
let light = new THREE.HemisphereLight(0xffffff);
light.position.set(0, 0, 200);
scene.add(light)
以上我們基本的繪製要素已新增完成,下面開始繪製各個幾何體內容;
幾何體的繪製有三部:建立幾何體,建立材料,新增網格模型;
二、地球的繪製
threejs中提供了球體的繪製,我們只需建立一個球體,材料使用紋理貼圖方式新增地圖;
貼圖圖片資源是我從官網上找的
const geometry = new THREE.SphereGeometry(this.radius, 100, 100); // 球體
const textureLoader = new THREE.TextureLoader(); // 建立紋理貼圖
const texture = textureLoader.load(require("@/assets/map.jpg"),texture => {
let material = new THREE.MeshLambertMaterial({map: texture,transparent: true,});
let mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
});
由於圖片載入是非同步的 ,這裡需等圖片載入完成後才能建立材質;
這裡我們就建立好了一個地球模型,接著還要讓其轉動起來;(中間為xyz座標軸)
threejs提供了幾何體的基本3d變換,直接使用rotateY(angleChange)根據時間設定其繞y軸(綠色軸)旋轉角度即可;
三、球面座標點的繪製
1、在繪製球面位置點時,需先前先看下球座標系,確定點的位置,webgl中座標方向與下圖不一致,但是對點的表示方法是一致的;
球面上任意點可以用r,θ,φ表示,也可通過以下公式轉化到直角座標系中
x=rsinθcosφ.
y=rsinθsinφ.
z=rcosθ
但實際中地球位置我們一般也會使用經緯度表示。。。 下面寫個經緯度轉座標的方法。
threejs提供了THREE.Math.degToRad方法可以將經緯度轉化為θ,φ,轉化方法如下,這裡為了方便後面使用,我直接返回一個3維向量;
getPosition(longitude, latitude, radius = this.radius) { // 經度,緯度轉換為座標
let lg = THREE.Math.degToRad(longitude);
let lt = THREE.Math.degToRad(latitude); // 獲取x,y,z座標
let temp = radius * Math.cos(lt);
let x = temp * Math.sin(lg);
let y = radius * Math.sin(lt);
let z = temp * Math.cos(lg);
return new THREE.Vector3(x, y, z);
}
2、知道了位置的表示方法後開始繪製表示位置的點
根據示例位置點的由點和一個圓環組成,我們先繪製一個二維平面內的點和圓弧,在通過設定其位置和旋轉方式移動到目標座標位置點;
(這裡也可以繪製幾何小球體來模擬)
(1)點的繪製
THREE.Shape是用來繪製二維平面內的形狀的,設定其形狀為圓弧,即可實現一個原點;
let shapePoint = new THREE.Shape();
shapePoint.absarc(0, 0, r - 4, 0, 2 * Math.PI, false);
let arcGeometry = new THREE.ShapeGeometry(shapePoint);
let arcMaterial = new THREE.MeshBasicMaterial({ color: 0x008080 });
let point = new THREE.Mesh(arcGeometry, arcMaterial);
(2)圓弧的繪製
let geometryLine = new THREE.Geometry();
let arc = new THREE.ArcCurve(0, 0, r, 0, 2 * Math.PI);
let points = arc.getPoints(40);
geometryLine.setFromPoints(points);
let LineMateri = new THREE.LineBasicMaterial({ color: 0x20b2aa });
let line = new THREE.Line(geometryLine, LineMateri);
(3)位置的設定
position.set(pos.x, pos.y, pos.z);
(4)二維圖形旋轉至球面
THREE.Spherical()方法 ,可將座標點轉化為座標點轉化回球座標系的偏移角度;
let spherical = new THREE.Spherical();
spherical.setFromCartesianCoords(pos.x, pos.y, pos.z);
設定位置點旋轉
Point.rotateX(spherical.phi - Math.PI / 2);
Point.rotateY(spherical.theta);
這裡為什麼要 - Math.PI / 2 是因為開始我們繪製時,平面是垂直於y軸的平面,參考下面這幅圖;
四、接著繪製連結球面兩點間的連線
連線兩點的曲線需在球面上方,
兩點間可以坐出無數條曲線,那麼如何確定曲線,我們需自己再選擇合適的引數來確定;
首先想的是二階貝塞爾曲線,取兩點的中點為控制點,但如果連結兩點剛好位於球面的兩個對稱端點(兩點間連線為直徑)時,控制點需在無窮遠處;
故考慮使用三階貝塞爾曲線,連線球面兩點和球心,三點確定的一個平面如下圖,
連結v1 v1,取中點c,連結oc,做一條射線,在射線取一點p,連結v1p,v2p,在v1,v2上取兩點作為控制點;
求兩點的中點
getVCenter(v1, v2) {
let v = v1.add(v2);
return v.divideScalar(2);
}
獲取兩點間指定比例位置座標
getLenVcetor(v1, v2, len) {
let v1v2Len = v1.distanceTo(v2);
return v1.lerp(v2, len / v1v2Len);
}
獲取貝塞爾控制點
getBezierPoint(v0, v3) {
let angle = (v0.angleTo(v3) * 180) / Math.PI; // 0 ~ Math.PI // 計算向量夾角
let aLen = angle * 2.5,
hLen = angle * angle * 50;
let p0 = new THREE.Vector3(0, 0, 0); // 法線向量
let rayLine = new THREE.Ray(p0, this.getVCenter(v0.clone(), v3.clone())); // 頂點座標
let vtop = rayLine.at(hLen / rayLine.at(1).distanceTo(p0), vtop); // 位置
// 控制點座標
let v1 = this.getLenVcetor(v0.clone(), vtop, aLen);
let v2 = this.getLenVcetor(v3.clone(), vtop, aLen);
return {
v1: v1,
v2: v2
};
},
繪製三次貝塞爾曲線
drawLine(longitude, latitude, longitude2, latitude2) {
let geometry = new THREE.Geometry(); //宣告一個幾何體物件Geometry
let v0 = this.getPosition(longitude, latitude, this.radius);
let v3 = this.getPosition(longitude2, latitude2, this.radius);
let { v1, v2 } = this.getBezierPoint(v0, v3); // 三維二次貝賽爾曲線
let curve = new THREE.CubicBezierCurve3(v0, v1, v2, v3);
let curvePoints = curve.getPoints(100);
geometry.setFromPoints(curvePoints);
let material = new THREE.LineBasicMaterial({
color: 0xff7e41
});
let line = new THREE.Line(geometry, material);
this.group.add(line);
this.sport(curvePoints);
},
五、小球的運動軌跡
小球的動畫我們使用three的幀動畫,路徑可以直接使用上一步中的曲線;
1、繪製小球
drawSportPoint(position, name) {
let box = new THREE.SphereGeometry(6, 6, 6);
let material = new THREE.MeshLambertMaterial({
color: 0x00bfff
}); //材質物件
let mesh = new THREE.Mesh(box, material);
mesh.name = name;
mesh.position.set(position.x, position.y, position.z);
this.groupBall.add(mesh);
this.group.add(this.groupBall);
return mesh;
}
2、讓小球動起來
sport(curvePoints, index) {
const Ball = this.drawSportPoint(curvePoints[0]);
let arr = Array.from(Array(101), (v, k) => k); // 生成一個時間序列
let times = new Float32Array(arr);
let posArr = [];
curvePoints.forEach(elem => {
posArr.push(elem.x, elem.y, elem.z);
}); // 建立一個和時間序列相對應的位置座標系列
let values = new Float32Array(posArr); // 建立一個幀動畫的關鍵幀資料,曲線上的位置序列對應一個時間序列
let posTrack = new THREE.KeyframeTrack("Ball.position", times, values);
let duration = 101;
let clip = new THREE.AnimationClip("default", duration, [posTrack]);
this.mixer = new THREE.AnimationMixer(Ball);
let AnimationAction = this.mixer.clipAction(clip);
AnimationAction.timeScale = 20;
AnimationAction.play();
}
3、在requestAnimationFrame中新增觸發動畫
this.mixer.update(this.clock.getDelta());