使用ArcGIS API和Three.js在三維場景中實(shí)現(xiàn)動(dòng)態(tài)立體墻效果
廢話不多說,直接先來看下最終實(shí)現(xiàn)的動(dòng)態(tài)立體墻效果圖。

如果圖片還不夠直觀,那么點(diǎn)擊鏈接查看在線示例。
首先我們需要用到ArcGIS API中的externalRenderers類將外部的Three.js渲染器加載到地圖三維場景中,如果不知道怎么使用的可以查看我的這篇文章《ArcGIS API在視圖中渲染Three.js場景》。那篇文章中加載的是一個(gè)三維模型,而本示例中只需加載一面“墻”,也就是一個(gè)平面,并增加一個(gè)動(dòng)態(tài)效果。所以重點(diǎn)就是怎么加載一個(gè)垂直于地球表面的平面,以及如何實(shí)現(xiàn)動(dòng)態(tài)效果。
1 垂直于地球表面的墻
如圖所示,先確定出兩個(gè)“墻角”的坐標(biāo)。

let points = [
[104.06179498614645, 30.659871702738265], // 坐標(biāo)1
[104.06494384459816, 30.659931252383917], // 坐標(biāo)2
];
現(xiàn)在我們有了兩個(gè)經(jīng)緯度坐標(biāo)的點(diǎn),但是我們需要4個(gè)頂點(diǎn)才能構(gòu)成一個(gè)矩形面,所以我們還需要2個(gè)點(diǎn)。假設(shè)2個(gè)墻角坐標(biāo)貼近于地面,那么它們的高度就為0,那就再只需要2個(gè)同樣經(jīng)緯度坐標(biāo)但高度大于0的點(diǎn)就能構(gòu)成一個(gè)在地面上并且垂直于地面的矩形面了。所以在我們定義的myRenderer對(duì)象中添加一個(gè)height屬性。
let myRenderer = {
// ... 其它屬性
height: 100, // 墻的高度
// ... 其它屬性、方法
};
現(xiàn)在我們有4個(gè)由經(jīng)緯度加高度構(gòu)成的點(diǎn),如果要在視圖中渲染成一個(gè)矩形面,我們要先將這4個(gè)點(diǎn)轉(zhuǎn)換為渲染坐標(biāo)系中的點(diǎn),再將每3個(gè)頂點(diǎn)為一組構(gòu)成一個(gè)三角面,最后由2個(gè)三角面構(gòu)成一個(gè)矩形面。這樣做是因?yàn)樵赥hree.js中所有的模型都是由頂點(diǎn)加三角面構(gòu)成的。
1.1 頂點(diǎn)轉(zhuǎn)換
在頂點(diǎn)轉(zhuǎn)換之前,我們還需要做一個(gè)操作,那就是將我們的經(jīng)緯度坐標(biāo)轉(zhuǎn)換為XY坐標(biāo)。這需要用到ArcGIS API中的webMercatorUtils工具中的lngLatToXY方法,該方法將給定的經(jīng)度和緯度轉(zhuǎn)換為Web Mercator的XY值。
points.forEach((point) => {
// 將經(jīng)緯度坐標(biāo)轉(zhuǎn)換為xy值
let pointXY = webMercatorUtils.lngLatToXY(point[0], point[1]);
});
然后需要用到Three.js的數(shù)學(xué)庫中的四維矩陣Matrix4類以及ArcGIS API中externalRenderers對(duì)象上的renderCoordinateTransformAt方法將點(diǎn)轉(zhuǎn)換為渲染坐標(biāo)系中的點(diǎn)坐標(biāo)。
let transform = new THREE.Matrix4(); // 變換矩陣
let transformation = new Array(16);
let vector3List = []; // 頂點(diǎn)數(shù)組
points.forEach((point) => {
// 將經(jīng)緯度坐標(biāo)轉(zhuǎn)換為xy值
let pointXY = webMercatorUtils.lngLatToXY(point[0], point[1]);
// 先轉(zhuǎn)換高度為0的點(diǎn)
transform.fromArray(
externalRenderers.renderCoordinateTransformAt(
this.view,
[pointXY[0], pointXY[1], 0], // 坐標(biāo)在地面上的點(diǎn)[x值, y值, 高度值]
this.view.spatialReference,
transformation
)
);
vector3List.push(
new THREE.Vector3(
transform.elements[12],
transform.elements[13],
transform.elements[14]
)
);
// 再轉(zhuǎn)換距離地面高度為height的點(diǎn)
transform.fromArray(
externalRenderers.renderCoordinateTransformAt(
this.view,
[pointXY[0], pointXY[1], this.height], // 坐標(biāo)在空中的點(diǎn)[x值, y值, 高度值]
this.view.spatialReference,
transformation
)
);
vector3List.push(
new THREE.Vector3(
transform.elements[12],
transform.elements[13],
transform.elements[14]
)
);
});
renderCoordinateTransformAt方法的作用是計(jì)算一個(gè)4x4變換矩陣,該矩陣構(gòu)成從局部笛卡爾坐標(biāo)系到虛擬世界坐標(biāo)系的線性坐標(biāo)變換。該方法傳入4個(gè)參數(shù):
1 view,ArcGIS API生成的三維視圖。
2 origin,局部笛卡爾坐標(biāo)系中原點(diǎn)的全局坐標(biāo),也就是[經(jīng)緯度轉(zhuǎn)換后的X坐標(biāo), 經(jīng)緯度轉(zhuǎn)換后的y坐標(biāo), 高度值]。
3 srcSpatialReference,原點(diǎn)坐標(biāo)的空間參考。
4 dest,存儲(chǔ)16個(gè)矩陣元素的數(shù)組的引用。生成的矩陣遵循OpenGL約定,其中轉(zhuǎn)換組件占據(jù)第13、14和第15個(gè)元素。
現(xiàn)在,vector3List變量中存儲(chǔ)的就是每個(gè)頂點(diǎn)轉(zhuǎn)換后的三維向量,一共為4個(gè)頂點(diǎn)。順序是[第一個(gè)經(jīng)緯度的地面頂點(diǎn), 第一個(gè)經(jīng)緯度的空中頂點(diǎn), 第二個(gè)經(jīng)緯度的地面頂點(diǎn), 第二個(gè)經(jīng)緯度的空中頂點(diǎn)],這個(gè)頂點(diǎn)的順序很重要,后面會(huì)用到。
1.2 生成三角面以及面的UV隊(duì)列
因?yàn)門hree.js中的面都是由小三角面構(gòu)成的,所以我們需要根據(jù)頂點(diǎn)列表中的頂點(diǎn)來組成三角面,每三個(gè)頂點(diǎn)構(gòu)成一個(gè)三角面,一定要注意構(gòu)成三角面的的頂點(diǎn)順序,因?yàn)橐兔娴腢V隊(duì)列一一對(duì)應(yīng)起來,這樣給每個(gè)面貼的紋理材質(zhì)才能正確顯示出來。
紋理貼圖的坐標(biāo)系統(tǒng)是這樣的:圖片左下角為原點(diǎn)(0, 0),右下角為(1, 0),右上角為(1, 1),左上角為(0, 1),這和圖片的大小寬高無關(guān)。如下圖所示:

將紋理坐標(biāo)關(guān)系轉(zhuǎn)換為二維向量表示。
const t0 = new THREE.Vector2(0, 0); // 圖片左下角
const t1 = new THREE.Vector2(1, 0); // 圖片右下角
const t2 = new THREE.Vector2(1, 1); // 圖片右上角
const t3 = new THREE.Vector2(0, 1); // 圖片左上角
一個(gè)簡單的矩形面由4個(gè)頂點(diǎn)和2個(gè)小三角面構(gòu)成,頂點(diǎn)和三角面關(guān)系如下圖所示:

圖中0、1、2、3序號(hào)代表
vector3List變量中頂點(diǎn)的順序。按照逆時(shí)針規(guī)則畫出2個(gè)三角面,下三角面為綠色三角面[0, 2, 1],上三角面為藍(lán)色三角面[1, 2, 3]。例如,要將紋理貼圖和綠色三角面映射起來,那么綠色三角面對(duì)應(yīng)的UV就是[t0, t1, t3],藍(lán)色三角面對(duì)應(yīng)的UV就是[t3, t1, t2]。
根據(jù)以上原理生成三角面列表以及UV隊(duì)列。
let faceList = []; // 三角面數(shù)組
let faceVertexUvs = []; // 面的 UV 層的隊(duì)列,該隊(duì)列用于將紋理和幾何信息進(jìn)行映射
for (let i = 0; i < vector3List.length - 2; i++) {
if (i % 2 === 0) { // 下三角面
faceList.push(new THREE.Face3(i, i + 2, i + 1));
faceVertexUvs.push([t0, t1, t3]);
} else { // 上三角面
faceList.push(new THREE.Face3(i, i + 1, i + 2));
faceVertexUvs.push([t3, t1, t2]);
}
}
1.3 生成幾何體
使用Three.js中的Geometry構(gòu)造函數(shù)來生成自定義幾何體。
const geometry = new THREE.Geometry(); // 生成幾何體
geometry.vertices = vector3List; // 幾何體頂點(diǎn)
geometry.faces = faceList; // 幾何體三角面
geometry.faceVertexUvs[0] = faceVertexUvs; // 面的UV隊(duì)列,用于將紋理信息映射到幾何體上
geometry.faceVertexUvs的屬性值為數(shù)組是因?yàn)橛卸嘟MUV。顏色貼圖、法線貼圖、高光貼圖、金屬度貼圖等共用一組紋理坐標(biāo)UV即geometry.faceVertexUvs[0],設(shè)置陰影的光照貼圖lightMap使用另外一組紋理坐標(biāo),也就是geometry.faceVertexUvs[1]。默認(rèn)情況下,geometry.faceVertexUvs屬性中會(huì)存在一個(gè)元素,所以可以直接對(duì)geometry.faceVertexUvs[0]進(jìn)行賦值操作。
注意:對(duì)于緩沖區(qū)類型幾何體也就是通過BufferGeometry構(gòu)造函數(shù)生成的幾何體,是通過設(shè)置.attributes.uv和.attributes.uv2兩個(gè)屬性分別定義兩組頂點(diǎn)紋理坐標(biāo)。
2 實(shí)現(xiàn)墻的動(dòng)態(tài)效果
動(dòng)態(tài)效果的原理其實(shí)是紋理貼圖實(shí)現(xiàn)的,一共兩層貼圖,一層顏色從上到下越來越不透明,給人一面墻的感覺,另一層從上到下越來越透明,然后每次渲染都改變第二層紋理在垂直方向上的偏移量,這樣就有了滾動(dòng)起來的效果。
因?yàn)楫?dāng)?shù)谝粚影胪该骱偷诙影胪该鞯男Ч集B加到一個(gè)幾何體上時(shí),這個(gè)幾何體就會(huì)變得更加的透明,顯示效果上就不是很好,所以我們把這兩層效果放到兩個(gè)幾何體上,只需要把上面創(chuàng)建好的幾何體克隆一遍。
const geometry2 = geometry.clone();
2.1 利用材質(zhì)的alphaMap貼圖實(shí)現(xiàn)半透明效果
我們選用基礎(chǔ)網(wǎng)絡(luò)材質(zhì)MeshBasicMaterial,該材質(zhì)不受光照的影響,所以不需要在場景中再額外的添加光源,省時(shí)省力~。該材質(zhì)對(duì)象上的alphaMap貼圖屬性可以用來控制整個(gè)表面的不透明度,黑色完全透明,白色完全不透明。如下圖所示,從上到下越來越白,也就是也來越不透明。

加載alphaMap的紋理貼圖資源,創(chuàng)建材質(zhì),和第一個(gè)幾何體生成網(wǎng)格,然后添加到場景中。
this.alphaMap = new THREE.TextureLoader().load( // 加載alpha貼圖資源
'../images/texture_1.png'
);
// 創(chuàng)建材質(zhì)
const material = new THREE.MeshBasicMaterial({
color: 0xff0000,
side: THREE.DoubleSide,
transparent: true, // 必須設(shè)置為true,alphaMap才有效果
depthWrite: false, // 渲染此材質(zhì)是否對(duì)深度緩沖區(qū)有任何影響
alphaMap: this.alphaMap, // alpha貼圖,控制透明度
});
const mesh = new THREE.Mesh(geometry, material); // 第一個(gè)幾何體和第一個(gè)材質(zhì)
this.scene.add(mesh);
注意:
side屬性要設(shè)置為THREE.DoubleSide,這樣才能兩個(gè)面都進(jìn)行繪制,也就是說從前后兩個(gè)方向都能看到幾何體。
transparent屬性一定要設(shè)置為true,不然alphaMap貼圖是沒有效果的,看不出透明效果。
depthWrite屬性一定要設(shè)置為false,才能正確渲染后方的半透明物體。
效果如圖所示:

2.2 利用材質(zhì)的map顏色貼圖實(shí)現(xiàn)漸變半透明效果
MeshBasicMaterial材質(zhì)的map屬性為顏色貼圖??梢栽O(shè)置為半透明的PNG格式圖片,也就達(dá)到了透明的效果。

加載PNG格式的紋理貼圖資源,創(chuàng)建材質(zhì),和克隆出來的幾何體生成網(wǎng)格,然后添加到場景中。
this.texture = new THREE.TextureLoader().load(
'../images/texture_2.png'
);
this.texture.wrapS = THREE.RepeatWrapping; // 水平方向重復(fù)
this.texture.wrapT = THREE.RepeatWrapping; // 垂直方向重復(fù)
const material2 = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide,
transparent: true,
depthWrite: false, // 渲染此材質(zhì)是否對(duì)深度緩沖區(qū)有任何影響
map: this.texture, // 顏色貼圖,加載PNG圖片達(dá)到透明效果
});
const mesh2 = new THREE.Mesh(geometry2, material2);
this.scene.add(mesh2);
注意:
因?yàn)樾枰诖怪狈较蛏洗嬖谄屏?,形成滾動(dòng)的效果,所以必須設(shè)置紋理的包裹方式為重復(fù),wrapS和wrapT屬性設(shè)置為THREE.RepeatWrapping。具體可查看文檔。
疊加到場景中的效果如圖所示:

2.3 效果動(dòng)起來
現(xiàn)在大體效果已經(jīng)差不多了,最后只要?jiǎng)悠饋砭屯旯ち?。要?shí)現(xiàn)動(dòng)起來的效果只需要在render函數(shù)中添加更新紋理貼圖偏移量的代碼,每渲染一次就更新一次偏移量。
render() {
// ... 其它代碼
if (this.offset <= 0) {
this.offset = 1;
} else {
this.offset -= 0.02; // 每次渲染就向上移動(dòng)0.02個(gè)單位,如果想要速度快就增大該值
}
if (this.texture) {
this.texture.offset.set(0, this.offset); // 水平偏移量0,垂直方向偏移量為offset
}
// ... 其它代碼
}
texture對(duì)象上存在offset屬性,該屬性值類型為二維向量Vector2,用來設(shè)置水平和垂直方向上的偏移量,值的范圍在0.0到1.0之間。
最終效果如圖所示:

3 總結(jié)
至此,我們的立體動(dòng)態(tài)墻效果就已經(jīng)實(shí)現(xiàn)了。重要點(diǎn)就是通過頂點(diǎn)向量加三角面構(gòu)成自定義的平面矩形幾何體,通過設(shè)置紋理貼圖以及改變紋理貼圖的偏移量來實(shí)現(xiàn)動(dòng)起來的效果。