城市建筑与烟雾效果
今天看到一个效果,感觉挺好看的吗,分享一下,完整的代码:https://codepen.io/vcomics/pen/aGmoae (opens in a new tab)
思路概述
- 创建基础环境
- 初始化渲染器(renderer)并将其插入 HTML 页面
- 设置自适应窗口大小的监听
- 创建透视相机(PerspectiveCamera)和主场景(Scene)
- 设置场景“雾”和背景
- 将场景背景色设为一个较鲜艳的颜色
- 在一定范围内启用雾效(Fog),增强纵深感
- 准备容器对象
- 三个
Object3D
:city
、smoke
、town
,分别承载不同类型的对象(建筑、烟雾、辅助网格等)
- 三个
- 辅助函数
- 例如生成随机数
mathRandom()
、切换颜色函数setTintColor()
等,为后续创建建筑做准备
- 例如生成随机数
- 创建城市(init 函数)
- 随机生成多个立方体,拉伸成不同高度,模拟“建筑”
- 在地面处生成一个大平面来承载阴影
- 生成一些小圆形几何体充当“烟雾”或“粒子”效果
- 光照与环境
- AmbientLight、SpotLight、PointLight 组合
- 开启阴影并设置光源位置、强度等
- 生成“小车”移动
- 利用
createCars()
函数在 X 或 Z 方向往返移动的小立方体 - 通过
generateLines()
创建多辆“车”来回穿梭
- 利用
- 鼠标或触屏事件
- 通过射线(Raycaster)或简单的鼠标移动函数来记录并处理交互
- 让
city
容器根据鼠标移动进行轻微旋转
- 动画主循环
requestAnimationFrame(animate)
实现不断刷新- 每帧让建筑物、烟雾、“汽车”等进行动画或位置更新
- 最后使用
renderer.render(scene, camera)
进行渲染
- 启动
- 先调用
generateLines()
、init()
初始化场景 - 最后进入
animate()
循环
通过以上简单思路,即可快速把握整段代码的核心脉络。下面是每部分的详细解析,便于深入理解实现原理。
一、项目基础结构与核心对象
1. 创建渲染器(Renderer)
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
- WebGLRenderer:Three.js 最常用的渲染器,使用 WebGL API 进行渲染。
antialias
:是否开启抗锯齿。renderer.setSize()
:设置渲染器的宽高为浏览器当前窗口的宽高。
if (window.innerWidth > 800) {
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.shadowMap.needsUpdate = true;
}
- 判断屏幕宽度大于 800 时,开启并设置阴影图参数(阴影质量、阴影类型等)。
PCFSoftShadowMap
提供了柔和阴影的效果。
document.body.appendChild(renderer.domElement);
- 将渲染器的 DOM 元素(即
<canvas>
标签)插入到页面中,确保能够在页面中看到渲染结果。
2. 动态自适应屏幕大小
window.addEventListener("resize", onWindowResize, false);
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
- 监听浏览器窗口变化,更新摄像机(
camera
)的宽高比和投影矩阵。 - 同时更新渲染器大小,保证场景在窗口大小变化时保持正确的比例。
3. 创建相机(Camera)与场景(Scene)
var camera = new THREE.PerspectiveCamera(20, window.innerWidth / window.innerHeight, 1, 500);
camera.position.set(0, 2, 14);
var scene = new THREE.Scene();
- PerspectiveCamera:使用透视投影相机,参数分别是
视角FOV
、宽高比
、近截面
和远截面
。 camera.position.set()
:将相机放置在 (0, 2, 14) 的位置。- 创建场景后,可以将 3D 对象、光照等全部添加到场景中。
4. 创建主要容器对象
var city = new THREE.Object3D();
var smoke = new THREE.Object3D();
var town = new THREE.Object3D();
- 分别创建了三个
Object3D
对象,用来承载不同分组的 Mesh 或者子物体。- city:在这段代码中,承载主要地面、光源以及格子辅助器等。
- smoke:存放烟雾粒子(一些小圆点的集合)。
- town:存放城市中生成的建筑群(一个个立方体)。
二、场景环境与辅助函数
1. 场景背景与雾效
var setcolor = 0xf02050;
scene.background = new THREE.Color(setcolor);
scene.fog = new THREE.Fog(setcolor, 10, 16);
- 将场景的背景色设置为
0xF02050
(红粉色)。 - 使用
THREE.Fog
创建线性雾效,其中 10 和 16 分别是开始产生雾和彻底被雾覆盖的距离。
2. 随机数生成函数
function mathRandom(num = 8) {
var numValue = -Math.random() * num + Math.random() * num;
return numValue;
}
- 生成一个范围为
-num ~ +num
的随机数。例如,mathRandom(8)
会在-8 ~ 8
的区间内产生随机值。
3. 切换建筑颜色函数
var setTintNum = true;
function setTintColor() {
if (setTintNum) {
setTintNum = false;
var setColor = 0x000000;
} else {
setTintNum = true;
var setColor = 0x000000;
}
return setColor;
}
- 这段代码看似想实现颜色在两个值之间切换,实际上因为同时设为
0x000000
,所以颜色并没有真正变化。可以根据需求自行改成其它颜色以达到不同效果。
三、创建城市(建筑、地面等)
1. init()
函数:核心建筑生成逻辑
function init() {
var segments = 2;
for (var i = 1; i<100; i++) {
var geometry = new THREE.CubeGeometry(1,0,0,segments,segments,segments);
var material = new THREE.MeshStandardMaterial({
color:setTintColor(),
wireframe:false,
side:THREE.DoubleSide
});
var wmaterial = new THREE.MeshLambertMaterial({
color:0xFFFFFF,
wireframe:true,
transparent:true,
opacity: 0.03,
side:THREE.DoubleSide
});
var cube = new THREE.Mesh(geometry, material);
var wire = new THREE.Mesh(geometry, wmaterial);
var floor = new THREE.Mesh(geometry, material);
var wfloor = new THREE.Mesh(geometry, wmaterial);
cube.add(wfloor);
cube.castShadow = true;
cube.receiveShadow = true;
cube.rotationValue = 0.1 + Math.abs(mathRandom(8));
// 设置地板/建筑等整体缩放
floor.scale.y = 0.05;
cube.scale.y = 0.1 + Math.abs(mathRandom(8));
cube.scale.x = cube.scale.z = 0.9 + mathRandom(1 - 0.9);
// 随机定位
cube.position.x = Math.round(mathRandom());
cube.position.z = Math.round(mathRandom());
floor.position.set(cube.position.x, 0, cube.position.z);
town.add(floor);
town.add(cube);
};
// ...
- 立方体几何体:
CubeGeometry(1,0,0,...)
时,高宽深中,高和深等被初始化为 0(其实 Three.js 的新版中推荐使用BoxGeometry
)。后续是通过scale
对建筑进行拉伸和缩放。 - 材料:
MeshStandardMaterial
:一种能够实现金属度、粗糙度等物理属性的材质。MeshLambertMaterial
:通过 Lambert 光照模型进行计算,适合简单光照场景。
- 缩放:
cube.scale.y = 0.1 + Math.abs(mathRandom(8))
让建筑物高度随机。 - 随机位置:
cube.position.x
和cube.position.z
根据mathRandom()
生成,分布在较小范围内,可以自行调整使建筑分布得更稀疏或更密集。 - 添加到容器:最终将
floor
(扁平立方体)与cube
(高楼)加到town
这个 Object3D 中。
2. 生成烟雾粒子(smoke)
var gmaterial = new THREE.MeshToonMaterial({ color: 0xffff00, side: THREE.DoubleSide });
var gparticular = new THREE.CircleGeometry(0.01, 3);
for (var h = 1; h < 300; h++) {
var particular = new THREE.Mesh(gparticular, gmaterial);
particular.position.set(mathRandom(5), mathRandom(5), mathRandom(5));
particular.rotation.set(mathRandom(), mathRandom(), mathRandom());
smoke.add(particular);
}
- 使用 CircleGeometry(小圆形)模拟“烟雾”或类似“粒子”。
- 随机位置与旋转,让这些圆点分散在空间内。
3. 创建地面
var pmaterial = new THREE.MeshPhongMaterial({
color:0x000000,
side:THREE.DoubleSide,
roughness: 10,
metalness: 0.6,
opacity:0.9,
transparent:true
});
var pgeometry = new THREE.PlaneGeometry(60,60);
var pelement = new THREE.Mesh(pgeometry, pmaterial);
pelement.rotation.x = -90 * Math.PI / 180;
pelement.position.y = -0.001;
pelement.receiveShadow = true;
city.add(pelement);
};
- 用一个 PlaneGeometry(平面几何)来作为地面。
- 这里将
rotation.x
设为-90°
让其与 XZ 平面重合。 position.y
设为 -0.001,略微降低一点点,避免与其他物体(建筑)重叠产生闪烁。
四、鼠标交互事件
1. 射线(Raycaster)与鼠标坐标
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2(),
INTERSECTED;
var intersected;
function onMouseMove(event) {
event.preventDefault();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
- Raycaster:用于鼠标与场景中物体的碰撞检测。这里仅初始化了对象,并把当前鼠标位置存储在
mouse
中。 - 之后可以使用
raycaster.setFromCamera(mouse, camera)
来检测射线与物体的交点。
2. 移动端触控事件
function onDocumentTouchStart(event) {
if (event.touches.length == 1) {
event.preventDefault();
mouse.x = event.touches[0].pageX - window.innerWidth / 2;
mouse.y = event.touches[0].pageY - window.innerHeight / 2;
}
}
- 用于在移动端获取触摸点的位置。此处的写法与 PC 端略有不同,是为了兼容移动端。
五、灯光设置
var ambientLight = new THREE.AmbientLight(0xffffff, 4);
var lightFront = new THREE.SpotLight(0xffffff, 20, 10);
var lightBack = new THREE.PointLight(0xffffff, 0.5);
lightFront.position.set(5, 5, 5);
lightFront.castShadow = true;
// ...
lightBack.position.set(0, 6, 0);
smoke.position.y = 2;
scene.add(ambientLight);
city.add(lightFront);
scene.add(lightBack);
scene.add(city);
city.add(smoke);
city.add(town);
- ambientLight:环境光,给场景一个整体的、无方向的光照。
- lightFront:聚光灯(SpotLight),能产生方向性的阴影效果。设置了相当高的强度
20
,并且可投射阴影。 - lightBack:点光源(PointLight),强度
0.5
,放在稍高处 (0, 6, 0)。 - 最后将这些光源及容器对象整合到场景中。
六、网格辅助线(GridHelper)
var gridHelper = new THREE.GridHelper(60, 120, 0xff0000, 0x000000);
city.add(gridHelper);
- GridHelper 可以帮助我们在调试或场景开发初期,看清坐标轴地面位置。
60
代表网格的大小,120
表示分割数量。0xFF0000
和0x000000
分别是网格线的两种颜色(中心线和其他线)。
七、小方块移动
1. createCars()
函数
var createCars = function (cScale = 2, cPos = 20, cColor = 0xffff00) {
var cMat = new THREE.MeshToonMaterial({ color: cColor, side: THREE.DoubleSide });
var cGeo = new THREE.CubeGeometry(1, cScale / 40, cScale / 40);
var cElem = new THREE.Mesh(cGeo, cMat);
var cAmp = 3;
if (createCarPos) {
createCarPos = false;
cElem.position.x = -cPos;
cElem.position.z = mathRandom(cAmp);
TweenMax.to(cElem.position, 3, {
x: cPos,
repeat: -1,
yoyo: true,
delay: mathRandom(3),
});
} else {
createCarPos = true;
cElem.position.x = mathRandom(cAmp);
cElem.position.z = -cPos;
cElem.rotation.y = (90 * Math.PI) / 180;
TweenMax.to(cElem.position, 5, {
z: cPos,
repeat: -1,
yoyo: true,
delay: mathRandom(3),
ease: Power1.easeInOut,
});
}
cElem.receiveShadow = true;
cElem.castShadow = true;
cElem.position.y = Math.abs(mathRandom(5));
city.add(cElem);
};
- 每次调用
createCars
,会随机生成一个小立方体,代表一辆车。 - 通过
TweenMax.to
(GreenSock 动画库) 来实现自动往返移动:- 若
createCarPos
为 true,汽车在x
轴与z
轴的初始位置不一样,控制它在 X 轴或 Z 轴移动往返。 repeat:-1
表示无限循环,yoyo:true
表示移动会来回往返。
- 若
2. generateLines()
函数
var generateLines = function () {
for (var i = 0; i < 60; i++) {
createCars(0.1, 20);
}
};
- 调用 60 次
createCars
,生成 60 辆“车”在场景中持续运动。
八、相机运动
var cameraSet = function () {
createCars(0.1, 20, 0xffffff);
};
- 只是额外创建了一辆车,颜色为白色,可根据需要持续调用,让场景更丰富。
九、动画循环与交互 (animate()
函数)
var animate = function () {
var time = Date.now() * 0.00005;
requestAnimationFrame(animate);
city.rotation.y -= (mouse.x * 8 - camera.rotation.y) * uSpeed;
city.rotation.x -= (-(mouse.y * 2) - camera.rotation.x) * uSpeed;
if (city.rotation.x < -0.05) city.rotation.x = -0.05;
else if (city.rotation.x > 1) city.rotation.x = 1;
// 烟雾缓慢旋转
smoke.rotation.y += 0.01;
smoke.rotation.x += 0.01;
camera.lookAt(city.position);
renderer.render(scene, camera);
};
- 通过
requestAnimationFrame
进行循环渲染,每一帧都调用animate
。 - 根据
mouse.x
和mouse.y
动态调整city
的旋转,让整个城市跟随鼠标视角进行一些摆动。 - 设置
city.rotation.x
上下限,避免过度倾斜。 - 烟雾(
smoke
)在每帧做微小旋转。 camera.lookAt(city.position)
:让相机始终朝向城市的中心。- 最后使用
renderer.render(scene, camera)
完成本帧的渲染。