11

国庆献礼之ThreeJs实现3D上海外滩全景

  • 最近入了three.js的坑,想用three.js做一些demo以便巩固自己最近所掌握的知识点,而且正好赶上国庆放假,随,有了这篇~
  • 预览地址: Three.js之上海外滩

    Tip1: 打开后浏览器一直转圈 建筑物的贴图不显示 是网络问题 等一会儿就好 毕竟是github...
    Tip2: 打开后帧数过低 比较卡的话,可以调整代码中的SHADOW_MAPSIZE大小 通过调整锯齿感来优化性能
  • 代码地址: Three.js之上海外滩
  • 觉得不错的话麻烦点个star 谢谢 👏

本篇虽是关于Three.js入门的文章, 但是太过入门的就不讲了,没意义,网上很多相关入门知识。本篇主要是把自己在写demo时候遇到的坑点给记录下来, 有什么不懂的直接去查阅文档或者网上搜,这里提一下:Three.js的官方文档和例子对开发者也是挺友好的(有中文版!)


废话不多, 先看下效果吧:

图片描述

图片描述

图片描述

图片描述

图片描述

代码比较多,就不一一讲解了,本篇主要分为以下几个部分:

  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上查看

该部分没有做什么比较核心的地方,只是一些场景 相机 渲染器 画辅助线 性能监控 等的初始化,但花费时间稍长的是画出两个不规则地面,留出一道“黄浦江”的位置。

clipboard.png

我们知道,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

搭建东方明珠

clipboard.png

以坐标(0, 0, 0)为原点创建东方明珠。其实这个还蛮简单的,不需要你自己画出不规则的网格,利用Three.js相关基础的API就可以做到,对其进行组合排列就好了。但是相关代码量是比较多的,这里就不贴出了,可以去github上翻阅
首先,我们可以观察下东方明珠,不难发现,它都是有一些常见的几何体组合而成,比如圆柱,球体,圆环等等。我们可以将整个东方明珠分为三个部分,底部,中部,顶部。
首先底部由两个叠加的圆台(其实就是高度很小的圆柱),加上3个竖直的圆柱和3个倾斜的圆柱组成。
中部就简单多了,两个球,加上中间整齐排列的圆环组成。
顶部也不难,3个半径不同球体和3个半径不同的圆柱组成。

搭建上海中心大厦

clipboard.png

这个当时也是花费了一些时间的。 由于上海中心大厦是个极其不规则的几何体,用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默认是不会改变其顶点的。

搭建环球金融中心

clipboard.png

可以看到, 环球金融中心也是一个不规则的网格几何体。
可以将其分为两部分,底部不规则网格和顶部不规则网格。此处实现不规则网格几何体的话我们就要通过手写顶点坐标vertices和手写每个面的三角形faces来实现自定义的不规则几何体。

底部不规则网格:

clipboard.png

顶部不规则网格:

clipboard.png

底部代码:

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
在创建不规则几何体每个顶点的时候,如果实在想不出每个顶点的空间位置的话,推荐在稿纸上先画出个大概的草图,建立坐标轴来进行标注。以下是我当时画的草图,不是很精确,只是草图,供参考:

clipboard.png

比如上图,确定好每个顶点的额坐标之后,再根据每3个顶点坐标写出每个三角形面。

搭建金茂大厦

clipboard.png


// 金茂大厦
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个外滩的标志性建筑:东方明珠,中心大厦,环球金融中心和金茂大厦。另外其他的建筑物我选择通过随机生成建筑物的形式来实现。

clipboard.png

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);
      }
}

这里的随机生成的几何体,除了位置坐标确定外,长度 宽度 高度包括之后要做的贴图都是随机的

给所有建筑物进行贴图优化

clipboard.png

给建筑物添加贴图,使其看起来更加美观一些。这里东方明珠和环球金融中心没有找到合适的贴图,所以没有做贴图处理。具体代码不复杂,有兴趣可以去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度的背景。

clipboard.png

首先,需要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了...

转载请注明出处 谢谢🙏


Funky_Tiger
443 声望33 粉丝

刷题,交流,offer,内推,涨薪,相亲,前端资源共享...