國慶獻禮之ThreeJs實現3D上海外灘全景

國慶獻禮之ThreeJs實現3D上海外灘全景

  • 最近入了three.js的坑,想用three.js作一些demo以便鞏固本身最近所掌握的知識點,並且正好遇上國慶放假,隨,有了這篇~html

  • 預覽地址: Three.js之上海外灘git

    Tip1: 打開後瀏覽器一直轉圈 建築物的貼圖不顯示 是網絡問題 等一下子就好 畢竟是github... Tip2: 若是打開後幀數太低 比較卡的話,能夠調整代碼中的SHADOW_MAPSIZE大小 經過調整鋸齒感來優化性能github

  • 代碼地址: Three.js之上海外灘web

  • 以爲不錯的話麻煩點個star 謝謝 👏算法


本篇雖是關於Three.js入門的文章, 可是太過入門的就不講了,沒意義,網上不少相關入門知識。本篇主要是把本身在寫demo時候遇到的坑點給記錄下來, 有什麼不懂的直接去查閱文檔或者網上搜,這裏提一下:Three.js的官方文檔和例子對開發者也是挺友好的(有中文版!)api


廢話很少, 先看下效果吧:數組

代碼比較多,就不一一講解了,本篇主要分爲如下幾個部分:瀏覽器

  1. 初始化代碼,搭建不規則的地面幾何體等「知識點:利用THREE.ShapeTHREE.ExtrudeGeometry建立不規則的幾何體」
  2. 搭建東方明珠「知識點:利用多種幾何體組合THREE.Object3D()建立不規則的幾何體」
  3. 搭建上海中心大廈「知識點:利用正弦函數Math.sin規律性的改變幾何體的頂點vertices建立不規則的幾何體」
  4. 搭建環球金融中心「知識點:利用手寫頂點數組vertices和三角形面數組faces的方式建立不規則的幾何體」
  5. 搭建金茂大廈「知識點:利用多種幾何體組合THREE.Object3D()建立不規則的幾何體」
  6. 隨機算法搭建其餘建築物「知識點:Math.random隨機生成幾何體」
  7. 給全部建築物進行貼圖優化 「知識點:THREE.TextureLoader的運用」
  8. 搭建黃浦江 「知識點:requestAnimationFrame動畫方法的運用」
  9. 搭建360全景空間 「知識點:THREE.CubeTextureLoader建立360度全景空間」

初始化代碼,建立場景,相機,渲染器,燈光,搭建不規則的地面幾何體等

  • main.js:
/** 材質顏色常量 */
const MATERIAL_COLOR = "rgb(120, 120, 120)";

init();

function init() {
  // 1.場景
  let scene = new THREE.Scene();

  let stats = new Stats();
  document.body.appendChild(stats.dom);

  let clock = new THREE.Clock();

  let gui = new dat.GUI();

  // 座標軸輔助器
  let axesHelper = new THREE.AxesHelper(500);
  // 網格輔助器
  let gridHelper = new THREE.GridHelper(100, 100);
  scene.add(axesHelper);
  scene.add(gridHelper);

  //經過Shape生成一個不規則等2D圖形
  // 地面1
  let ground1 = getGroundFront();
  // 地面2
  let ground2 = getGroundBehind();

  scene.add(ground1);
  scene.add(ground2);

  // 光源
  let spotLight = getSpotLight(1.2);
  spotLight.position.set(100, 100, 80);
  scene.add(spotLight);

  // 相機
  let camera = new THREE.PerspectiveCamera(
    45,
    window.innerWidth / window.innerHeight,
    1,
    1000
  );
  camera.position.set(0, 30, 90);
  camera.lookAt(new THREE.Vector3(0, 0, 0));

  // 3.渲染器
  let renderer = new THREE.WebGLRenderer();
  renderer.setClearColor(MATERIAL_COLOR);
  renderer.shadowMap.enabled = true; // 開啓渲染器的陰影功能
  renderer.shadowMap.type = THREE.PCFShadowMap; // PCF陰影類型
  renderer.setSize(window.innerWidth, window.innerHeight);

  document.getElementById("webgl").appendChild(renderer.domElement);

  // 相機軌道控制器
  let controls = new THREE.OrbitControls(camera, renderer.domElement);

  update(renderer, scene, camera, controls, stats);
}

function getSpotLight(intensity) {
// 生成光源
  let light = new THREE.PointLight(0xffffff, intensity);
  light.castShadow = true;
  light.receiveShadow = true;

  light.shadow.bias = 0.001;
  light.shadow.mapSize.width = 2048;
  light.shadow.mapSize.height = 2048;

  return light;
}

function getGroundBehind() {
// 地面2 後半部分
  let shape = new THREE.Shape();
  shape.moveTo(45, 100); // moveTo( x, y )
  shape.lineTo(50, 100); // lineTo( x, y ) - 線
  shape.lineTo(50, 0); // lineTo( x, y ) - 線
  shape.lineTo(-50, 0); // lineTo( x, y ) - 線
  shape.lineTo(-50, 60); // lineTo( x, y ) - 線
// 貝塞爾曲線
  shape.bezierCurveTo(5, 15, 15, 5, 45, 100);

  let extrudeGeometry = new THREE.ExtrudeGeometry(shape, {
    depth: 3,
    steps: 2,
    bevelThickness: 0,
    bevelSize: 1
  });

  let material = new THREE.MeshLambertMaterial({ color: "gray" });

  let mesh = new THREE.Mesh(extrudeGeometry, material);

  mesh.receiveShadow = true;
  mesh.rotation.x = Math.PI + Math.PI / 2; // 地面旋轉180度
  mesh.rotation.y = Math.PI; // 地面旋轉180度

  mesh.position.set(0, 0, 50);
  return mesh;
}

function getGroundFront() {
// 地面1 前半部分
  let shape = new THREE.Shape();
  shape.moveTo(50, 0); // moveTo( x, y )
  shape.lineTo(-25, 0); // lineTo( x, y ) - 線
  shape.quadraticCurveTo(-10, 107, 50, 15); // 二次曲線

  let extrudeGeometry = new THREE.ExtrudeGeometry(shape, {
    depth: 3,
    steps: 2,
    bevelThickness: 0,
    bevelSize: 1
  });

  let material = new THREE.MeshLambertMaterial({ color: "#666" });

  let mesh = new THREE.Mesh(extrudeGeometry, material);

  mesh.receiveShadow = true;
  mesh.rotation.x = Math.PI / 2; // 地面旋轉90度
  mesh.position.set(0, 0, -50);
  return mesh;
}

function update(renderer, scene, camera, controls, stats) {
  renderer.render(scene, camera);

  // 性能監控
  stats.update();

  // 相機軌道控制器
  controls.update();
  renderer.render(scene, camera);
  requestAnimationFrame(function() {
    update(renderer, scene, camera, controls, stats);
  });
}

複製代碼

核心代碼在main.js中,因此只貼出main.js的相關代碼, 其他還有index.html和第三方庫引用文件等能夠去github上查看bash

該部分沒有作什麼比較核心的地方,只是一些場景 相機 渲染器 畫輔助線 性能監控 等的初始化,但花費時間稍長的是畫出兩個不規則地面,留出一道「黃浦江」的位置。網絡

咱們知道,three.js的基礎API只能畫出規則網格(幾何體),像什麼方體 錐體 球體等等.. 像這種自定義的網格(幾何體)咱們能夠用THREE.Shap這個API。

function getGroundFront() {
  let shape = new THREE.Shape(); // 1.畫一個二維面 Shape
  shape.moveTo(50, 0); // 2. 將.currentPoint 移動到 x, y
  shape.lineTo(-25, 0); // 3. 在當前路徑上,從.currentPoint 鏈接一條直線到 x,y。
  shape.quadraticCurveTo(-10, 107, 50, 15); // 4. 從.currentPoint 建立一條二次曲線,以(cpX,cpY)做爲控制點,並將.currentPoint 更新到 x,y。

  let extrudeGeometry = new THREE.ExtrudeGeometry(shape, {
    depth: 3,
    steps: 2,
    bevelThickness: 0,
    bevelSize: 1
  });// 5. 擠壓幾何體 ExtrudeGeometry

  let material = new THREE.MeshLambertMaterial({ color: "#666" });

  let mesh = new THREE.Mesh(extrudeGeometry, material);

  mesh.receiveShadow = true; // 接收陰影
  mesh.rotation.x = Math.PI / 2; // 地面旋轉90度
  mesh.position.set(0, 0, -50); // 改變位置
  return mesh;
}

複製代碼

其實說白就了就是 首先經過 THREE.Shap 對象,建立一個二維的圖形,而後再經過 ExtrudeGeometry 將其拉伸爲一個三維圖形。這是前半部分的地面幾何體,後半部分也是同樣的。 其餘的具體參數信息就查閱文檔:ShapeExtrudeGeometry

搭建東方明珠

以座標(0, 0, 0)爲原點建立東方明珠。其實這個還蠻簡單的,不須要你本身畫出不規則的網格,利用Three.js相關基礎的API就能夠作到,對其進行組合排列就行了。可是相關代碼量是比較多的,這裏就不貼出了,能夠去github上翻閱 首先,咱們能夠觀察下東方明珠,不難發現,它都是有一些常見的幾何體組合而成,好比圓柱,球體,圓環等等。咱們能夠將整個東方明珠分爲三個部分,底部,中部,頂部。 首先底部由兩個疊加的圓臺(其實就是高度很小的圓柱),加上3個豎直的圓柱和3個傾斜的圓柱組成。 中部就簡單多了,兩個球,加上中間整齊排列的圓環組成。 頂部也不難,3個半徑不一樣球體和3個半徑不一樣的圓柱組成。

搭建上海中心大廈

這個當時也是花費了一些時間的。 因爲上海中心大廈是個極其不規則的幾何體,用Three.js實現仍是略微麻煩一些的,最終的實現效果也並非特別理想,只是略像而已。

function getShanghaiTower() {
// 1. 經過 THREE.CylinderGeometry 生成一個圓柱體 注意參數
  let _geometry = new THREE.CylinderGeometry(2, 3, 18, 7, 50);
// 2. 操做該圓柱的頂點, 經過正弦函數規律性的變化 使其網格發生變化
  _geometry.vertices.forEach((vertex, ind) => {
    // 正弦函數規律性的改變頂點座標的x軸和z軸
    vertex.z = vertex.z + Math.sin((vertex.y + ind) * 0.015);
    vertex.x = vertex.x + Math.sin((vertex.y + ind) * 0.01) * 1;
    if (vertex.y >= 8.5) {
      // 3. 這裏作了一個斜塔尖 
      vertex.y -= vertex.x * 0.2;
    }
  });
// 4. 改變頂點後別忘記了讓網格的verticesNeedUpdate等於true
  _geometry.verticesNeedUpdate = true;

  let _material = new THREE.MeshPhongMaterial({
    color: "rgb(120, 120, 120)"
    // wireframe: true
  });
  let tower = new THREE.Mesh(_geometry, _material);
  tower.position.set(10, 17, -8); // 位置
  tower.scale.set(1, 2, 0.5); // 縮放

  return tower;
}

複製代碼

首先經過 THREE.CylinderGeometry 生成一個圓柱體 注意參數,上半徑和下半徑不一樣。 經過網格.vertices,獲取該幾何體的全部頂點,經過規律性的改變每一個頂點的x和z座標,從而實現改變最終幾何體的目的。可是最終別忘了要手動設置網格.verticesNeedUpdate = true,若是不設置的話,Three.js默認是不會改變其頂點的。

搭建環球金融中心

能夠看到, 環球金融中心也是一個不規則的網格幾何體。 能夠將其分爲兩部分,底部不規則網格和頂部不規則網格。此處實現不規則網格幾何體的話咱們就要經過手寫頂點座標vertices和手寫每一個面的三角形faces來實現自定義的不規則幾何體。

底部不規則網格:

頂部不規則網格:

底部代碼:

getGlobalFinancialCenterBottom方法:

function getGlobalFinancialCenterBottom() {
// 1. 手寫幾何體的每一個頂點座標 
  let vertices = [
    // 底部
    new THREE.Vector3(3, 0, 3), // 下標0
    new THREE.Vector3(3, 0, -3), // 下標1
    new THREE.Vector3(-3, 0, 3), // 下標2
    new THREE.Vector3(-3, 0, -3), // 下標3
    // 中部
    new THREE.Vector3(3, 10, 3), // 下標4
    new THREE.Vector3(-3, 10, -3), // 下標5
    // 上部
    new THREE.Vector3(-1.5, 30, 3), // 下標6
    new THREE.Vector3(3, 30, -1.5), // 下標7
    new THREE.Vector3(3, 30, -3), // 下標8
    new THREE.Vector3(1.5, 30, -3), // 下標9
    new THREE.Vector3(-3, 30, 1.5), // 下標10
    new THREE.Vector3(-3, 30, 3) // 下標11
  ]; //頂點座標,一共8個頂點

  let faces = [
    // 底部2個三角形
    new THREE.Face3(0, 1, 2),
    new THREE.Face3(3, 2, 1),
    // 每一個面的 3個三角形
    // 1.
    new THREE.Face3(6, 2, 0),
    new THREE.Face3(0, 4, 6),
    new THREE.Face3(11, 2, 6),
    // 2.
    new THREE.Face3(0, 1, 7),
    new THREE.Face3(7, 4, 0),
    new THREE.Face3(8, 7, 1),
    // 3.
    new THREE.Face3(1, 3, 9),
    new THREE.Face3(9, 8, 1),
    new THREE.Face3(3, 5, 9),
    // 4.
    new THREE.Face3(10, 3, 2),
    new THREE.Face3(11, 10, 2),
    new THREE.Face3(10, 5, 3),
    // 頂部4個三角形
    new THREE.Face3(6, 10, 11),
    new THREE.Face3(7, 8, 9),
    new THREE.Face3(6, 7, 10),
    new THREE.Face3(7, 9, 10),
    // 兩個剖面 三角形
    new THREE.Face3(7, 6, 4),
    new THREE.Face3(10, 9, 5)
  ]; //頂點索引,每個面都會根據頂點索引的順序去繪製線條
  let globalGeometry_bottom = new THREE.Geometry();
  globalGeometry_bottom.vertices = vertices;
  globalGeometry_bottom.faces = faces;
  globalGeometry_bottom.computeFaceNormals(); //計算法向量,會對光照產生影響
  let _material = new THREE.MeshPhongMaterial({
    color: "rgb(120, 120, 120)"
    // wireframe: true
  });
  let globalFinancialCenter = new THREE.Mesh(globalGeometry_bottom, _material);

  return globalFinancialCenter;
}

複製代碼

咱們知道,每3個點能夠肯定一個三角形面,其實說白了,在Three.js中的每一個幾何體,都是有n個三角形面組合而成的。 個人理解是 頂點 --> 三角形面 --> 幾何體面 --> 幾何體

首先定義來一個不規則幾何體的頂點數組vertices,裏面都是幾何體每一個頂點的座標。 此處注意:若是繪製的面是朝向相機的,那這個面的頂點的書寫方式是逆時針繪製的,好比圖上模型的第一個面的添加里面書寫的是(0,1,2)。不然,你看到的幾何體的那個面就是透明的 數組的頂點座標排序無所謂,可是數組的下標很重要,由於要對應到每一個三角形面faces ok,緊接着又定義了一個三角形面的數組faces,裏面每個元素都是一個三角形,而每一個元素裏面又經過 new THREE.Face3(0, 1, 2)來肯定一個三角形面, 其中的(0, 1, 2)是咱們剛纔定義的頂點數組的下標,注意:必定是頂點數組下標。 好比(0, 1, 2) 對應的就是上面定義的頂點數組中的new THREE.Vector3(3, 0, 3), new THREE.Vector3(3, 0, -3) 和 new THREE.Vector3(-3, 0, 3)。可能有人疑惑,定義三角形面的時候,爲何不直接經過寫頂點座標而非要寫頂點座標的下標呢?那樣不是更方便更直觀嗎? 其實仔細想一下不難發現,若是經過直接寫頂點座標來定義三角形的話,那麼三角形面的三個頂點確定會與其餘三角形面的某個頂點重合,簡單的幾何體還好,若是是複雜的幾何體,重複的頂點座標會極大的形成性能浪費。因此Three.js纔會經過頂點數組的下標來肯定每一個三角形面。

這個是底部不規則幾何體的代碼, 頂部實現方式同樣的,也是經過定義頂點數組和三角形面的方式來自定義幾何體。 只不過頂部的幾何體稍微複雜一些,是兩個對稱的不規則幾何體加上一個面THREE.PlaneGeometry組成。 環球金融中心的具體代碼請查閱github。 在建立不規則幾何體每一個頂點的時候,若是實在想不出每一個頂點的空間位置的話,推薦在稿紙上先畫出個大概的草圖,創建座標軸來進行標註。如下是我當時畫的草圖,不是很精確,只是草圖,供參考:

好比上圖,肯定好每一個頂點的額座標以後,再根據每3個頂點座標寫出每一個三角形面。

搭建金茂大廈

// 金茂大廈
function getJinmaoTower(x, y, z) {
  let JinmaoTower = new THREE.Object3D();
  let _geometry = new THREE.BoxGeometry(1, 22, 6);
  let _material = new THREE.MeshPhongMaterial({
    color: "rgb(120, 120, 120)"
  });

  // 金茂大廈中間骨架
  var cube1 = new THREE.Mesh(_geometry, _material);
  var cube2 = new THREE.Mesh(_geometry, _material);
  cube2.rotation.set(0, Math.PI / 2, 0);

  // 金茂大廈主幹
  let towerBody = getJinmaoTowerBody();
  // 金茂大廈頂部主體
  let towerTop = getJinmaoTowerTop();

  JinmaoTower.add(cube1);
  JinmaoTower.add(cube2);
  JinmaoTower.add(towerBody);
  JinmaoTower.add(towerTop);

  JinmaoTower.position.set(x, y, z);
  return JinmaoTower;
}

// 金茂大廈頂部主體
function getJinmaoTowerTop() {
  let towerTop = new THREE.Object3D();
  let _geometry1 = new THREE.BoxGeometry(3.8, 0.5, 3.8);
  let _geometry2 = new THREE.BoxGeometry(3, 0.5, 3);
  let _geometry3 = new THREE.BoxGeometry(2.2, 0.5, 2.2);
  let _geometry4 = new THREE.BoxGeometry(1.4, 0.5, 1.4);
  let _cylinderGeometry = new THREE.CylinderGeometry(0.1, 0.5, 5, 3);

  let _material = new THREE.MeshPhongMaterial({
    color: "rgb(120, 120, 120)"
  });

  let cube1 = new THREE.Mesh(_geometry1, _material);
  let cube2 = new THREE.Mesh(_geometry2, _material);
  let cube3 = new THREE.Mesh(_geometry3, _material);
  let cube4 = new THREE.Mesh(_geometry4, _material);
  let cylinder = new THREE.Mesh(_cylinderGeometry, _material);

  cube2.position.set(0, 0.5, 0);
  cube3.position.set(0, 1, 0);
  cube4.position.set(0, 1.5, 0);
  cylinder.position.set(0, 2, 0);

  towerTop.add(cube1);
  towerTop.add(cube2);
  towerTop.add(cube3);
  towerTop.add(cube4);
  towerTop.add(cylinder);
  towerTop.position.set(0, 11, 0);
  towerTop.rotation.set(0, Math.PI / 4, 0);
  return towerTop;
}

// 金茂大廈身體主幹
function getJinmaoTowerBody() {
  let towerBody = new THREE.Object3D();
  let _geometry1 = new THREE.BoxGeometry(5, 7, 5);
  let _geometry2 = new THREE.BoxGeometry(4.5, 5.5, 4.5);
  let _geometry3 = new THREE.BoxGeometry(4, 4, 4);
  let _geometry4 = new THREE.BoxGeometry(3.5, 3, 3.5);
  let _geometry5 = new THREE.BoxGeometry(3, 2, 3);
  let _geometry6 = new THREE.BoxGeometry(2.5, 1.5, 2.5);
  let _geometry7 = new THREE.BoxGeometry(2, 1.3, 2);
  let _geometry8 = new THREE.BoxGeometry(1.5, 1, 1.5);
  let _material = new THREE.MeshPhongMaterial({
    color: "rgb(120, 120, 120)"
  });

  let cube1 = new THREE.Mesh(_geometry1, _material);
  let cube2 = new THREE.Mesh(_geometry2, _material);
  let cube3 = new THREE.Mesh(_geometry3, _material);
  let cube4 = new THREE.Mesh(_geometry4, _material);
  let cube5 = new THREE.Mesh(_geometry5, _material);
  let cube6 = new THREE.Mesh(_geometry6, _material);
  let cube7 = new THREE.Mesh(_geometry7, _material);
  let cube8 = new THREE.Mesh(_geometry8, _material);
  cube2.position.set(0, 5.5, 0);
  cube3.position.set(0, 9.5, 0);
  cube4.position.set(0, 12.5, 0);
  cube5.position.set(0, 14.5, 0);
  cube6.position.set(0, 16, 0);
  cube7.position.set(0, 17.3, 0);
  cube8.position.set(0, 18.3, 0);

  towerBody.add(cube1);
  towerBody.add(cube2);
  towerBody.add(cube3);
  towerBody.add(cube4);
  towerBody.add(cube5);
  towerBody.add(cube6);
  towerBody.add(cube7);
  towerBody.add(cube8);
  towerBody.position.set(0, -8, 0);
  return towerBody;
}

複製代碼

搭建金茂大廈也是比較簡單的,由於不用本身手寫不規則幾何體,經過Three.js現有的幾何體就能夠完成。 大體步驟是:首先生成一個十字的骨架,而後經過底部累加多個方形幾何體 頂部也是相似。用到的方法在以前已經講解過,此處就不過多贅述,有興趣能夠去github上查閱代碼。

隨機算法搭建其餘建築物

由於只寫了4個外灘的標誌性建築:東方明珠,中心大廈,環球金融中心和金茂大廈。另外其餘的建築物我選擇經過隨機生成建築物的形式來實現。

function getBuilding(scene) {
    // 1. 定義了隨機建築物的位置座標數組 
    let positionsArr = [
        { x: -13, y: 0, z: -15 },
        { x: -7, y: 0, z: -13 },
        { x: -1, y: 0, z: -16 },
        { x: -2, y: 0, z: -10 },
        { x: -10, y: 0, z: -5 },
        { x: 5, y: 0, z: -25 },
        { x: -3, y: 0, z: -18 },
        { x: -8, y: 0, z: -18 },
        { x: -18, y: 0, z: -25 },
        { x: -6, y: 0, z: -25 },
        { x: -3, y: 0, z: -30 },
        { x: -10, y: 0, z: -30 },
        { x: -17, y: 0, z: -30 },
        { x: -3, y: 0, z: -35 },
        { x: -12, y: 0, z: -35 },
        { x: -20, y: 0, z: -35 },
        { x: -3, y: 0, z: -40 },
        { x: -16, y: 0, z: -40 },
        { x: 16, y: 0, z: -40 },
        { x: 18, y: 0, z: -38 },
        { x: 16, y: 0, z: -40 },
        { x: 30, y: 0, z: -40 },
        { x: 32, y: 0, z: -40 },
        { x: 16, y: 0, z: -35 },
        { x: 36, y: 0, z: -38 },
        { x: 42, y: 0, z: -32 },
        { x: 42, y: 0, z: -26 },
        { x: 35, y: 0, z: -20 },
        { x: 36, y: 0, z: -32 },
        { x: 25, y: 0, z: -22 },
        { x: 26, y: 0, z: -20 },
        { x: 19, y: 0, z: -8 },
        { x: 30, y: 0, z: -18 },
        { x: 25, y: 0, z: -15 },
        { x: 9, y: 0, z: -10 },
        { x: 1, y: 0, z: -9 },
        { x: 1, y: 0, z: -30 },
        { x: 0, y: 0, z: -35 },
        { x: 1, y: 0, z: -32 },
        { x: 8, y: 0, z: -5 },
        { x: 15, y: 0, z: -6 },
        { x: 5, y: 0, z: -40 },
        { x: 9, y: 0, z: -40 }
      ];
      let defauleLength = 16;
      // 2. 循環數組,生成每一個幾何體
      for (let i = 0; i < positionsArr.length; i++) {
        // 3. 經過隨機數Math.random,隨機生成每一個幾何體的長 寬 高
        let w = Math.random() * 3 + 2; // 隨機數(2, 5);
        let d = Math.random() * 3 + 2; // 隨機數(2, 5);
        let h = Math.random() * defauleLength + 5; // 隨機數(0, 20);
        let geometry = new THREE.BoxGeometry(w, h, d);
        let material = new THREE.MeshPhongMaterial({ color: "rgb(120, 120, 120)" });
        // 4. 生成每一個幾何體
        let mesh = new THREE.Mesh(geometry, material);
        // 5. 每一個幾何體的位置
        mesh.position.set(
          positionsArr[i].x,
          positionsArr[i].y + h / 2,
          positionsArr[i].z
        );
        // 6. 顯示陰影
        mesh.castShadow = true;
        // 7. 將每一個幾何體添加到場景中
        scene.add(mesh);
      }
}

複製代碼

這裏的隨機生成的幾何體,除了位置座標肯定外,長度 寬度 高度包括以後要作的貼圖都是隨機的

給全部建築物進行貼圖優化

給建築物添加貼圖,使其看起來更加美觀一些。這裏東方明珠和環球金融中心沒有找到合適的貼圖,因此沒有作貼圖處理。具體代碼不復雜,有興趣能夠去GitHub上查閱代碼

搭建黃浦江

以前咱們爲黃浦江留出了空位,如今就要把它「填」上。 需求是要做出水流的感受,原理不難,首先定一個平面,並對它進行貼圖,使之更像江水的顏色。 這裏用了MeshStandardMaterial材質,該材質是標準網絡材質。該材質提供了比MeshLambertMaterialMeshPhongMaterial 更精確和逼真的結果,但代價是計算成本更高。 由於這個江水的波浪須要動態的變化,因此江水的代碼須要在兩個地方書寫。 首先經過getRiver定義好江水的基本樣式,其次給江水設置一個名稱river.name = 'river',而後在update方法中動態的改變其頂點

getRiver方法:

function getRiver() {
  let material = new THREE.MeshStandardMaterial({
    color: MATERIAL_COLOR
  });
    
//顏色貼圖
  material.map = loader.load("../assets/textures/river.jpg");
//凹凸貼圖
  material.bumpMap = loader.load("../assets/textures/river.jpg");
// 粗糙貼圖 使紋理反光起伏變化
  material.roughnessMap = loader.load("../assets/textures/river.jpg");
// bumpScale:凹凸參數 控制貼圖平面的平整度
  material.bumpScale = 0.01;
// metalness:金屬質感 範圍(0,1)
  material.metalness = 0.1;
// roughness:反光強度/粗糙度 取值範圍(0,1)
  material.roughness = 0.7;
  // 調整透明度 使之更加靠近江水的顏色
  material.transparent = true;
  material.opacity = 0.85;

  let geometry = new THREE.PlaneGeometry(73, 100, 60, 60);
// 平面要定義side爲THREE.DoubleSide 否則只能展現出一個面
  material.side = THREE.DoubleSide;
  let river = new THREE.Mesh(geometry, material);
  river.receiveShadow = true;
  // river.castShadow = true;

  river.rotation.x = Math.PI / 2;
  river.rotation.z = Math.PI / 2;
  river.position.z = -14.5;
  river.position.y = -2;

  return river;
}
複製代碼
init方法:

...
  // 黃浦江
  let river = getRiver();
  river.name = "river";
  scene.add(river);
...
複製代碼
update方法:

function update(renderer, scene, camera, controls, stats, clock) {
  renderer.render(scene, camera);

  // 性能監控
  stats.update();

  // 相機軌道控制器
  controls.update();

  // 獲取時間
  let elapsedTime = clock.getElapsedTime();

// 獲取到江水river
  let plane = scene.getObjectByName("river");
// 拿到geometry
  let planeGeo = plane.geometry;
// 使其按照Math.sin規律性的動態改變江水的頂點
  planeGeo.vertices.forEach((vertex, ind) => {
    vertex.z = Math.sin(elapsedTime + ind * 0.3) * 0.5;
  });
// 改變頂點後設置verticesNeedUpdate爲true纔有效果
  planeGeo.verticesNeedUpdate = true;

  renderer.render(scene, camera);
  requestAnimationFrame(function() {
    update(renderer, scene, camera, controls, stats, clock);
  });
}
複製代碼

update方法中,獲取到剛纔定義的name爲river的幾何體(黃浦江),並經過clock.getElapsedTime來獲取自時鐘啓動後的秒數和Math.sin 並循環調用requestAnimationFrame 方法 來動態的改變該平面幾何體的頂點,使其更像水流的波浪。

搭建360全景空間

能夠看到,代碼寫到這裏,上海外灘已經初具模樣。燈光,建築物,貼圖,河流等都有了。可是 場景中的整個背景仍是灰色的。接下來,咱們就開始着手優化這個。 經過 THREE.CubeTextureLoader 來實現一個360度的背景。

首先,須要6個圖片。其實就是把一個全景照片分紅6份,將其每一個圖片路徑放到一個數組中。最終經過new THREE.CubeTextureLoader().load(數組);來加載這個圖片數組便可。 相關貼圖和背景圖片在github中,須要能夠拿去

getReflectionCube方法:

function getReflectionCube() {
  let path = "/assets/cubemap/";
  let format = ".jpg";
// 定義好包含6長全景圖片的數組urls
  let urls = [
    `${path}px${format}`,
    `${path}nx${format}`,
    `${path}py${format}`,
    `${path}ny${format}`,
    `${path}pz${format}`,
    `${path}nz${format}`
  ];
  let reflectionCube = new THREE.CubeTextureLoader().load(urls);
  reflectionCube.format = THREE.RGBFormat;
  return reflectionCube;
}
複製代碼
init方法中:

...
 // 全景視圖
  scene.background = getReflectionCube();
...
複製代碼

至此,就大概實現了3D版的上海外灘。其實還有許多地方沒有進行優化,好比說貼圖太醜,優化總體燈光,江水反射效果等。根本緣由仍是時間不夠,最近給本身挖了很多的坑,出來混老是要還了,須要本身慢慢去填上。加上公司項目也是比較緊,沒有過多的時間去作一些優化。不過經過該項目本身仍是在three.js中獲得了很多鍛鍊和提高,算是入了個門了吧😄。可是在three.js中的一些高級方法本身仍是掌握的不是很到位,須要繼續深刻研究。還有就是手寫模型是真的累啊...考慮複雜模型仍是要經過3D建模軟件畫出理想的模型,在three.js中直接引入模型就行了,就不須要本身手寫複雜模型了。不說了,我要去練習blender了...

轉載請註明出處 謝謝🙏

相關文章
相關標籤/搜索