国庆献礼之ThreeJs实现3D上海外滩全景
- 最近入了three.js的坑,想用three.js做一些demo以便巩固自己最近所掌握的知识点,而且正好赶上国庆放假,随,有了这篇~
-
预览地址: Three.js之上海外滩
Tip1: 打开后浏览器一直转圈 建筑物的贴图不显示 是网络问题 等一会儿就好 毕竟是github... Tip2: 打开后帧数过低 比较卡的话,可以调整代码中的SHADOW_MAPSIZE大小 通过调整锯齿感来优化性能
- 代码地址: Three.js之上海外滩
- 觉得不错的话麻烦点个star 谢谢 👏
本篇虽是关于Three.js入门的文章, 但是太过入门的就不讲了,没意义,网上很多相关入门知识。本篇主要是把自己在写demo时候遇到的坑点给记录下来, 有什么不懂的直接去查阅文档或者网上搜,这里提一下:Three.js的官方文档和例子对开发者也是挺友好的(有中文版!)
废话不多, 先看下效果吧:
代码比较多,就不一一讲解了,本篇主要分为以下几个部分:
-
初始化代码,搭建不规则的地面几何体等「知识点:利用
THREE.Shape
和THREE.ExtrudeGeometry
创建不规则的几何体」 -
搭建东方明珠「知识点:利用多种几何体组合
THREE.Object3D()
创建不规则的几何体」 -
搭建上海中心大厦「知识点:利用正弦函数
Math.sin
规律性的改变几何体的顶点vertices
创建不规则的几何体」 -
搭建环球金融中心「知识点:利用手写顶点数组
vertices
和三角形面数组faces
的方式创建不规则的几何体」 -
搭建金茂大厦「知识点:利用多种几何体组合
THREE.Object3D()
创建不规则的几何体」 -
随机算法搭建其他建筑物「知识点:
Math.random
随机生成几何体」 -
给所有建筑物进行贴图优化 「知识点:
THREE.TextureLoader
的运用」 -
搭建黄浦江 「知识点:
requestAnimationFrame
动画方法的运用」 -
搭建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
上查看
该部分没有做什么比较核心的地方,只是一些场景 相机 渲染器 画辅助线 性能监控 等的初始化,但花费时间稍长的是画出两个不规则地面,留出一道“黄浦江”的位置。
我们知道,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
将其拉伸为一个三维图形。这是前半部分的地面几何体,后半部分也是一样的。
其他的具体参数信息就查阅文档:Shape 和 ExtrudeGeometry
搭建东方明珠
以坐标(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
材质,该材质是标准网络材质。该材质提供了比MeshLambertMaterial
或 MeshPhongMaterial
更精确和逼真的结果,但代价是计算成本更高。
因为这个江水的波浪需要动态的变化,所以江水的代码需要在两个地方书写。
首先通过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了...
转载请注明出处 谢谢🙏
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。