目录
版本1.0:简易版本
<!DOCTYPE html>
<html>
<head>
<title>3D凯旋门与扩展建筑群(斜角俯视)</title>
<style>
body { margin: 0; }
canvas { display: block; }
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
<script>
// 设置场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xffffff, 1); // 白色背景
document.body.appendChild(renderer.domElement);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(0, 1, 1);
scene.add(directionalLight);
// 创建凯旋门主要结构
const archMaterial = new THREE.MeshPhongMaterial({ color: 0xD2B48C });
// 底部基座
const baseGeometry = new THREE.BoxGeometry(8, 2, 4);
const base = new THREE.Mesh(baseGeometry, archMaterial);
base.position.y = 1;
scene.add(base);
// 左右立柱
const pillarGeometry = new THREE.BoxGeometry(2, 6, 4);
const leftPillar = new THREE.Mesh(pillarGeometry, archMaterial);
leftPillar.position.set(-3, 5, 0);
scene.add(leftPillar);
const rightPillar = new THREE.Mesh(pillarGeometry, archMaterial);
rightPillar.position.set(3, 5, 0);
scene.add(rightPillar);
// 顶部横梁
const topGeometry = new THREE.BoxGeometry(8, 2, 4);
const topBeam = new THREE.Mesh(topGeometry, archMaterial);
topBeam.position.y = 8;
scene.add(topBeam);
// 添加简单的装饰细节
const detailGeometry = new THREE.BoxGeometry(7, 0.5, 0.5);
const detail = new THREE.Mesh(detailGeometry, archMaterial);
detail.position.set(0, 7, 2);
scene.add(detail);
// 添加扩展建筑群(5环布局,不遮挡凯旋门)
const buildingMaterial = new THREE.MeshPhongMaterial({ color: 0x808080 }); // 灰色高楼材质
const buildingGeometry = new THREE.BoxGeometry(3, 10, 3); // 高楼基本形状
// 定义5环,调整半径和高度
const rings = 5;
const baseRadius = 20; // 起始环半径
const ringSpacing = 10; // 每环间距
for (let ring = 1; ring <= rings; ring++) {
const radius = baseRadius + (ring - 1) * ringSpacing;
const numBuildings = 8 + ring * 4; // 每环建筑数量递增
for (let i = 0; i < numBuildings; i++) {
const angle = (i / numBuildings) * Math.PI * 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
const height = 6 + Math.random() * 6 * ring; // 高度6-12到6-36
const buildingMesh = new THREE.Mesh(buildingGeometry, buildingMaterial);
buildingMesh.position.set(x, height / 2, z);
buildingMesh.scale.y = height / 10; // 调整高度缩放
scene.add(buildingMesh);
}
}
// 设置相机位置(斜角俯视)
camera.position.set(30, 30, 30); // 斜上方位置
camera.lookAt(0, 5, 0); // 聚焦于凯旋门中心
// 动画循环
function animate() {
requestAnimationFrame(animate);
// 添加旋转动画
scene.rotation.y += 0.002; // 减慢旋转速度
renderer.render(scene, camera);
}
animate();
// 处理窗口大小变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
效果图:
版本2.0:建筑渲染
<!DOCTYPE html>
<html>
<head>
<title>3D凯旋门与多彩建筑群(斜角俯视)</title>
<style>
body { margin: 0; }
canvas { display: block; }
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
<script>
// 设置场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xffffff, 1); // 白色背景
document.body.appendChild(renderer.domElement);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(0, 1, 1);
scene.add(directionalLight);
// 创建凯旋门主要结构
const archMaterial = new THREE.MeshPhongMaterial({ color: 0xD2B48C });
// 底部基座
const baseGeometry = new THREE.BoxGeometry(8, 2, 4);
const base = new THREE.Mesh(baseGeometry, archMaterial);
base.position.y = 1;
scene.add(base);
// 左右立柱
const pillarGeometry = new THREE.BoxGeometry(2, 6, 4);
const leftPillar = new THREE.Mesh(pillarGeometry, archMaterial);
leftPillar.position.set(-3, 5, 0); // 修正:移除 .demo
scene.add(leftPillar);
const rightPillar = new THREE.Mesh(pillarGeometry, archMaterial);
rightPillar.position.set(3, 5, 0);
scene.add(rightPillar);
// 顶部横梁
const topGeometry = new THREE.BoxGeometry(8, 2, 4);
const topBeam = new THREE.Mesh(topGeometry, archMaterial);
topBeam.position.y = 8;
scene.add(topBeam);
// 添加简单的装饰细节
const detailGeometry = new THREE.BoxGeometry(7, 0.5, 0.5);
const detail = new THREE.Mesh(detailGeometry, archMaterial);
detail.position.set(0, 7, 2);
scene.add(detail);
// 添加扩展建筑群(5环布局,丰富颜色和窗户)
const buildingGeometry = new THREE.BoxGeometry(3, 10, 3); // 高楼基本形状
const windowMaterial = new THREE.MeshPhongMaterial({ color: 0xFFFFFF }); // 白色窗户材质
const windowGeometry = new THREE.BoxGeometry(0.4, 0.4, 0.1); // 窗户形状
// 定义颜色调色板
const buildingColors = [
0xFF6347, // 番茄红
0x4682B4, // 钢蓝
0x32CD32, // 柠檬绿
0xFFD700, // 金黄
0x9932CC, // 深紫
0xFF4500, // 橙红
0x00CED1 // 深青
];
// 定义5环
const rings = 5;
const baseRadius = 20; // 起始环半径
const ringSpacing = 10; // 每环间距
for (let ring = 1; ring <= rings; ring++) {
const radius = baseRadius + (ring - 1) * ringSpacing;
const numBuildings = 8 + ring * 4; // 每环建筑数量递增
for (let i = 0; i < numBuildings; i++) {
const angle = (i / numBuildings) * Math.PI * 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
const height = 6 + Math.random() * 6 * ring; // 高度6-12到6-36
// 创建建筑
const buildingMaterial = new THREE.MeshPhongMaterial({
color: buildingColors[Math.floor(Math.random() * buildingColors.length)] // 随机颜色
});
const buildingMesh = new THREE.Mesh(buildingGeometry, buildingMaterial);
buildingMesh.position.set(x, height / 2, z);
buildingMesh.scale.y = height / 10; // 调整高度缩放
scene.add(buildingMesh);
// 添加窗户(在建筑正面和侧面)
const numWindows = Math.floor(height / 2); // 根据高度确定窗户数量
for (let w = 0; w < numWindows; w++) {
// 正面窗户
const windowMesh1 = new THREE.Mesh(windowGeometry, windowMaterial);
windowMesh1.position.set(x + 1.2, (w * 1.5) + 1, z + 1.51); // 建筑正面
windowMesh1.scale.y = height / 10;
scene.add(windowMesh1);
const windowMesh2 = new THREE.Mesh(windowGeometry, windowMaterial);
windowMesh2.position.set(x - 1.2, (w * 1.5) + 1, z + 1.51); // 建筑正面另一侧
windowMesh2.scale.y = height / 10;
scene.add(windowMesh2);
// 侧面窗户
const windowMesh3 = new THREE.Mesh(windowGeometry, windowMaterial);
windowMesh3.position.set(x + 1.51, (w * 1.5) + 1, z + 1.2); // 建筑侧面
windowMesh3.scale.y = height / 10;
scene.add(windowMesh3);
const windowMesh4 = new THREE.Mesh(windowGeometry, windowMaterial); // 修正:outraTHREE 改为 THREE
windowMesh4.position.set(x + 1.51, (w * 1.5) + 1, z - 1.2); // 建筑侧面另一侧
windowMesh4.scale.y = height / 10;
scene.add(windowMesh4);
}
}
}
// 设置相机位置(斜角俯视)
camera.position.set(30, 30, 30); // 斜上方位置
camera.lookAt(0, 5, 0); // 聚焦于凯旋门中心
// 动画循环
function animate() {
requestAnimationFrame(animate);
// 添加旋转动画
scene.rotation.y += 0.002; // 减慢旋转速度
renderer.render(scene, camera);
}
animate();
// 处理窗口大小变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
效果图
版本3.0:优化建筑群
<!DOCTYPE html>
<html>
<head>
<title>3D超大精细凯旋门与多彩建筑群(斜角俯视)</title>
<style>
body { margin: 0; }
canvas { display: block; }
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
<script>
// 设置场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xffffff, 1); // 白色背景
document.body.appendChild(renderer.domElement);
// 添加光源(增强细节)
const ambientLight = new THREE.AmbientLight(0x404040, 1.2);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(30, 50, 30);
scene.add(directionalLight);
// 凯旋门材质(增加石材质感)
const archMaterial = new THREE.MeshStandardMaterial({
color: 0xD2B48C,
roughness: 0.8,
metalness: 0.2
});
// 精细凯旋门结构(宽13.5、高15、深6.6,放大1.5倍)
// 基座
const baseGeometry = new THREE.BoxGeometry(13.5, 1.5, 6.6);
const base = new THREE.Mesh(baseGeometry, archMaterial);
base.position.y = 0.75;
scene.add(base);
// 四个支撑柱子(更宽大,模拟拱门)
const pillarWidth = 2.2;
const pillarHeight = 12;
const pillarGeometry = new THREE.BoxGeometry(pillarWidth, pillarHeight, pillarWidth);
const frontLeftPillar = new THREE.Mesh(pillarGeometry, archMaterial);
frontLeftPillar.position.set(-5.65, 7.5, -2.2);
scene.add(frontLeftPillar);
const frontRightPillar = new THREE.Mesh(pillarGeometry, archMaterial);
frontRightPillar.position.set(5.65, 7.5, -2.2);
scene.add(frontRightPillar);
const backLeftPillar = new THREE.Mesh(pillarGeometry, archMaterial);
backLeftPillar.position.set(-5.65, 7.5, 2.2);
scene.add(backLeftPillar);
const backRightPillar = new THREE.Mesh(pillarGeometry, archMaterial);
backRightPillar.position.set(5.65, 7.5, 2.2);
scene.add(backRightPillar);
// 中间横梁(中央拱门顶部)
const midBeamGeometry = new THREE.BoxGeometry(13.5, 1, 6.6);
const midBeam = new THREE.Mesh(midBeamGeometry, archMaterial);
midBeam.position.y = 10;
scene.add(midBeam);
// 顶部attic
const atticGeometry = new THREE.BoxGeometry(13.5, 1.5, 6.6);
const attic = new THREE.Mesh(atticGeometry, archMaterial);
attic.position.y = 13.5;
scene.add(attic);
// 顶部雕塑(简化的Quadriga)
const quadrigaGeometry = new THREE.BoxGeometry(2, 1, 2);
const quadrigaMaterial = new THREE.MeshPhongMaterial({ color: 0xB8860B });
const quadriga = new THREE.Mesh(quadrigaGeometry, quadrigaMaterial);
quadriga.position.set(0, 14.5, 0);
scene.add(quadriga);
// Frieze(带状装饰,增加细节)
const friezeGeometry = new THREE.BoxGeometry(12.5, 0.4, 0.4);
const frieze = new THREE.Mesh(friezeGeometry, archMaterial);
frieze.position.set(0, 12.8, 3.1);
scene.add(frieze);
const friezeBack = frieze.clone();
friezeBack.position.z = -3.1;
scene.add(friezeBack);
// Frieze上的小装饰(模拟胜利盾牌)
const friezeDetailGeometry = new THREE.BoxGeometry(0.5, 0.3, 0.2);
for (let i = -5; i <= 5; i += 2) {
const detail = new THREE.Mesh(friezeDetailGeometry, quadrigaMaterial);
detail.position.set(i, 12.8, 3.2);
scene.add(detail);
const detailBack = detail.clone();
detailBack.position.z = -3.2;
scene.add(detailBack);
}
// 基座雕塑(四个雕塑组)
const sculptureGeometry = new THREE.BoxGeometry(2.2, 3, 0.3);
const sculptureMaterial = new THREE.MeshPhongMaterial({ color: 0xB8860B });
const sculpture1 = new THREE.Mesh(sculptureGeometry, sculptureMaterial);
sculpture1.position.set(-5.65, 3, -3.2);
scene.add(sculpture1);
const sculpture2 = sculpture1.clone();
sculpture2.position.set(5.65, 3, -3.2);
scene.add(sculpture2);
const sculpture3 = sculpture1.clone();
sculpture3.position.set(-5.65, 3, 3.2);
scene.add(sculpture3);
const sculpture4 = sculpture1.clone();
sculpture4.position.set(5.65, 3, 3.2);
scene.add(sculpture4);
// 扩展建筑群(高度4-8单位)
const buildingGeometry = new THREE.BoxGeometry(3, 10, 3);
const windowMaterial = new THREE.MeshPhongMaterial({ color: 0xFFFFFF });
const windowGeometry = new THREE.BoxGeometry(0.4, 0.4, 0.1);
const buildingColors = [0xFF6347, 0x4682B4, 0x32CD32, 0xFFD700, 0x9932CC, 0xFF4500, 0x00CED1];
const rings = 5;
const baseRadius = 25; // 增大半径以避免遮挡
const ringSpacing = 10;
for (let ring = 1; ring <= rings; ring++) {
const radius = baseRadius + (ring - 1) * ringSpacing;
const numBuildings = 8 + ring * 4;
for (let i = 0; i < numBuildings; i++) {
const angle = (i / numBuildings) * Math.PI * 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
const height = 4 + Math.random() * 4; // 高度4-8单位
const buildingMaterial = new THREE.MeshPhongMaterial({
color: buildingColors[Math.floor(Math.random() * buildingColors.length)]
});
const buildingMesh = new THREE.Mesh(buildingGeometry, buildingMaterial);
buildingMesh.position.set(x, height / 2, z);
buildingMesh.scale.y = height / 10;
scene.add(buildingMesh);
const numWindows = Math.floor(height / 2);
for (let w = 0; w < numWindows; w++) {
const windowMesh1 = new THREE.Mesh(windowGeometry, windowMaterial);
windowMesh1.position.set(x + 1.2, (w * 1.5) + 1, z + 1.51);
windowMesh1.scale.y = height / 10;
scene.add(windowMesh1);
const windowMesh2 = new THREE.Mesh(windowGeometry, windowMaterial);
windowMesh2.position.set(x - 1.2, (w * 1.5) + 1, z + 1.51);
windowMesh2.scale.y = height / 10;
scene.add(windowMesh2);
const windowMesh3 = new THREE.Mesh(windowGeometry, windowMaterial);
windowMesh3.position.set(x + 1.51, (w * 1.5) + 1, z + 1.2);
windowMesh3.scale.y = height / 10;
scene.add(windowMesh3);
const windowMesh4 = new THREE.Mesh(windowGeometry, windowMaterial);
windowMesh4.position.set(x + 1.51, (w * 1.5) + 1, z - 1.2);
windowMesh4.scale.y = height / 10;
scene.add(windowMesh4);
}
}
}
// 设置相机位置(斜角俯视,适应更大凯旋门)
camera.position.set(45, 45, 45);
camera.lookAt(0, 7.5, 0);
// 动画循环
function animate() {
requestAnimationFrame(animate);
scene.rotation.y += 0.002;
renderer.render(scene, camera);
}
animate();
// 处理窗口大小变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
图示效果
版本4.0:增加公路和车流
- 在原本凯旋门和建筑群的基础上,加上 环形公路 + 放射状公路 + 科技感灯光 + 内外环对向车流。
<!DOCTYPE html>
<html>
<head>
<title>3D超大精细凯旋门与多彩建筑群(斜角俯视)</title>
<style>
body { margin: 0; }
canvas { display: block; }
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
<script>
// 场景 & 相机 & 渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xffffff, 1);
document.body.appendChild(renderer.domElement);
// 灯光
scene.add(new THREE.AmbientLight(0x404040, 1.2));
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(30, 50, 30);
scene.add(directionalLight);
// 凯旋门材质
const archMaterial = new THREE.MeshStandardMaterial({color: 0xD2B48C, roughness: 0.8, metalness: 0.2});
// 凯旋门主体
const base = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1.5, 6.6), archMaterial);
base.position.y = 0.75; scene.add(base);
const pillarGeometry = new THREE.BoxGeometry(2.2, 12, 2.2);
const pillars = [
[-5.65, 7.5, -2.2], [5.65, 7.5, -2.2],
[-5.65, 7.5, 2.2], [5.65, 7.5, 2.2]
];
pillars.forEach(p => {
const m = new THREE.Mesh(pillarGeometry, archMaterial);
m.position.set(...p);
scene.add(m);
});
const midBeam = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1, 6.6), archMaterial);
midBeam.position.y = 10; scene.add(midBeam);
const attic = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1.5, 6.6), archMaterial);
attic.position.y = 13.5; scene.add(attic);
const quadrigaMaterial = new THREE.MeshPhongMaterial({color: 0xB8860B});
const quadriga = new THREE.Mesh(new THREE.BoxGeometry(2, 1, 2), quadrigaMaterial);
quadriga.position.set(0, 14.5, 0); scene.add(quadriga);
const friezeGeometry = new THREE.BoxGeometry(12.5, 0.4, 0.4);
const frieze = new THREE.Mesh(friezeGeometry, archMaterial);
frieze.position.set(0, 12.8, 3.1); scene.add(frieze);
const friezeBack = frieze.clone(); friezeBack.position.z = -3.1; scene.add(friezeBack);
const friezeDetailGeometry = new THREE.BoxGeometry(0.5, 0.3, 0.2);
for (let i = -5; i <= 5; i += 2) {
const detail = new THREE.Mesh(friezeDetailGeometry, quadrigaMaterial);
detail.position.set(i, 12.8, 3.2); scene.add(detail);
const detailBack = detail.clone(); detailBack.position.z = -3.2; scene.add(detailBack);
}
const sculptureGeometry = new THREE.BoxGeometry(2.2, 3, 0.3);
const sculptureMaterial = new THREE.MeshPhongMaterial({color: 0xB8860B});
const sculptures = [
[-5.65, 3, -3.2], [5.65, 3, -3.2],
[-5.65, 3, 3.2], [5.65, 3, 3.2]
];
sculptures.forEach(p => {
const s = new THREE.Mesh(sculptureGeometry, sculptureMaterial);
s.position.set(...p); scene.add(s);
});
// 建筑群
const buildingGeometry = new THREE.BoxGeometry(3, 10, 3);
const windowMaterial = new THREE.MeshPhongMaterial({color: 0xFFFFFF});
const windowGeometry = new THREE.BoxGeometry(0.4, 0.4, 0.1);
const buildingColors = [0xFF6347,0x4682B4,0x32CD32,0xFFD700,0x9932CC,0xFF4500,0x00CED1];
const rings = 5, baseRadius = 25, ringSpacing = 10;
for (let ring = 1; ring <= rings; ring++) {
const radius = baseRadius + (ring - 1) * ringSpacing;
const numBuildings = 8 + ring * 4;
for (let i = 0; i < numBuildings; i++) {
const angle = (i / numBuildings) * Math.PI * 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
const height = 4 + Math.random() * 4;
const buildingMaterial = new THREE.MeshPhongMaterial({
color: buildingColors[Math.floor(Math.random()*buildingColors.length)]
});
const buildingMesh = new THREE.Mesh(buildingGeometry, buildingMaterial);
buildingMesh.position.set(x, height/2, z);
buildingMesh.scale.y = height / 10;
scene.add(buildingMesh);
}
}
// ===== 道路 =====
const roadMaterial = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.9 });
const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 1 });
const roadRadius = baseRadius - 3;
const roadWidth = 4;
const roadGeometry = new THREE.RingGeometry(roadRadius - roadWidth / 2, roadRadius + roadWidth / 2, 128);
const roadMesh = new THREE.Mesh(roadGeometry, roadMaterial);
roadMesh.rotation.x = -Math.PI / 2;
scene.add(roadMesh);
const glowRingGeometry = new THREE.RingGeometry(roadRadius + roadWidth / 2, roadRadius + roadWidth / 2 + 0.3, 128);
const glowRingMesh = new THREE.Mesh(glowRingGeometry, glowMaterial);
glowRingMesh.rotation.x = -Math.PI / 2;
scene.add(glowRingMesh);
// 放射状公路
const radialRoadLength = 60;
const radialRoadWidth = 3;
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2;
const roadGeo = new THREE.BoxGeometry(radialRoadLength, 0.1, radialRoadWidth);
const road = new THREE.Mesh(roadGeo, roadMaterial);
road.position.set(Math.cos(angle) * (radialRoadLength / 2 + roadRadius), 0.05, Math.sin(angle) * (radialRoadLength / 2 + roadRadius));
road.rotation.y = -angle;
scene.add(road);
const glowGeo = new THREE.BoxGeometry(radialRoadLength, 0.05, 0.2);
const glow = new THREE.Mesh(glowGeo, glowMaterial);
glow.position.copy(road.position);
glow.rotation.y = road.rotation.y;
glow.position.y = 0.06;
scene.add(glow);
}
// ===== 动态车群(内外环对向行驶) =====
const cars = [];
const numCars = 30;
const innerRadius = roadRadius - 1;
const outerRadius = roadRadius + 1;
for (let i = 0; i < numCars; i++) {
const carGeometry = new THREE.BoxGeometry(1.5, 0.7, 0.8);
const carMaterial = new THREE.MeshStandardMaterial({
color: new THREE.Color(Math.random(), Math.random(), Math.random()),
emissive: new THREE.Color(Math.random(), Math.random(), Math.random()),
emissiveIntensity: 0.8
});
const car = new THREE.Mesh(carGeometry, carMaterial);
const isOuter = i % 2 === 0; // 偶数外环,奇数内环
car.userData = {
angle: Math.random() * Math.PI * 2,
speed: (0.002 + Math.random() * 0.002) * (isOuter ? 1 : -1),
radius: isOuter ? outerRadius : innerRadius
};
scene.add(car);
cars.push(car);
}
// 相机位置
camera.position.set(45, 45, 45);
camera.lookAt(0, 7.5, 0);
// 动画
function animate() {
requestAnimationFrame(animate);
cars.forEach(car => {
car.userData.angle += car.userData.speed;
car.position.set(
Math.cos(car.userData.angle) * car.userData.radius,
0.4,
Math.sin(car.userData.angle) * car.userData.radius
);
car.rotation.y = -car.userData.angle + Math.PI / 2;
});
scene.rotation.y += 0.002;
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
效果图
版本5.0:去除压在公路上的建筑
优化点
公路(环形和放射状)完全没有建筑压住
所有道路都畅通可见
建筑分布更合理
<!DOCTYPE html>
<html>
<head>
<title>3D凯旋门与畅通道路的建筑群</title>
<style>
body { margin: 0; }
canvas { display: block; }
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
<script>
// 场景 & 相机 & 渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xffffff, 1);
document.body.appendChild(renderer.domElement);
// 灯光
scene.add(new THREE.AmbientLight(0x404040, 1.2));
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(30, 50, 30);
scene.add(directionalLight);
// 凯旋门材质
const archMaterial = new THREE.MeshStandardMaterial({color: 0xD2B48C, roughness: 0.8, metalness: 0.2});
// 凯旋门主体
const base = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1.5, 6.6), archMaterial);
base.position.y = 0.75; scene.add(base);
const pillarGeometry = new THREE.BoxGeometry(2.2, 12, 2.2);
const pillars = [
[-5.65, 7.5, -2.2], [5.65, 7.5, -2.2],
[-5.65, 7.5, 2.2], [5.65, 7.5, 2.2]
];
pillars.forEach(p => {
const m = new THREE.Mesh(pillarGeometry, archMaterial);
m.position.set(...p);
scene.add(m);
});
const midBeam = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1, 6.6), archMaterial);
midBeam.position.y = 10; scene.add(midBeam);
const attic = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1.5, 6.6), archMaterial);
attic.position.y = 13.5; scene.add(attic);
const quadrigaMaterial = new THREE.MeshPhongMaterial({color: 0xB8860B});
const quadriga = new THREE.Mesh(new THREE.BoxGeometry(2, 1, 2), quadrigaMaterial);
quadriga.position.set(0, 14.5, 0); scene.add(quadriga);
const friezeGeometry = new THREE.BoxGeometry(12.5, 0.4, 0.4);
const frieze = new THREE.Mesh(friezeGeometry, archMaterial);
frieze.position.set(0, 12.8, 3.1); scene.add(frieze);
const friezeBack = frieze.clone(); friezeBack.position.z = -3.1; scene.add(friezeBack);
const friezeDetailGeometry = new THREE.BoxGeometry(0.5, 0.3, 0.2);
for (let i = -5; i <= 5; i += 2) {
const detail = new THREE.Mesh(friezeDetailGeometry, quadrigaMaterial);
detail.position.set(i, 12.8, 3.2); scene.add(detail);
const detailBack = detail.clone(); detailBack.position.z = -3.2; scene.add(detailBack);
}
const sculptureGeometry = new THREE.BoxGeometry(2.2, 3, 0.3);
const sculptureMaterial = new THREE.MeshPhongMaterial({color: 0xB8860B});
const sculptures = [
[-5.65, 3, -3.2], [5.65, 3, -3.2],
[-5.65, 3, 3.2], [5.65, 3, 3.2]
];
sculptures.forEach(p => {
const s = new THREE.Mesh(sculptureGeometry, sculptureMaterial);
s.position.set(...p); scene.add(s);
});
// ===== 建筑群(避开环形和放射状道路) =====
const buildingGeometry = new THREE.BoxGeometry(3, 10, 3);
const buildingColors = [0xFF6347,0x4682B4,0x32CD32,0xFFD700,0x9932CC,0xFF4500,0x00CED1];
const rings = 5, baseRadius = 25, ringSpacing = 10;
const radialAngles = []; // 放射状道路角度
const numRadials = 8;
for (let i = 0; i < numRadials; i++) {
radialAngles.push((i / numRadials) * Math.PI * 2);
}
for (let ring = 2; ring <= rings; ring++) { // 从第2圈开始
const radius = baseRadius + (ring - 1) * ringSpacing;
const numBuildings = 8 + ring * 4;
for (let i = 0; i < numBuildings; i++) {
const angle = (i / numBuildings) * Math.PI * 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
// 检测是否在放射状道路范围内
let onRadialRoad = false;
for (const roadAngle of radialAngles) {
const dx = x;
const dz = z;
const distFromRoadCenter = Math.abs(Math.sin(roadAngle) * dx - Math.cos(roadAngle) * dz);
if (distFromRoadCenter < 6) { // 道路两侧 6 米范围内不建楼
onRadialRoad = true;
break;
}
}
if (onRadialRoad) continue;
const height = 4 + Math.random() * 4;
const buildingMaterial = new THREE.MeshPhongMaterial({
color: buildingColors[Math.floor(Math.random()*buildingColors.length)]
});
const buildingMesh = new THREE.Mesh(buildingGeometry, buildingMaterial);
buildingMesh.position.set(x, height/2, z);
buildingMesh.scale.y = height / 10;
scene.add(buildingMesh);
}
}
// ===== 道路 =====
const roadMaterial = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.9 });
const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 1 });
const roadRadius = baseRadius - 3;
const roadWidth = 4;
const roadGeometry = new THREE.RingGeometry(roadRadius - roadWidth / 2, roadRadius + roadWidth / 2, 128);
const roadMesh = new THREE.Mesh(roadGeometry, roadMaterial);
roadMesh.rotation.x = -Math.PI / 2;
scene.add(roadMesh);
const glowRingGeometry = new THREE.RingGeometry(roadRadius + roadWidth / 2, roadRadius + roadWidth / 2 + 0.3, 128);
const glowRingMesh = new THREE.Mesh(glowRingGeometry, glowMaterial);
glowRingMesh.rotation.x = -Math.PI / 2;
scene.add(glowRingMesh);
// 放射状公路
const radialRoadLength = 60;
const radialRoadWidth = 3;
for (let angle of radialAngles) {
const roadGeo = new THREE.BoxGeometry(radialRoadLength, 0.1, radialRoadWidth);
const road = new THREE.Mesh(roadGeo, roadMaterial);
road.position.set(Math.cos(angle) * (radialRoadLength / 2 + roadRadius), 0.05, Math.sin(angle) * (radialRoadLength / 2 + roadRadius));
road.rotation.y = -angle;
scene.add(road);
const glowGeo = new THREE.BoxGeometry(radialRoadLength, 0.05, 0.2);
const glow = new THREE.Mesh(glowGeo, glowMaterial);
glow.position.copy(road.position);
glow.rotation.y = road.rotation.y;
glow.position.y = 0.06;
scene.add(glow);
}
// ===== 动态车群 =====
const cars = [];
const numCars = 30;
const innerRadius = roadRadius - 1;
const outerRadius = roadRadius + 1;
for (let i = 0; i < numCars; i++) {
const carGeometry = new THREE.BoxGeometry(1.5, 0.7, 0.8);
const carMaterial = new THREE.MeshStandardMaterial({
color: new THREE.Color(Math.random(), Math.random(), Math.random()),
emissive: new THREE.Color(Math.random(), Math.random(), Math.random()),
emissiveIntensity: 0.8
});
const car = new THREE.Mesh(carGeometry, carMaterial);
const isOuter = i % 2 === 0;
car.userData = {
angle: Math.random() * Math.PI * 2,
speed: (0.002 + Math.random() * 0.002) * (isOuter ? 1 : -1),
radius: isOuter ? outerRadius : innerRadius
};
scene.add(car);
cars.push(car);
}
// 相机
camera.position.set(45, 45, 45);
camera.lookAt(0, 7.5, 0);
// 动画
function animate() {
requestAnimationFrame(animate);
cars.forEach(car => {
car.userData.angle += car.userData.speed;
car.position.set(
Math.cos(car.userData.angle) * car.userData.radius,
0.4,
Math.sin(car.userData.angle) * car.userData.radius
);
car.rotation.y = -car.userData.angle + Math.PI / 2;
});
scene.rotation.y += 0.002;
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
效果图
版本6.0:优化车流群
环形公路:双向环形车流(内圈逆时针,外圈顺时针)
放射状公路:每条都有双向车流,到端点自动掉头
建筑:完全避开公路,不会挡路
统一的交通逻辑:汽车不再有两个完全独立的交通环路,而是具有可以是 或 的属性。这个单一的动画循环可以处理所有汽车,并允许动态路径切换。
userData.path
'ring'
'radial'
无缝路径转换:
径向到环形:当径向道路上的汽车到达终点 () 或中心 () 时,它会自动切换到 。然后,它计算出一个新的角度和速度,以继续沿环形道路行驶,从而创建平滑过渡。
car.userData.position > radialRoadLength
car.userData.position < 0
path
'ring'
环形到径向:当环路上的汽车经过径向道路交叉口时,它有很小的机会切换到径向道路。这是通过防止所有汽车同时改变路径并确保动态、逼真的流动进行控制的。
Math.random() < 0.01
改进的代码结构:汽车创建循环更加集中。创建汽车并给出初始路径和位置。然后,单个动画循环管理所有汽车的状态和运动,无论其当前路径如何。
动态摄像机:场景现在围绕原点旋转 ()。这让用户更好地感受到一个充满活力、不断运动的充满活力的城市。
scene.rotation.y += 0.002
<!DOCTYPE html>
<html>
<head>
<title>3D凯旋门与畅通道路的建筑群 - 优化版</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
<script>
// 场景 & 相机 & 渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xffffff, 1);
document.body.appendChild(renderer.domElement);
// 灯光
scene.add(new THREE.AmbientLight(0x404040, 1.2));
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(30, 50, 30);
scene.add(directionalLight);
// 凯旋门材质
const archMaterial = new THREE.MeshStandardMaterial({color: 0xD2B48C, roughness: 0.8, metalness: 0.2});
// 凯旋门主体
const base = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1.5, 6.6), archMaterial);
base.position.y = 0.75; scene.add(base);
const pillarGeometry = new THREE.BoxGeometry(2.2, 12, 2.2);
const pillars = [
[-5.65, 7.5, -2.2], [5.65, 7.5, -2.2],
[-5.65, 7.5, 2.2], [5.65, 7.5, 2.2]
];
pillars.forEach(p => {
const m = new THREE.Mesh(pillarGeometry, archMaterial);
m.position.set(...p);
scene.add(m);
});
const midBeam = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1, 6.6), archMaterial);
midBeam.position.y = 10; scene.add(midBeam);
const attic = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1.5, 6.6), archMaterial);
attic.position.y = 13.5; scene.add(attic);
const quadrigaMaterial = new THREE.MeshPhongMaterial({color: 0xB8860B});
const quadriga = new THREE.Mesh(new THREE.BoxGeometry(2, 1, 2), quadrigaMaterial);
quadriga.position.set(0, 14.5, 0); scene.add(quadriga);
const friezeGeometry = new THREE.BoxGeometry(12.5, 0.4, 0.4);
const frieze = new THREE.Mesh(friezeGeometry, archMaterial);
frieze.position.set(0, 12.8, 3.1); scene.add(frieze);
const friezeBack = frieze.clone(); friezeBack.position.z = -3.1; scene.add(friezeBack);
const friezeDetailGeometry = new THREE.BoxGeometry(0.5, 0.3, 0.2);
for (let i = -5; i <= 5; i += 2) {
const detail = new THREE.Mesh(friezeDetailGeometry, quadrigaMaterial);
detail.position.set(i, 12.8, 3.2); scene.add(detail);
const detailBack = detail.clone(); detailBack.position.z = -3.2; scene.add(detailBack);
}
const sculptureGeometry = new THREE.BoxGeometry(2.2, 3, 0.3);
const sculptureMaterial = new THREE.MeshPhongMaterial({color: 0xB8860B});
const sculptures = [
[-5.65, 3, -3.2], [5.65, 3, -3.2],
[-5.65, 3, 3.2], [5.65, 3, 3.2]
];
sculptures.forEach(p => {
const s = new THREE.Mesh(sculptureGeometry, sculptureMaterial);
s.position.set(...p); scene.add(s);
});
// ===== 建筑群 =====
const buildingGeometry = new THREE.BoxGeometry(3, 10, 3);
const buildingColors = [0xFF6347,0x4682B4,0x32CD32,0xFFD700,0x9932CC,0xFF4500,0x00CED1];
const rings = 5, baseRadius = 25, ringSpacing = 10;
const radialAngles = [];
const numRadials = 8;
for (let i = 0; i < numRadials; i++) {
radialAngles.push((i / numRadials) * Math.PI * 2);
}
for (let ring = 2; ring <= rings; ring++) {
const radius = baseRadius + (ring - 1) * ringSpacing;
const numBuildings = 8 + ring * 4;
for (let i = 0; i < numBuildings; i++) {
const angle = (i / numBuildings) * Math.PI * 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
let onRadialRoad = false;
for (const roadAngle of radialAngles) {
const dx = x;
const dz = z;
const distFromRoadCenter = Math.abs(Math.sin(roadAngle) * dx - Math.cos(roadAngle) * dz);
if (distFromRoadCenter < 6) {
onRadialRoad = true;
break;
}
}
if (onRadialRoad) continue;
const height = 4 + Math.random() * 4;
const buildingMaterial = new THREE.MeshPhongMaterial({
color: buildingColors[Math.floor(Math.random()*buildingColors.length)]
});
const buildingMesh = new THREE.Mesh(buildingGeometry, buildingMaterial);
buildingMesh.position.set(x, height/2, z);
buildingMesh.scale.y = height / 10;
scene.add(buildingMesh);
}
}
// ===== 道路 =====
const roadMaterial = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.9 });
const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 1 });
const roadRadius = baseRadius - 3;
const roadWidth = 6;
const roadGeometry = new THREE.RingGeometry(roadRadius - roadWidth / 2, roadRadius + roadWidth / 2, 128);
const roadMesh = new THREE.Mesh(roadGeometry, roadMaterial);
roadMesh.rotation.x = -Math.PI / 2;
scene.add(roadMesh);
const glowRingGeometry = new THREE.RingGeometry(roadRadius + roadWidth / 2, roadRadius + roadWidth / 2 + 0.3, 128);
const glowRingMesh = new THREE.Mesh(glowRingGeometry, glowMaterial);
glowRingMesh.rotation.x = -Math.PI / 2;
scene.add(glowRingMesh);
const radialRoadLength = 60;
const radialRoadWidth = 5;
for (let angle of radialAngles) {
const roadGeo = new THREE.BoxGeometry(radialRoadLength, 0.1, radialRoadWidth);
const road = new THREE.Mesh(roadGeo, roadMaterial);
road.position.set(Math.cos(angle) * (radialRoadLength / 2 + roadRadius), 0.05, Math.sin(angle) * (radialRoadLength / 2 + roadRadius));
road.rotation.y = -angle;
scene.add(road);
const glowGeoLeft = new THREE.BoxGeometry(radialRoadLength, 0.05, 0.2);
const glowLeft = new THREE.Mesh(glowGeoLeft, glowMaterial);
glowLeft.position.set(road.position.x + Math.sin(angle) * (radialRoadWidth / 2 + 0.1), 0.06, road.position.z - Math.cos(angle) * (radialRoadWidth / 2 + 0.1));
glowLeft.rotation.y = road.rotation.y;
scene.add(glowLeft);
const glowGeoRight = new THREE.BoxGeometry(radialRoadLength, 0.05, 0.2);
const glowRight = new THREE.Mesh(glowGeoRight, glowMaterial);
glowRight.position.set(road.position.x - Math.sin(angle) * (radialRoadWidth / 2 + 0.1), 0.06, road.position.z + Math.cos(angle) * (radialRoadWidth / 2 + 0.1));
glowRight.rotation.y = road.rotation.y;
scene.add(glowRight);
}
// ===== 优化车流 =====
const cars = [];
const carGeometry = new THREE.BoxGeometry(1.5, 0.7, 0.8);
const numCars = 100;
const laneOffset = radialRoadWidth / 4;
const innerRadius = roadRadius - roadWidth / 4;
const outerRadius = roadRadius + roadWidth / 4;
for (let i = 0; i < numCars; i++) {
const carMaterial = new THREE.MeshStandardMaterial({
color: new THREE.Color(Math.random(), Math.random(), Math.random()),
emissive: new THREE.Color(Math.random(), Math.random(), Math.random()),
emissiveIntensity: 0.8
});
const car = new THREE.Mesh(carGeometry, carMaterial);
const initialPath = Math.random() < 0.5 ? 'ring' : 'radial';
let initialAngle, initialDirection, initialRadius, initialPosition, initialLane;
if (initialPath === 'ring') {
initialAngle = Math.random() * Math.PI * 2;
initialDirection = Math.random() < 0.5 ? 1 : -1;
initialRadius = initialDirection === 1 ? outerRadius : innerRadius;
car.userData = {
path: 'ring',
angle: initialAngle,
speed: (0.0015 + Math.random() * 0.0025) * initialDirection,
radius: initialRadius,
lane: 0 // Not used for ring, but for consistency
};
} else {
initialAngle = radialAngles[Math.floor(Math.random() * numRadials)];
initialDirection = Math.random() < 0.5 ? 1 : -1;
initialPosition = Math.random() * radialRoadLength;
initialLane = initialDirection === 1 ? laneOffset : -laneOffset;
car.userData = {
path: 'radial',
angle: initialAngle,
position: initialPosition,
speed: (0.05 + Math.random() * 0.03) * initialDirection,
lane: initialLane
};
}
scene.add(car);
cars.push(car);
}
// 相机
camera.position.set(45, 45, 45);
camera.lookAt(0, 7.5, 0);
// 动画
function animate() {
requestAnimationFrame(animate);
cars.forEach(car => {
if (car.userData.path === 'ring') {
car.userData.angle += car.userData.speed;
// 检查是否接近放射状道路的交叉口
const currentAngle = car.userData.angle % (Math.PI * 2);
for (const radialAngle of radialAngles) {
if (Math.abs(currentAngle - radialAngle) < 0.05 && Math.random() < 0.01) { // 随机决定是否转向
const newDirection = car.userData.speed > 0 ? 1 : -1;
car.userData.path = 'radial';
car.userData.angle = radialAngle;
car.userData.position = newDirection > 0 ? 0 : radialRoadLength;
car.userData.speed = (0.05 + Math.random() * 0.03) * newDirection;
car.userData.lane = car.userData.speed > 0 ? laneOffset : -laneOffset;
break;
}
}
car.position.set(
Math.cos(car.userData.angle) * car.userData.radius,
0.4,
Math.sin(car.userData.angle) * car.userData.radius
);
car.rotation.y = -car.userData.angle + Math.PI / 2;
} else if (car.userData.path === 'radial') {
car.userData.position += car.userData.speed;
// 检查是否到达道路尽头或中心
if ((car.userData.speed > 0 && car.userData.position > radialRoadLength) || (car.userData.speed < 0 && car.userData.position < 0)) {
// 切换到环形道路
const newDirection = car.userData.speed > 0 ? 1 : -1;
car.userData.path = 'ring';
car.userData.angle = car.userData.angle + (Math.PI / 2); // 从径向到环形,角度需要偏移
car.userData.speed = (0.0015 + Math.random() * 0.0025) * newDirection;
car.userData.radius = newDirection > 0 ? outerRadius : innerRadius;
}
const baseX = Math.cos(car.userData.angle) * (car.userData.position + roadRadius);
const baseZ = Math.sin(car.userData.angle) * (car.userData.position + roadRadius);
const offsetX = Math.sin(car.userData.angle) * car.userData.lane;
const offsetZ = -Math.cos(car.userData.angle) * car.userData.lane;
car.position.set(baseX + offsetX, 0.4, baseZ + offsetZ);
car.rotation.y = -car.userData.angle + (car.userData.speed > 0 ? 0 : Math.PI);
}
});
scene.rotation.y += 0.002;
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
效果图
版本7.0:添加烟花效果
<!DOCTYPE html>
<html>
<head>
<title>3D凯旋门与畅通道路的建筑群 - 天蓝色背景版</title>
<style>
body { margin: 0; overflow: hidden; font-family: sans-serif; }
canvas { display: block; }
#controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
border: none;
background-color: rgba(0, 0, 0, 0.2);
color: white;
border-radius: 5px;
}
</style>
</head>
<body>
<button id="controls">启动阶梯式烟花</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
<script>
// 场景 & 相机 & 渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x87CEEB, 1); // 天蓝色背景
document.body.appendChild(renderer.domElement);
// 灯光
scene.add(new THREE.AmbientLight(0x404040, 1.2));
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(30, 50, 30);
scene.add(directionalLight);
// 地面
const groundGeometry = new THREE.PlaneGeometry(2000, 2000);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x333333 }); // 接近公路的颜色
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
// 凯旋门材质
const archMaterial = new THREE.MeshStandardMaterial({color: 0xD2B48C, roughness: 0.8, metalness: 0.2}); // 黄褐色凯旋门
// 凯旋门主体
const base = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1.5, 6.6), archMaterial);
base.position.y = 0.75; scene.add(base);
const pillarGeometry = new THREE.BoxGeometry(2.2, 12, 2.2);
const pillars = [
[-5.65, 7.5, -2.2], [5.65, 7.5, -2.2],
[-5.65, 7.5, 2.2], [5.65, 7.5, 2.2]
];
pillars.forEach(p => {
const m = new THREE.Mesh(pillarGeometry, archMaterial);
m.position.set(...p);
scene.add(m);
});
const midBeam = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1, 6.6), archMaterial);
midBeam.position.y = 10; scene.add(midBeam);
const attic = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1.5, 6.6), archMaterial);
attic.position.y = 13.5; scene.add(attic);
const quadrigaMaterial = new THREE.MeshPhongMaterial({color: 0xB8860B});
const quadriga = new THREE.Mesh(new THREE.BoxGeometry(2, 1, 2), quadrigaMaterial);
quadriga.position.set(0, 14.5, 0); scene.add(quadriga);
const friezeGeometry = new THREE.BoxGeometry(12.5, 0.4, 0.4);
const frieze = new THREE.Mesh(friezeGeometry, archMaterial);
frieze.position.set(0, 12.8, 3.1); scene.add(frieze);
const friezeBack = frieze.clone(); friezeBack.position.z = -3.1; scene.add(friezeBack);
const friezeDetailGeometry = new THREE.BoxGeometry(0.5, 0.3, 0.2);
for (let i = -5; i <= 5; i += 2) {
const detail = new THREE.Mesh(friezeDetailGeometry, quadrigaMaterial);
detail.position.set(i, 12.8, 3.2); scene.add(detail);
const detailBack = detail.clone(); detailBack.position.z = -3.2; scene.add(detailBack);
}
const sculptureGeometry = new THREE.BoxGeometry(2.2, 3, 0.3);
const sculptureMaterial = new THREE.MeshPhongMaterial({color: 0xB8860B});
const sculptures = [
[-5.65, 3, -3.2], [5.65, 3, -3.2],
[-5.65, 3, 3.2], [5.65, 3, 3.2]
];
sculptures.forEach(p => {
const s = new THREE.Mesh(sculptureGeometry, sculptureMaterial);
s.position.set(...p); scene.add(s);
});
// ===== 建筑群 =====
const buildingGeometry = new THREE.BoxGeometry(3, 10, 3);
const buildingColors = [0xFF6347,0x4682B4,0x32CD32,0xFFD700,0x9932CC,0xFF4500,0x00CED1];
const rings = 5, baseRadius = 25, ringSpacing = 10;
const radialAngles = [];
const numRadials = 8;
for (let i = 0; i < numRadials; i++) {
radialAngles.push((i / numRadials) * Math.PI * 2);
}
for (let ring = 2; ring <= rings; ring++) {
const radius = baseRadius + (ring - 1) * ringSpacing;
const numBuildings = 8 + ring * 4;
for (let i = 0; i < numBuildings; i++) {
const angle = (i / numBuildings) * Math.PI * 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
let onRadialRoad = false;
for (const roadAngle of radialAngles) {
const dx = x;
const dz = z;
const distFromRoadCenter = Math.abs(Math.sin(roadAngle) * dx - Math.cos(roadAngle) * dz);
if (distFromRoadCenter < 6) {
onRadialRoad = true;
break;
}
}
if (onRadialRoad) continue;
const height = 4 + Math.random() * 4;
const buildingMaterial = new THREE.MeshPhongMaterial({
color: buildingColors[Math.floor(Math.random()*buildingColors.length)]
});
const buildingMesh = new THREE.Mesh(buildingGeometry, buildingMaterial);
buildingMesh.position.set(x, height/2, z);
buildingMesh.scale.y = height / 10;
scene.add(buildingMesh);
}
}
// ===== 道路 =====
const roadMaterial = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.9 });
const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 1 });
const roadRadius = baseRadius - 3;
const roadWidth = 6;
const roadGeometry = new THREE.RingGeometry(roadRadius - roadWidth / 2, roadRadius + roadWidth / 2, 128);
const roadMesh = new THREE.Mesh(roadGeometry, roadMaterial);
roadMesh.rotation.x = -Math.PI / 2;
scene.add(roadMesh);
// 移除闪烁效果,直接添加不闪烁的发光环
const glowRingGeometry = new THREE.RingGeometry(roadRadius + roadWidth / 2, roadRadius + roadWidth / 2 + 0.3, 128);
const glowRingMesh = new THREE.Mesh(glowRingGeometry, glowMaterial);
glowRingMesh.rotation.x = -Math.PI / 2;
scene.add(glowRingMesh);
const radialRoadLength = 60;
const radialRoadWidth = 5;
for (let angle of radialAngles) {
const roadGeo = new THREE.BoxGeometry(radialRoadLength, 0.1, radialRoadWidth);
const road = new THREE.Mesh(roadGeo, roadMaterial);
road.position.set(Math.cos(angle) * (radialRoadLength / 2 + roadRadius), 0.05, Math.sin(angle) * (radialRoadLength / 2 + roadRadius));
road.rotation.y = -angle;
scene.add(road);
const glowGeoLeft = new THREE.BoxGeometry(radialRoadLength, 0.05, 0.2);
const glowLeft = new THREE.Mesh(glowGeoLeft, glowMaterial);
glowLeft.position.set(road.position.x + Math.sin(angle) * (radialRoadWidth / 2 + 0.1), 0.06, road.position.z - Math.cos(angle) * (radialRoadWidth / 2 + 0.1));
glowLeft.rotation.y = road.rotation.y;
scene.add(glowLeft);
const glowGeoRight = new THREE.BoxGeometry(radialRoadLength, 0.05, 0.2);
const glowRight = new THREE.Mesh(glowGeoRight, glowMaterial);
glowRight.position.set(road.position.x - Math.sin(angle) * (radialRoadWidth / 2 + 0.1), 0.06, road.position.z + Math.cos(angle) * (radialRoadWidth / 2 + 0.1));
glowRight.rotation.y = road.rotation.y;
scene.add(glowRight);
}
// ===== 优化车流 =====
const cars = [];
const carGeometry = new THREE.BoxGeometry(1.5, 0.7, 0.8);
const numCars = 100;
const laneOffset = radialRoadWidth / 4;
const innerRadius = roadRadius - roadWidth / 4;
const outerRadius = roadRadius + roadWidth / 4;
for (let i = 0; i < numCars; i++) {
const carMaterial = new THREE.MeshStandardMaterial({
color: new THREE.Color(Math.random(), Math.random(), Math.random()),
emissive: new THREE.Color(Math.random(), Math.random(), Math.random()),
emissiveIntensity: 0.8
});
const car = new THREE.Mesh(carGeometry, carMaterial);
const initialPath = Math.random() < 0.5 ? 'ring' : 'radial';
let initialAngle, initialDirection, initialRadius, initialPosition, initialLane;
if (initialPath === 'ring') {
initialAngle = Math.random() * Math.PI * 2;
initialDirection = Math.random() < 0.5 ? 1 : -1;
initialRadius = initialDirection === 1 ? outerRadius : innerRadius;
car.userData = {
path: 'ring',
angle: initialAngle,
speed: (0.0015 + Math.random() * 0.0025) * initialDirection,
radius: initialRadius,
lane: 0
};
} else {
initialAngle = radialAngles[Math.floor(Math.random() * numRadials)];
initialDirection = Math.random() < 0.5 ? 1 : -1;
initialPosition = Math.random() * radialRoadLength;
initialLane = initialDirection === 1 ? laneOffset : -laneOffset;
car.userData = {
path: 'radial',
angle: initialAngle,
position: initialPosition,
speed: (0.05 + Math.random() * 0.03) * initialDirection,
lane: initialLane
};
}
scene.add(car);
cars.push(car);
}
// ===== 烟花代码开始 =====
const fireworks = [];
const particleGeometry = new THREE.SphereGeometry(0.08, 8, 8);
const innerCircleRadius = roadRadius - roadWidth / 2;
// 烟花爆炸函数
function createExplosion(originPosition, color) {
const numParticles = 60 + Math.floor(Math.random() * 30);
const explosionRadius = 0.5 + Math.random();
for (let i = 0; i < numParticles; i++) {
const particleMaterial = new THREE.MeshBasicMaterial({ color: color });
const particle = new THREE.Mesh(particleGeometry, particleMaterial);
particle.position.copy(originPosition);
const velocity = new THREE.Vector3(
(Math.random() - 0.5) * explosionRadius,
(Math.random() - 0.5) * explosionRadius,
(Math.random() - 0.5) * explosionRadius
);
particle.userData = {
velocity: velocity,
life: 1.5 + Math.random() * 1,
state: 'exploded'
};
fireworks.push(particle);
scene.add(particle);
}
}
// 发射烟花(升空)
function launchFirework(startPosition) {
const rocketMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const rocket = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.3, 0.1), rocketMaterial);
rocket.position.copy(startPosition);
// 爆炸高度比凯旋门低一些,约12-18
const targetHeight = 12 + Math.random() * 6;
const color = new THREE.Color().setHSL(Math.random(), 1, 0.5).getHex();
rocket.userData = {
velocity: new THREE.Vector3(0, 0.35 + Math.random() * 0.25, 0),
state: 'rising',
targetY: targetHeight,
color: color
};
fireworks.push(rocket);
scene.add(rocket);
}
// 场景初始化时的所有烟花一起升空
function launchInitialFireworks() {
for (let i = 0; i < 15; i++) {
const angle = (i / 15) * Math.PI * 2;
const radius = Math.random() * innerCircleRadius;
const position = new THREE.Vector3(Math.cos(angle) * radius, 0.1, Math.sin(angle) * radius);
launchFirework(position);
}
}
// 间歇性发射单个烟花
function launchIntermittentFirework() {
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * innerCircleRadius;
const position = new THREE.Vector3(Math.cos(angle) * radius, 0.1, Math.sin(angle) * radius);
launchFirework(position);
}
// 阶梯式环形发射烟花
function launchTieredFireworks() {
const numTiers = 6;
const delayPerTier = 400; // 毫秒
const baseHeight = 12;
const heightStep = 3; // 阶梯高度
for (let i = 0; i < numTiers; i++) {
setTimeout(() => {
const angle = (i / numTiers) * Math.PI * 2;
const radius = innerCircleRadius * 0.8;
const position = new THREE.Vector3(Math.cos(angle) * radius, 0.1, Math.sin(angle) * radius);
const rocketMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const rocket = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.3, 0.1), rocketMaterial);
rocket.position.copy(position);
rocket.userData = {
velocity: new THREE.Vector3(0, 0.35, 0),
state: 'rising',
targetY: baseHeight + i * heightStep,
color: new THREE.Color().setHSL(Math.random(), 1, 0.5).getHex()
};
fireworks.push(rocket);
scene.add(rocket);
}, i * delayPerTier);
}
}
function updateFireworks() {
for (let i = fireworks.length - 1; i >= 0; i--) {
const p = fireworks [i];
if (p.userData.state === 'rising') {
p.position.add(p.userData.velocity);
if (p.position.y >= p.userData.targetY) {
createExplosion(p.position, p.userData.color);
scene.remove(p);
fireworks.splice(i, 1);
}
} else if (p.userData.state === 'exploded') {
p.userData.life -= 0.015;
p.position.add(p.userData.velocity);
p.material.opacity = Math.max(0, p.userData.life / 2);
p.material.transparent = true;
p.userData.velocity.y -= 0.002;
if (p.userData.life <= 0) {
scene.remove(p);
fireworks.splice(i, 1);
}
}
}
}
// ===== 烟花代码结束 =====
// 相机
camera.position.set(45, 45, 45);
camera.lookAt(0, 7.5, 0);
// 动画
function animate() {
requestAnimationFrame(animate);
cars.forEach(car => {
if (car.userData.path === 'ring') {
car.userData.angle += car.userData.speed;
const currentAngle = car.userData.angle % (Math.PI * 2);
for (const radialAngle of radialAngles) {
if (Math.abs(currentAngle - radialAngle) < 0.05 && Math.random() < 0.01) {
const newDirection = car.userData.speed > 0 ? 1 : -1;
car.userData.path = 'radial';
car.userData.angle = radialAngle;
car.userData.position = newDirection > 0 ? 0 : radialRoadLength;
car.userData.speed = (0.05 + Math.random() * 0.03) * newDirection;
car.userData.lane = car.userData.speed > 0 ? laneOffset : -laneOffset;
break;
}
}
car.position.set(
Math.cos(car.userData.angle) * car.userData.radius,
0.4,
Math.sin(car.userData.angle) * car.userData.radius
);
car.rotation.y = -car.userData.angle + Math.PI / 2;
} else if (car.userData.path === 'radial') {
car.userData.position += car.userData.speed;
if ((car.userData.speed > 0 && car.userData.position > radialRoadLength) || (car.userData.speed < 0 && car.userData.position < 0)) {
const newDirection = car.userData.speed > 0 ? 1 : -1;
car.userData.path = 'ring';
car.userData.angle = car.userData.angle + (Math.PI / 2);
car.userData.speed = (0.0015 + Math.random() * 0.0025) * newDirection;
car.userData.radius = newDirection > 0 ? outerRadius : innerRadius;
}
const baseX = Math.cos(car.userData.angle) * (car.userData.position + roadRadius);
const baseZ = Math.sin(car.userData.angle) * (car.userData.position + roadRadius);
const offsetX = Math.sin(car.userData.angle) * car.userData.lane;
const offsetZ = -Math.cos(car.userData.angle) * car.userData.lane;
car.position.set(baseX + offsetX, 0.4, baseZ + offsetZ);
car.rotation.y = -car.userData.angle + (car.userData.speed > 0 ? 0 : Math.PI);
}
});
// 间歇性发射新烟花
if (Math.random() < 0.005) {
launchIntermittentFirework();
}
// 更新烟花状态
updateFireworks();
scene.rotation.y += 0.002;
renderer.render(scene, camera);
}
// 初始烟花发射
launchInitialFireworks();
animate();
// 按钮点击事件
document.getElementById('controls').addEventListener('click', launchTieredFireworks);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
显示效果
版本8.0:添加树木
背景颜色: 天蓝色
地面颜色: 略浅于公路的深灰色
凯旋门颜色: 黄褐色
树木: 在环形道路内侧和放射状公路两侧都增加了树木
放射状树木: 去除了最靠近环形公路的那一圈树木
<!DOCTYPE html>
<html>
<head>
<title>3D凯旋门与畅通道路的建筑群 - 最终版</title>
<style>
body { margin: 0; overflow: hidden; font-family: sans-serif; }
canvas { display: block; }
#controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
border: none;
background-color: rgba(0, 0, 0, 0.2);
color: white;
border-radius: 5px;
}
</style>
</head>
<body>
<button id="controls">启动阶梯式烟花</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
<script>
// 场景 & 相机 & 渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x87CEEB, 1); // 天蓝色背景
document.body.appendChild(renderer.domElement);
// 灯光
scene.add(new THREE.AmbientLight(0x404040, 1.2));
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(30, 50, 30);
scene.add(directionalLight);
// 地面
const groundGeometry = new THREE.PlaneGeometry(2000, 2000);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x333333 }); // 接近公路的颜色
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
// 凯旋门材质
const archMaterial = new THREE.MeshStandardMaterial({color: 0xD2B48C, roughness: 0.8, metalness: 0.2}); // 黄褐色凯旋门
// 凯旋门主体
const base = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1.5, 6.6), archMaterial);
base.position.y = 0.75; scene.add(base);
const pillarGeometry = new THREE.BoxGeometry(2.2, 12, 2.2);
const pillars = [
[-5.65, 7.5, -2.2], [5.65, 7.5, -2.2],
[-5.65, 7.5, 2.2], [5.65, 7.5, 2.2]
];
pillars.forEach(p => {
const m = new THREE.Mesh(pillarGeometry, archMaterial);
m.position.set(...p);
scene.add(m);
});
const midBeam = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1, 6.6), archMaterial);
midBeam.position.y = 10; scene.add(midBeam);
const attic = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1.5, 6.6), archMaterial);
attic.position.y = 13.5; scene.add(attic);
const quadrigaMaterial = new THREE.MeshPhongMaterial({color: 0xB8860B});
const quadriga = new THREE.Mesh(new THREE.BoxGeometry(2, 1, 2), quadrigaMaterial);
quadriga.position.set(0, 14.5, 0); scene.add(quadriga);
const friezeGeometry = new THREE.BoxGeometry(12.5, 0.4, 0.4);
const frieze = new THREE.Mesh(friezeGeometry, archMaterial);
frieze.position.set(0, 12.8, 3.1); scene.add(frieze);
const friezeBack = frieze.clone(); friezeBack.position.z = -3.1; scene.add(friezeBack);
const friezeDetailGeometry = new THREE.BoxGeometry(0.5, 0.3, 0.2);
for (let i = -5; i <= 5; i += 2) {
const detail = new THREE.Mesh(friezeDetailGeometry, quadrigaMaterial);
detail.position.set(i, 12.8, 3.2); scene.add(detail);
const detailBack = detail.clone(); detailBack.position.z = -3.2; scene.add(detailBack);
}
const sculptureGeometry = new THREE.BoxGeometry(2.2, 3, 0.3);
const sculptureMaterial = new THREE.MeshPhongMaterial({color: 0xB8860B});
const sculptures = [
[-5.65, 3, -3.2], [5.65, 3, -3.2],
[-5.65, 3, 3.2], [5.65, 3, 3.2]
];
sculptures.forEach(p => {
const s = new THREE.Mesh(sculptureGeometry, sculptureMaterial);
s.position.set(...p); scene.add(s);
});
// ===== 建筑群 =====
const buildingGeometry = new THREE.BoxGeometry(3, 10, 3);
const buildingColors = [0xFF6347,0x4682B4,0x32CD32,0xFFD700,0x9932CC,0xFF4500,0x00CED1];
const rings = 5, baseRadius = 25, ringSpacing = 10;
const radialAngles = [];
const numRadials = 8;
for (let i = 0; i < numRadials; i++) {
radialAngles.push((i / numRadials) * Math.PI * 2);
}
for (let ring = 2; ring <= rings; ring++) {
const radius = baseRadius + (ring - 1) * ringSpacing;
const numBuildings = 8 + ring * 4;
for (let i = 0; i < numBuildings; i++) {
const angle = (i / numBuildings) * Math.PI * 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
let onRadialRoad = false;
for (const roadAngle of radialAngles) {
const dx = x;
const dz = z;
const distFromRoadCenter = Math.abs(Math.sin(roadAngle) * dx - Math.cos(roadAngle) * dz);
if (distFromRoadCenter < 6) {
onRadialRoad = true;
break;
}
}
if (onRadialRoad) continue;
const height = 4 + Math.random() * 4;
const buildingMaterial = new THREE.MeshPhongMaterial({
color: buildingColors[Math.floor(Math.random()*buildingColors.length)]
});
const buildingMesh = new THREE.Mesh(buildingGeometry, buildingMaterial);
buildingMesh.position.set(x, height/2, z);
buildingMesh.scale.y = height / 10;
scene.add(buildingMesh);
}
}
// ===== 道路 =====
const roadMaterial = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.9 });
const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 1 });
const roadRadius = baseRadius - 3;
const roadWidth = 6;
const roadGeometry = new THREE.RingGeometry(roadRadius - roadWidth / 2, roadRadius + roadWidth / 2, 128);
const roadMesh = new THREE.Mesh(roadGeometry, roadMaterial);
roadMesh.rotation.x = -Math.PI / 2;
scene.add(roadMesh);
// 移除闪烁效果,直接添加不闪烁的发光环
const glowRingGeometry = new THREE.RingGeometry(roadRadius + roadWidth / 2, roadRadius + roadWidth / 2 + 0.3, 128);
const glowRingMesh = new THREE.Mesh(glowRingGeometry, glowMaterial);
glowRingMesh.rotation.x = -Math.PI / 2;
scene.add(glowRingMesh);
const radialRoadLength = 60;
const radialRoadWidth = 5;
for (let angle of radialAngles) {
const roadGeo = new THREE.BoxGeometry(radialRoadLength, 0.1, radialRoadWidth);
const road = new THREE.Mesh(roadGeo, roadMaterial);
road.position.set(Math.cos(angle) * (radialRoadLength / 2 + roadRadius), 0.05, Math.sin(angle) * (radialRoadLength / 2 + roadRadius));
road.rotation.y = -angle;
scene.add(road);
const glowGeoLeft = new THREE.BoxGeometry(radialRoadLength, 0.05, 0.2);
const glowLeft = new THREE.Mesh(glowGeoLeft, glowMaterial);
glowLeft.position.set(road.position.x + Math.sin(angle) * (radialRoadWidth / 2 + 0.1), 0.06, road.position.z - Math.cos(angle) * (radialRoadWidth / 2 + 0.1));
glowLeft.rotation.y = road.rotation.y;
scene.add(glowLeft);
const glowGeoRight = new THREE.BoxGeometry(radialRoadLength, 0.05, 0.2);
const glowRight = new THREE.Mesh(glowGeoRight, glowMaterial);
glowRight.position.set(road.position.x - Math.sin(angle) * (radialRoadWidth / 2 + 0.1), 0.06, road.position.z + Math.cos(angle) * (radialRoadWidth / 2 + 0.1));
glowRight.rotation.y = road.rotation.y;
scene.add(glowRight);
}
// ===== 树木 =====
const treeTrunkGeometry = new THREE.CylinderGeometry(0.3, 0.3, 2, 8);
const treeLeavesGeometry = new THREE.ConeGeometry(1.5, 3, 8);
const treeTrunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
const treeLeavesMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22 });
function createTree(x, z) {
const trunk = new THREE.Mesh(treeTrunkGeometry, treeTrunkMaterial);
trunk.position.set(x, 1, z);
scene.add(trunk);
const leaves = new THREE.Mesh(treeLeavesGeometry, treeLeavesMaterial);
leaves.position.set(x, 3.5, z);
scene.add(leaves);
}
// 在凯旋门和道路之间生成树林
const innerForestRadius = 15;
const outerForestRadius = roadRadius - roadWidth / 2 - 2;
const numForestTrees = 150;
for (let i = 0; i < numForestTrees; i++) {
const angle = Math.random() * Math.PI * 2;
const radius = innerForestRadius + Math.random() * (outerForestRadius - innerForestRadius);
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
createTree(x, z);
}
// 沿径向公路两侧生成树木,跳过最内侧一圈
const treeSpacing = 6;
const radialOffset = radialRoadWidth / 2 + 1;
for (let angle of radialAngles) {
for (let i = 1; i < radialRoadLength / treeSpacing; i++) { // i从1开始,跳过最里面一圈
const dist = i * treeSpacing;
// 左侧树木
const xLeft = Math.cos(angle) * (dist + roadRadius) + Math.sin(angle) * radialOffset;
const zLeft = Math.sin(angle) * (dist + roadRadius) - Math.cos(angle) * radialOffset;
createTree(xLeft, zLeft);
// 右侧树木
const xRight = Math.cos(angle) * (dist + roadRadius) - Math.sin(angle) * radialOffset;
const zRight = Math.sin(angle) * (dist + roadRadius) + Math.cos(angle) * radialOffset;
createTree(xRight, zRight);
}
}
// ===== 优化车流 =====
const cars = [];
const carGeometry = new THREE.BoxGeometry(1.5, 0.7, 0.8);
const numCars = 100;
const laneOffset = radialRoadWidth / 4;
const innerRadius = roadRadius - roadWidth / 4;
const outerRadius = roadRadius + roadWidth / 4;
for (let i = 0; i < numCars; i++) {
const carMaterial = new THREE.MeshStandardMaterial({
color: new THREE.Color(Math.random(), Math.random(), Math.random()),
emissive: new THREE.Color(Math.random(), Math.random(), Math.random()),
emissiveIntensity: 0.8
});
const car = new THREE.Mesh(carGeometry, carMaterial);
const initialPath = Math.random() < 0.5 ? 'ring' : 'radial';
let initialAngle, initialDirection, initialRadius, initialPosition, initialLane;
if (initialPath === 'ring') {
initialAngle = Math.random() * Math.PI * 2;
initialDirection = Math.random() < 0.5 ? 1 : -1;
initialRadius = initialDirection === 1 ? outerRadius : innerRadius;
car.userData = {
path: 'ring',
angle: initialAngle,
speed: (0.0015 + Math.random() * 0.0025) * initialDirection,
radius: initialRadius,
lane: 0
};
} else {
initialAngle = radialAngles[Math.floor(Math.random() * numRadials)];
initialDirection = Math.random() < 0.5 ? 1 : -1;
initialPosition = Math.random() * radialRoadLength;
initialLane = initialDirection === 1 ? laneOffset : -laneOffset;
car.userData = {
path: 'radial',
angle: initialAngle,
position: initialPosition,
speed: (0.05 + Math.random() * 0.03) * initialDirection,
lane: initialLane
};
}
scene.add(car);
cars.push(car);
}
// ===== 烟花代码开始 =====
const fireworks = [];
const particleGeometry = new THREE.SphereGeometry(0.08, 8, 8);
const innerCircleRadius = roadRadius - roadWidth / 2;
// 烟花爆炸函数
function createExplosion(originPosition, color) {
const numParticles = 60 + Math.floor(Math.random() * 30);
const explosionRadius = 0.5 + Math.random();
for (let i = 0; i < numParticles; i++) {
const particleMaterial = new THREE.MeshBasicMaterial({ color: color });
const particle = new THREE.Mesh(particleGeometry, particleMaterial);
particle.position.copy(originPosition);
const velocity = new THREE.Vector3(
(Math.random() - 0.5) * explosionRadius,
(Math.random() - 0.5) * explosionRadius,
(Math.random() - 0.5) * explosionRadius
);
particle.userData = {
velocity: velocity,
life: 1.5 + Math.random() * 1,
state: 'exploded'
};
fireworks.push(particle);
scene.add(particle);
}
}
// 发射烟花(升空)
function launchFirework(startPosition) {
const rocketMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const rocket = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.3, 0.1), rocketMaterial);
rocket.position.copy(startPosition);
// 爆炸高度比凯旋门低一些,约12-18
const targetHeight = 12 + Math.random() * 6;
const color = new THREE.Color().setHSL(Math.random(), 1, 0.5).getHex();
rocket.userData = {
velocity: new THREE.Vector3(0, 0.35 + Math.random() * 0.25, 0),
state: 'rising',
targetY: targetHeight,
color: color
};
fireworks.push(rocket);
scene.add(rocket);
}
// 场景初始化时的所有烟花一起升空
function launchInitialFireworks() {
for (let i = 0; i < 15; i++) {
const angle = (i / 15) * Math.PI * 2;
const radius = Math.random() * innerCircleRadius;
const position = new THREE.Vector3(Math.cos(angle) * radius, 0.1, Math.sin(angle) * radius);
launchFirework(position);
}
}
// 间歇性发射单个烟花
function launchIntermittentFirework() {
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * innerCircleRadius;
const position = new THREE.Vector3(Math.cos(angle) * radius, 0.1, Math.sin(angle) * radius);
launchFirework(position);
}
// 阶梯式环形发射烟花
function launchTieredFireworks() {
const numTiers = 6;
const delayPerTier = 400; // 毫秒
const baseHeight = 12;
const heightStep = 3; // 阶梯高度
for (let i = 0; i < numTiers; i++) {
setTimeout(() => {
const angle = (i / numTiers) * Math.PI * 2;
const radius = innerCircleRadius * 0.8;
const position = new THREE.Vector3(Math.cos(angle) * radius, 0.1, Math.sin(angle) * radius);
const rocketMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const rocket = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.3, 0.1), rocketMaterial);
rocket.position.copy(position);
rocket.userData = {
velocity: new THREE.Vector3(0, 0.35, 0),
state: 'rising',
targetY: baseHeight + i * heightStep,
color: new THREE.Color().setHSL(Math.random(), 1, 0.5).getHex()
};
fireworks.push(rocket);
scene.add(rocket);
}, i * delayPerTier);
}
}
function updateFireworks() {
for (let i = fireworks.length - 1; i >= 0; i--) {
const p = fireworks [i];
if (p.userData.state === 'rising') {
p.position.add(p.userData.velocity);
if (p.position.y >= p.userData.targetY) {
createExplosion(p.position, p.userData.color);
scene.remove(p);
fireworks.splice(i, 1);
}
} else if (p.userData.state === 'exploded') {
p.userData.life -= 0.015;
p.position.add(p.userData.velocity);
p.material.opacity = Math.max(0, p.userData.life / 2);
p.material.transparent = true;
p.userData.velocity.y -= 0.002;
if (p.userData.life <= 0) {
scene.remove(p);
fireworks.splice(i, 1);
}
}
}
}
// ===== 烟花代码结束 =====
// 相机
camera.position.set(45, 45, 45);
camera.lookAt(0, 7.5, 0);
// 动画
function animate() {
requestAnimationFrame(animate);
cars.forEach(car => {
if (car.userData.path === 'ring') {
car.userData.angle += car.userData.speed;
const currentAngle = car.userData.angle % (Math.PI * 2);
for (const radialAngle of radialAngles) {
if (Math.abs(currentAngle - radialAngle) < 0.05 && Math.random() < 0.01) {
const newDirection = car.userData.speed > 0 ? 1 : -1;
car.userData.path = 'radial';
car.userData.angle = radialAngle;
car.userData.position = newDirection > 0 ? 0 : radialRoadLength;
car.userData.speed = (0.05 + Math.random() * 0.03) * newDirection;
car.userData.lane = car.userData.speed > 0 ? laneOffset : -laneOffset;
break;
}
}
car.position.set(
Math.cos(car.userData.angle) * car.userData.radius,
0.4,
Math.sin(car.userData.angle) * car.userData.radius
);
car.rotation.y = -car.userData.angle + Math.PI / 2;
} else if (car.userData.path === 'radial') {
car.userData.position += car.userData.speed;
if ((car.userData.speed > 0 && car.userData.position > radialRoadLength) || (car.userData.speed < 0 && car.userData.position < 0)) {
const newDirection = car.userData.speed > 0 ? 1 : -1;
car.userData.path = 'ring';
car.userData.angle = car.userData.angle + (Math.PI / 2);
car.userData.speed = (0.0015 + Math.random() * 0.0025) * newDirection;
car.userData.radius = newDirection > 0 ? outerRadius : innerRadius;
}
const baseX = Math.cos(car.userData.angle) * (car.userData.position + roadRadius);
const baseZ = Math.sin(car.userData.angle) * (car.userData.position + roadRadius);
const offsetX = Math.sin(car.userData.angle) * car.userData.lane;
const offsetZ = -Math.cos(car.userData.angle) * car.userData.lane;
car.position.set(baseX + offsetX, 0.4, baseZ + offsetZ);
car.rotation.y = -car.userData.angle + (car.userData.speed > 0 ? 0 : Math.PI);
}
});
// 间歇性发射新烟花
if (Math.random() < 0.005) {
launchIntermittentFirework();
}
// 更新烟花状态
updateFireworks();
scene.rotation.y += 0.002;
renderer.render(scene, camera);
}
// 初始烟花发射
launchInitialFireworks();
animate();
// 按钮点击事件
document.getElementById('controls').addEventListener('click', launchTieredFireworks);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
显示效果
版本9.0:美化建筑群
<!DOCTYPE html>
<html>
<head>
<title>3D凯旋门与畅通道路的建筑群 - 细节版</title>
<style>
body { margin: 0; overflow: hidden; font-family: sans-serif; }
canvas { display: block; }
#controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
border: none;
background-color: rgba(0, 0, 0, 0.2);
color: white;
border-radius: 5px;
}
</style>
</head>
<body>
<button id="controls">启动阶梯式烟花</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
<script>
// 场景 & 相机 & 渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x87CEEB, 1); // 天蓝色背景
document.body.appendChild(renderer.domElement);
// 灯光
scene.add(new THREE.AmbientLight(0x404040, 1.2));
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(30, 50, 30);
scene.add(directionalLight);
// 地面
const groundGeometry = new THREE.PlaneGeometry(2000, 2000);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x333333 }); // 接近公路的颜色
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
// 凯旋门材质
const archMaterial = new THREE.MeshStandardMaterial({color: 0xFFFFFF, roughness: 0.8, metalness: 0.2}); // 白色凯旋门
// 凯旋门主体
const base = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1.5, 6.6), archMaterial);
base.position.y = 0.75; scene.add(base);
const pillarGeometry = new THREE.BoxGeometry(2.2, 12, 2.2);
const pillars = [
[-5.65, 7.5, -2.2], [5.65, 7.5, -2.2],
[-5.65, 7.5, 2.2], [5.65, 7.5, 2.2]
];
pillars.forEach(p => {
const m = new THREE.Mesh(pillarGeometry, archMaterial);
m.position.set(...p);
scene.add(m);
});
const midBeam = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1, 6.6), archMaterial);
midBeam.position.y = 10; scene.add(midBeam);
const attic = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1.5, 6.6), archMaterial);
attic.position.y = 13.5; scene.add(attic);
const quadrigaMaterial = new THREE.MeshPhongMaterial({color: 0xB8860B});
const quadriga = new THREE.Mesh(new THREE.BoxGeometry(2, 1, 2), quadrigaMaterial);
quadriga.position.set(0, 14.5, 0); scene.add(quadriga);
const friezeGeometry = new THREE.BoxGeometry(12.5, 0.4, 0.4);
const frieze = new THREE.Mesh(friezeGeometry, archMaterial);
frieze.position.set(0, 12.8, 3.1); scene.add(frieze);
const friezeBack = frieze.clone(); friezeBack.position.z = -3.1; scene.add(friezeBack);
const friezeDetailGeometry = new THREE.BoxGeometry(0.5, 0.3, 0.2);
for (let i = -5; i <= 5; i += 2) {
const detail = new THREE.Mesh(friezeDetailGeometry, quadrigaMaterial);
detail.position.set(i, 12.8, 3.2); scene.add(detail);
const detailBack = detail.clone(); detailBack.position.z = -3.2; scene.add(detailBack);
}
const sculptureGeometry = new THREE.BoxGeometry(2.2, 3, 0.3);
const sculptureMaterial = new THREE.MeshPhongMaterial({color: 0xB8860B});
const sculptures = [
[-5.65, 3, -3.2], [5.65, 3, -3.2],
[-5.65, 3, 3.2], [5.65, 3, 3.2]
];
sculptures.forEach(p => {
const s = new THREE.Mesh(sculptureGeometry, sculptureMaterial);
s.position.set(...p); scene.add(s);
});
// ===== 建筑群 =====
const frenchRoofMaterial = new THREE.MeshPhongMaterial({color: 0x708090, shininess: 10}); // Slate gray for mansard roofs
const frenchBodyMaterial = new THREE.MeshPhongMaterial({color: 0xE0CDA9, roughness: 0.6}); // Beige stone
const frenchDetailMaterial = new THREE.MeshPhongMaterial({color: 0xD4AF37, metalness: 0.5}); // Gold accents
const frenchWindowMaterial = new THREE.MeshBasicMaterial({color: 0xADD8E6}); // Light blue glass
function createFrenchBuilding(x, z, baseHeight) {
const building = new THREE.Group();
// Bottom base (rectangular foundation with stone texture effect)
const baseGeo = new THREE.BoxGeometry(5, 0.8, 5);
const base = new THREE.Mesh(baseGeo, frenchBodyMaterial);
base.position.y = 0.4;
building.add(base);
// Main body (multi-story with classical symmetry)
const bodyHeight = baseHeight * 0.7;
const bodyGeo = new THREE.BoxGeometry(4.5, bodyHeight, 4.5);
const body = new THREE.Mesh(bodyGeo, frenchBodyMaterial);
body.position.y = 0.8 + bodyHeight / 2;
building.add(body);
// Columns (Corinthian-style, simplified with capitals)
const columnHeight = bodyHeight;
const columnGeo = new THREE.CylinderGeometry(0.2, 0.2, columnHeight, 12);
const capitalGeo = new THREE.BoxGeometry(0.4, 0.3, 0.4);
const positions = [
[-2, -2], [2, -2],
[-2, 2], [2, 2]
];
positions.forEach(pos => {
const column = new THREE.Mesh(columnGeo, frenchDetailMaterial);
column.position.set(pos[0], 0.8 + columnHeight / 2, pos[1]);
building.add(column);
const capital = new THREE.Mesh(capitalGeo, frenchDetailMaterial);
capital.position.set(pos[0], 0.8 + columnHeight + 0.15, pos[1]);
building.add(capital);
});
// Windows (detailed arched windows on each face, multi per story)
const windowGeo = new THREE.BoxGeometry(0.8, 1.5, 0.1);
const archGeo = new THREE.TorusGeometry(0.4, 0.1, 8, 12, Math.PI);
const frameGeo = new THREE.BoxGeometry(0.9, 1.6, 0.12);
const numStories = 3; // Fine detail with multiple stories
for (let story = 0; story < numStories; story++) {
const storyY = 0.8 + (bodyHeight / numStories) * (story + 0.5);
for (let side = 0; side < 4; side++) {
const angle = side * Math.PI / 2;
const windowMesh = new THREE.Mesh(windowGeo, frenchWindowMaterial);
windowMesh.position.set(Math.cos(angle) * 2.3, storyY, Math.sin(angle) * 2.3);
windowMesh.rotation.y = -angle;
building.add(windowMesh);
// Arched top
const arch = new THREE.Mesh(archGeo, frenchDetailMaterial);
arch.position.set(0, 0.8, 0);
arch.rotation.x = Math.PI / 2;
windowMesh.add(arch);
// Frame
const frame = new THREE.Mesh(frameGeo, frenchDetailMaterial);
frame.position.set(0, 0, -0.01);
windowMesh.add(frame);
// Pane divisions (vertical and horizontal for fine detail)
const paneGeo = new THREE.BoxGeometry(0.05, 1.5, 0.05);
for (let p = -0.3; p <= 0.3; p += 0.3) {
const vPane = new THREE.Mesh(paneGeo, frenchDetailMaterial);
vPane.position.set(p, 0, 0.05);
windowMesh.add(vPane);
}
for (let q = -0.6; q <= 0.6; q += 0.6) {
const hPane = new THREE.Mesh(paneGeo, frenchDetailMaterial);
hPane.rotation.z = Math.PI / 2;
hPane.position.set(0, q, 0.05);
windowMesh.add(hPane);
}
}
}
// Mansard roof (sloped with dormers for French style)
const roofHeight = baseHeight * 0.3;
const roofGeo = new THREE.BoxGeometry(4.5, roofHeight, 4.5);
const roof = new THREE.Mesh(roofGeo, frenchRoofMaterial);
roof.position.y = 0.8 + bodyHeight + roofHeight / 2;
building.add(roof);
// Dormer windows on roof (fine detail)
const dormerGeo = new THREE.BoxGeometry(1, 1, 0.5);
const dormerRoofGeo = new THREE.ConeGeometry(0.6, 0.8, 4);
for (let side = 0; side < 4; side++) {
const angle = side * Math.PI / 2;
const dormer = new THREE.Mesh(dormerGeo, frenchBodyMaterial);
dormer.position.set(Math.cos(angle) * 1.8, 0.8 + bodyHeight + roofHeight / 2, Math.sin(angle) * 1.8);
dormer.rotation.y = -angle;
building.add(dormer);
const dormerRoof = new THREE.Mesh(dormerRoofGeo, frenchRoofMaterial);
dormerRoof.position.set(0, 0.9, 0);
dormer.add(dormerRoof);
const dormerWindow = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.8, 0.1), frenchWindowMaterial);
dormerWindow.position.set(0, 0, 0.26);
dormer.add(dormerWindow);
}
// Ornate cornices and friezes (fine decorative bands)
const corniceGeo = new THREE.BoxGeometry(5, 0.3, 5);
const cornice = new THREE.Mesh(corniceGeo, frenchDetailMaterial);
cornice.position.y = 0.8 + bodyHeight;
building.add(cornice);
building.position.set(x, 0, z);
scene.add(building);
}
const rings = 5, baseRadius = 25, ringSpacing = 10;
const radialAngles = [];
const numRadials = 8;
for (let i = 0; i < numRadials; i++) {
radialAngles.push((i / numRadials) * Math.PI * 2);
}
for (let ring = 2; ring <= rings; ring++) {
const radius = baseRadius + (ring - 1) * ringSpacing;
const numBuildings = 8 + ring * 4;
for (let i = 0; i < numBuildings; i++) {
const angle = (i / numBuildings) * Math.PI * 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
let onRadialRoad = false;
for (const roadAngle of radialAngles) {
const dx = x;
const dz = z;
const distFromRoadCenter = Math.abs(Math.sin(roadAngle) * dx - Math.cos(roadAngle) * dz);
if (distFromRoadCenter < 6) {
onRadialRoad = true;
break;
}
}
if (onRadialRoad) continue;
const height = 6 + Math.random() * 8;
createFrenchBuilding(x, z, height);
}
}
// ===== 道路 =====
const roadMaterial = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.9 });
const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 1 });
const roadRadius = baseRadius - 3;
const roadWidth = 6;
const roadGeometry = new THREE.RingGeometry(roadRadius - roadWidth / 2, roadRadius + roadWidth / 2, 128);
const roadMesh = new THREE.Mesh(roadGeometry, roadMaterial);
roadMesh.rotation.x = -Math.PI / 2;
scene.add(roadMesh);
// 移除闪烁效果,直接添加不闪烁的发光环
const glowRingGeometry = new THREE.RingGeometry(roadRadius + roadWidth / 2, roadRadius + roadWidth / 2 + 0.3, 128);
const glowRingMesh = new THREE.Mesh(glowRingGeometry, glowMaterial);
glowRingMesh.rotation.x = -Math.PI / 2;
scene.add(glowRingMesh);
const radialRoadLength = 60;
const radialRoadWidth = 5;
for (let angle of radialAngles) {
const roadGeo = new THREE.BoxGeometry(radialRoadLength, 0.1, radialRoadWidth);
const road = new THREE.Mesh(roadGeo, roadMaterial);
road.position.set(Math.cos(angle) * (radialRoadLength / 2 + roadRadius), 0.05, Math.sin(angle) * (radialRoadLength / 2 + roadRadius));
road.rotation.y = -angle;
scene.add(road);
const glowGeoLeft = new THREE.BoxGeometry(radialRoadLength, 0.05, 0.2);
const glowLeft = new THREE.Mesh(glowGeoLeft, glowMaterial);
glowLeft.position.set(road.position.x + Math.sin(angle) * (radialRoadWidth / 2 + 0.1), 0.06, road.position.z - Math.cos(angle) * (radialRoadWidth / 2 + 0.1));
glowLeft.rotation.y = road.rotation.y;
scene.add(glowLeft);
const glowGeoRight = new THREE.BoxGeometry(radialRoadLength, 0.05, 0.2);
const glowRight = new THREE.Mesh(glowGeoRight, glowMaterial);
glowRight.position.set(road.position.x - Math.sin(angle) * (radialRoadWidth / 2 + 0.1), 0.06, road.position.z + Math.cos(angle) * (radialRoadWidth / 2 + 0.1));
glowRight.rotation.y = road.rotation.y;
scene.add(glowRight);
}
// ===== 树木 =====
const treeTrunkGeometry = new THREE.CylinderGeometry(0.3, 0.3, 2, 8);
const treeLeavesGeometry = new THREE.ConeGeometry(1.5, 3, 8);
const treeTrunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
const treeLeavesMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22 });
function createTree(x, z) {
const trunk = new THREE.Mesh(treeTrunkGeometry, treeTrunkMaterial);
trunk.position.set(x, 1, z);
scene.add(trunk);
const leaves = new THREE.Mesh(treeLeavesGeometry, treeLeavesMaterial);
leaves.position.set(x, 3.5, z);
scene.add(leaves);
}
// 在凯旋门和道路之间生成树林
const innerForestRadius = 15;
const outerForestRadius = roadRadius - roadWidth / 2 - 2;
const numForestTrees = 150;
for (let i = 0; i < numForestTrees; i++) {
const angle = Math.random() * Math.PI * 2;
const radius = innerForestRadius + Math.random() * (outerForestRadius - innerForestRadius);
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
createTree(x, z);
}
// 径向公路两侧生成树木,跳过最内侧一圈
const treeSpacing = 6;
const radialOffset = radialRoadWidth / 2 + 1;
for (let angle of radialAngles) {
for (let i = 1; i < radialRoadLength / treeSpacing; i++) {
const dist = i * treeSpacing;
// 左侧树木
const xLeft = Math.cos(angle) * (dist + roadRadius) + Math.sin(angle) * radialOffset;
const zLeft = Math.sin(angle) * (dist + roadRadius) - Math.cos(angle) * radialOffset;
createTree(xLeft, zLeft);
// 右侧树木
const xRight = Math.cos(angle) * (dist + roadRadius) - Math.sin(angle) * radialOffset;
const zRight = Math.sin(angle) * (dist + roadRadius) + Math.cos(angle) * radialOffset;
createTree(xRight, zRight);
}
}
// ===== 优化车流 =====
const cars = [];
const carGeometry = new THREE.BoxGeometry(1.5, 0.7, 0.8);
const numCars = 100;
const laneOffset = radialRoadWidth / 4;
const innerRadius = roadRadius - roadWidth / 4;
const outerRadius = roadRadius + roadWidth / 4;
for (let i = 0; i < numCars; i++) {
const carMaterial = new THREE.MeshStandardMaterial({
color: new THREE.Color(Math.random(), Math.random(), Math.random()),
emissive: new THREE.Color(Math.random(), Math.random(), Math.random()),
emissiveIntensity: 0.8
});
const car = new THREE.Mesh(carGeometry, carMaterial);
const initialPath = Math.random() < 0.5 ? 'ring' : 'radial';
let initialAngle, initialDirection, initialRadius, initialPosition, initialLane;
if (initialPath === 'ring') {
initialAngle = Math.random() * Math.PI * 2;
initialDirection = Math.random() < 0.5 ? 1 : -1;
initialRadius = initialDirection === 1 ? outerRadius : innerRadius;
car.userData = {
path: 'ring',
angle: initialAngle,
speed: (0.0015 + Math.random() * 0.0025) * initialDirection,
radius: initialRadius,
lane: 0
};
} else {
initialAngle = radialAngles[Math.floor(Math.random() * numRadials)];
initialDirection = Math.random() < 0.5 ? 1 : -1;
initialPosition = Math.random() * radialRoadLength;
initialLane = initialDirection === 1 ? laneOffset : -laneOffset;
car.userData = {
path: 'radial',
angle: initialAngle,
position: initialPosition,
speed: (0.05 + Math.random() * 0.03) * initialDirection,
lane: initialLane
};
}
scene.add(car);
cars.push(car);
}
// ===== 烟花代码开始 =====
const fireworks = [];
const particleGeometry = new THREE.SphereGeometry(0.08, 8, 8);
const innerCircleRadius = roadRadius - roadWidth / 2;
// 烟花爆炸函数
function createExplosion(originPosition, color) {
const numParticles = 60 + Math.floor(Math.random() * 30);
const explosionRadius = 0.5 + Math.random();
for (let i = 0; i < numParticles; i++) {
const particleMaterial = new THREE.MeshBasicMaterial({ color: color });
const particle = new THREE.Mesh(particleGeometry, particleMaterial);
particle.position.copy(originPosition);
const velocity = new THREE.Vector3(
(Math.random() - 0.5) * explosionRadius,
(Math.random() - 0.5) * explosionRadius,
(Math.random() - 0.5) * explosionRadius
);
particle.userData = {
velocity: velocity,
life: 1.5 + Math.random() * 1,
state: 'exploded'
};
fireworks.push(particle);
scene.add(particle);
}
}
// 发射烟花(升空)
function launchFirework(startPosition) {
const rocketMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const rocket = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.3, 0.1), rocketMaterial);
rocket.position.copy(startPosition);
// 爆炸高度比凯旋门低一些,约12-18
const targetHeight = 12 + Math.random() * 6;
const color = new THREE.Color().setHSL(Math.random(), 1, 0.5).getHex();
rocket.userData = {
velocity: new THREE.Vector3(0, 0.35 + Math.random() * 0.25, 0),
state: 'rising',
targetY: targetHeight,
color: color
};
fireworks.push(rocket);
scene.add(rocket);
}
// 场景初始化时的所有烟花一起升空
function launchInitialFireworks() {
for (let i = 0; i < 15; i++) {
const angle = (i / 15) * Math.PI * 2;
const radius = Math.random() * innerCircleRadius;
const position = new THREE.Vector3(Math.cos(angle) * radius, 0.1, Math.sin(angle) * radius);
launchFirework(position);
}
}
// 间歇性发射单个烟花
function launchIntermittentFirework() {
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * innerCircleRadius;
const position = new THREE.Vector3(Math.cos(angle) * radius, 0.1, Math.sin(angle) * radius);
launchFirework(position);
}
// 阶梯式环形发射烟花
function launchTieredFireworks() {
const numTiers = 6;
const delayPerTier = 400; // 毫秒
const baseHeight = 12;
const heightStep = 3; // 阶梯高度
for (let i = 0; i < numTiers; i++) {
setTimeout(() => {
const angle = (i / numTiers) * Math.PI * 2;
const radius = innerCircleRadius * 0.8;
const position = new THREE.Vector3(Math.cos(angle) * radius, 0.1, Math.sin(angle) * radius);
const rocketMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const rocket = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.3, 0.1), rocketMaterial);
rocket.position.copy(position);
rocket.userData = {
velocity: new THREE.Vector3(0, 0.35, 0),
state: 'rising',
targetY: baseHeight + i * heightStep,
color: new THREE.Color().setHSL(Math.random(), 1, 0.5).getHex()
};
fireworks.push(rocket);
scene.add(rocket);
}, i * delayPerTier);
}
}
function updateFireworks() {
for (let i = fireworks.length - 1; i >= 0; i--) {
const p = fireworks [i];
if (p.userData.state === 'rising') {
p.position.add(p.userData.velocity);
if (p.position.y >= p.userData.targetY) {
createExplosion(p.position, p.userData.color);
scene.remove(p);
fireworks.splice(i, 1);
}
} else if (p.userData.state === 'exploded') {
p.userData.life -= 0.015;
p.position.add(p.userData.velocity);
p.material.opacity = Math.max(0, p.userData.life / 2);
p.material.transparent = true;
p.userData.velocity.y -= 0.002;
if (p.userData.life <= 0) {
scene.remove(p);
fireworks.splice(i, 1);
}
}
}
}
// ===== 烟花代码结束 =====
// 相机
camera.position.set(45, 45, 45);
camera.lookAt(0, 7.5, 0);
// 动画
function animate() {
requestAnimationFrame(animate);
cars.forEach(car => {
if (car.userData.path === 'ring') {
car.userData.angle += car.userData.speed;
const currentAngle = car.userData.angle % (Math.PI * 2);
for (const radialAngle of radialAngles) {
if (Math.abs(currentAngle - radialAngle) < 0.05 && Math.random() < 0.01) {
const newDirection = car.userData.speed > 0 ? 1 : -1;
car.userData.path = 'radial';
car.userData.angle = radialAngle;
car.userData.position = newDirection > 0 ? 0 : radialRoadLength;
car.userData.speed = (0.05 + Math.random() * 0.03) * newDirection;
car.userData.lane = car.userData.speed > 0 ? laneOffset : -laneOffset;
break;
}
}
car.position.set(
Math.cos(car.userData.angle) * car.userData.radius,
0.4,
Math.sin(car.userData.angle) * car.userData.radius
);
car.rotation.y = -car.userData.angle + Math.PI / 2;
} else if (car.userData.path === 'radial') {
car.userData.position += car.userData.speed;
if ((car.userData.speed > 0 && car.userData.position > radialRoadLength) || (car.userData.speed < 0 && car.userData.position < 0)) {
const newDirection = car.userData.speed > 0 ? 1 : -1;
car.userData.path = 'ring';
car.userData.angle = car.userData.angle + (Math.PI / 2);
car.userData.speed = (0.0015 + Math.random() * 0.0025) * newDirection;
car.userData.radius = newDirection > 0 ? outerRadius : innerRadius;
}
const baseX = Math.cos(car.userData.angle) * (car.userData.position + roadRadius);
const baseZ = Math.sin(car.userData.angle) * (car.userData.position + roadRadius);
const offsetX = Math.sin(car.userData.angle) * car.userData.lane;
const offsetZ = -Math.cos(car.userData.angle) * car.userData.lane;
car.position.set(baseX + offsetX, 0.4, baseZ + offsetZ);
car.rotation.y = -car.userData.angle + (car.userData.speed > 0 ? 0 : Math.PI);
}
});
// 间歇性发射新烟花
if (Math.random() < 0.005) {
launchIntermittentFirework();
}
// 更新烟花状态
updateFireworks();
scene.rotation.y += 0.002;
renderer.render(scene, camera);
}
// 初始烟花发射
launchInitialFireworks();
animate();
// 按钮点击事件
document.getElementById('controls').addEventListener('click', launchTieredFireworks);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
效果显示
版本10.0:添加云朵
<!DOCTYPE html>
<html>
<head>
<title>3D凯旋门与畅通道路的建筑群 - 细节版</title>
<style>
body { margin: 0; overflow: hidden; font-family: sans-serif; }
canvas { display: block; }
#controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
border: none;
background-color: rgba(0, 0, 0, 0.2);
color: white;
border-radius: 5px;
}
</style>
</head>
<body>
<button id="controls">启动阶梯式烟花</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
<script>
// 场景 & 相机 & 渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x87CEEB, 1); // 天蓝色背景
document.body.appendChild(renderer.domElement);
// 灯光
scene.add(new THREE.AmbientLight(0x404040, 1.2));
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(30, 50, 30);
scene.add(directionalLight);
// 地面
const groundGeometry = new THREE.PlaneGeometry(2000, 2000);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x333333 }); // 公路灰色
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
// 凯旋门材质
const archMaterial = new THREE.MeshStandardMaterial({color: 0xFFFFFF, roughness: 0.8, metalness: 0.2}); // 白色石材
// 凯旋门主体
const base = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1.5, 6.6), archMaterial);
base.position.y = 0.75; scene.add(base);
const pillarGeometry = new THREE.BoxGeometry(2.2, 12, 2.2);
const pillars = [
[-5.65, 7.5, -2.2], [5.65, 7.5, -2.2],
[-5.65, 7.5, 2.2], [5.65, 7.5, 2.2]
];
pillars.forEach(p => {
const m = new THREE.Mesh(pillarGeometry, archMaterial);
m.position.set(...p);
scene.add(m);
});
const midBeam = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1, 6.6), archMaterial);
midBeam.position.y = 10; scene.add(midBeam);
const attic = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1.5, 6.6), archMaterial);
attic.position.y = 13.5; scene.add(attic);
const quadrigaMaterial = new THREE.MeshPhongMaterial({color: 0xB8860B}); // 金色雕塑
const quadriga = new THREE.Mesh(new THREE.BoxGeometry(2, 1, 2), quadrigaMaterial);
quadriga.position.set(0, 14.5, 0); scene.add(quadriga);
const friezeGeometry = new THREE.BoxGeometry(12.5, 0.4, 0.4);
const frieze = new THREE.Mesh(friezeGeometry, archMaterial);
frieze.position.set(0, 12.8, 3.1); scene.add(frieze);
const friezeBack = frieze.clone(); friezeBack.position.z = -3.1; scene.add(friezeBack);
const friezeDetailGeometry = new THREE.BoxGeometry(0.5, 0.3, 0.2);
for (let i = -5; i <= 5; i += 2) {
const detail = new THREE.Mesh(friezeDetailGeometry, quadrigaMaterial);
detail.position.set(i, 12.8, 3.2); scene.add(detail);
const detailBack = detail.clone(); detailBack.position.z = -3.2; scene.add(detailBack);
}
const sculptureGeometry = new THREE.BoxGeometry(2.2, 3, 0.3);
const sculptureMaterial = new THREE.MeshPhongMaterial({color: 0xB8860B});
const sculptures = [
[-5.65, 3, -3.2], [5.65, 3, -3.2],
[-5.65, 3, 3.2], [5.65, 3, 3.2]
];
sculptures.forEach(p => {
const s = new THREE.Mesh(sculptureGeometry, sculptureMaterial);
s.position.set(...p); scene.add(s);
});
// ===== 法式建筑群 =====
const frenchRoofMaterial = new THREE.MeshPhongMaterial({color: 0x708090, shininess: 10}); // 曼萨德屋顶灰色
const frenchBodyMaterial = new THREE.MeshPhongMaterial({color: 0xE0CDA9, roughness: 0.6}); // 米色石材
const frenchDetailMaterial = new THREE.MeshPhongMaterial({color: 0xD4AF37, metalness: 0.5}); // 金色装饰
const frenchWindowMaterial = new THREE.MeshBasicMaterial({color: 0xADD8E6}); // 浅蓝色玻璃
function createFrenchBuilding(x, z, baseHeight) {
const building = new THREE.Group();
// 底部基座(矩形石材基础)
const baseGeo = new THREE.BoxGeometry(5, 0.8, 5);
const base = new THREE.Mesh(baseGeo, frenchBodyMaterial);
base.position.y = 0.4;
building.add(base);
// 主体(多层对称结构)
const bodyHeight = baseHeight * 0.7;
const bodyGeo = new THREE.BoxGeometry(4.5, bodyHeight, 4.5);
const body = new THREE.Mesh(bodyGeo, frenchBodyMaterial);
body.position.y = 0.8 + bodyHeight / 2;
building.add(body);
// 柱子(简化科林斯柱式,带柱头)
const columnHeight = bodyHeight;
const columnGeo = new THREE.CylinderGeometry(0.2, 0.2, columnHeight, 12);
const capitalGeo = new THREE.BoxGeometry(0.4, 0.3, 0.4);
const positions = [
[-2, -2], [2, -2],
[-2, 2], [2, 2]
];
positions.forEach(pos => {
const column = new THREE.Mesh(columnGeo, frenchDetailMaterial);
column.position.set(pos[0], 0.8 + columnHeight / 2, pos[1]);
building.add(column);
const capital = new THREE.Mesh(capitalGeo, frenchDetailMaterial);
capital.position.set(pos[0], 0.8 + columnHeight + 0.15, pos[1]);
building.add(capital);
});
// 窗户(每层多个拱形窗,细节丰富)
const windowGeo = new THREE.BoxGeometry(0.8, 1.5, 0.1);
const archGeo = new THREE.TorusGeometry(0.4, 0.1, 8, 12, Math.PI);
const frameGeo = new THREE.BoxGeometry(0.9, 1.6, 0.12);
const numStories = 3;
for (let story = 0; story < numStories; story++) {
const storyY = 0.8 + (bodyHeight / numStories) * (story + 0.5);
for (let side = 0; side < 4; side++) {
const angle = side * Math.PI / 2;
const windowMesh = new THREE.Mesh(windowGeo, frenchWindowMaterial);
windowMesh.position.set(Math.cos(angle) * 2.3, storyY, Math.sin(angle) * 2.3);
windowMesh.rotation.y = -angle;
building.add(windowMesh);
// 拱形顶部
const arch = new THREE.Mesh(archGeo, frenchDetailMaterial);
arch.position.set(0, 0.8, 0);
arch.rotation.x = Math.PI / 2;
windowMesh.add(arch);
// 窗框
const frame = new THREE.Mesh(frameGeo, frenchDetailMaterial);
frame.position.set(0, 0, -0.01);
windowMesh.add(frame);
// 窗格(垂直和水平分隔)
const paneGeo = new THREE.BoxGeometry(0.05, 1.5, 0.05);
for (let p = -0.3; p <= 0.3; p += 0.3) {
const vPane = new THREE.Mesh(paneGeo, frenchDetailMaterial);
vPane.position.set(p, 0, 0.05);
windowMesh.add(vPane);
}
for (let q = -0.6; q <= 0.6; q += 0.6) {
const hPane = new THREE.Mesh(paneGeo, frenchDetailMaterial);
hPane.rotation.z = Math.PI / 2;
hPane.position.set(0, q, 0.05);
windowMesh.add(hPane);
}
}
}
// 曼萨德屋顶(带天窗)
const roofHeight = baseHeight * 0.3;
const roofGeo = new THREE.BoxGeometry(4.5, roofHeight, 4.5);
const roof = new THREE.Mesh(roofGeo, frenchRoofMaterial);
roof.position.y = 0.8 + bodyHeight + roofHeight / 2;
building.add(roof);
// 屋顶天窗
const dormerGeo = new THREE.BoxGeometry(1, 1, 0.5);
const dormerRoofGeo = new THREE.ConeGeometry(0.6, 0.8, 4);
for (let side = 0; side < 4; side++) {
const angle = side * Math.PI / 2;
const dormer = new THREE.Mesh(dormerGeo, frenchBodyMaterial);
dormer.position.set(Math.cos(angle) * 1.8, 0.8 + bodyHeight + roofHeight / 2, Math.sin(angle) * 1.8);
dormer.rotation.y = -angle;
building.add(dormer);
const dormerRoof = new THREE.Mesh(dormerRoofGeo, frenchRoofMaterial);
dormerRoof.position.set(0, 0.9, 0);
dormer.add(dormerRoof);
const dormerWindow = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.8, 0.1), frenchWindowMaterial);
dormerWindow.position.set(0, 0, 0.26);
dormer.add(dormerWindow);
}
// 装饰线脚和檐口
const corniceGeo = new THREE.BoxGeometry(5, 0.3, 5);
const cornice = new THREE.Mesh(corniceGeo, frenchDetailMaterial);
cornice.position.y = 0.8 + bodyHeight;
building.add(cornice);
building.position.set(x, 0, z);
scene.add(building);
}
const rings = 5, baseRadius = 25, ringSpacing = 10;
const radialAngles = [];
const numRadials = 8;
for (let i = 0; i < numRadials; i++) {
radialAngles.push((i / numRadials) * Math.PI * 2);
}
for (let ring = 2; ring <= rings; ring++) {
const radius = baseRadius + (ring - 1) * ringSpacing;
const numBuildings = 8 + ring * 4;
for (let i = 0; i < numBuildings; i++) {
const angle = (i / numBuildings) * Math.PI * 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
let onRadialRoad = false;
for (const roadAngle of radialAngles) {
const dx = x;
const dz = z;
const distFromRoadCenter = Math.abs(Math.sin(roadAngle) * dx - Math.cos(roadAngle) * dz);
if (distFromRoadCenter < 6) {
onRadialRoad = true;
break;
}
}
if (onRadialRoad) continue;
const height = 6 + Math.random() * 8;
createFrenchBuilding(x, z, height);
}
}
// ===== 道路 =====
const roadMaterial = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.9 });
const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 1 });
const roadRadius = baseRadius - 3;
const roadWidth = 6;
const roadGeometry = new THREE.RingGeometry(roadRadius - roadWidth / 2, roadRadius + roadWidth / 2, 128);
const roadMesh = new THREE.Mesh(roadGeometry, roadMaterial);
roadMesh.rotation.x = -Math.PI / 2;
scene.add(roadMesh);
const glowRingGeometry = new THREE.RingGeometry(roadRadius + roadWidth / 2, roadRadius + roadWidth / 2 + 0.3, 128);
const glowRingMesh = new THREE.Mesh(glowRingGeometry, glowMaterial);
glowRingMesh.rotation.x = -Math.PI / 2;
scene.add(glowRingMesh);
const radialRoadLength = 60;
const radialRoadWidth = 5;
for (let angle of radialAngles) {
const roadGeo = new THREE.BoxGeometry(radialRoadLength, 0.1, radialRoadWidth);
const road = new THREE.Mesh(roadGeo, roadMaterial);
road.position.set(Math.cos(angle) * (radialRoadLength / 2 + roadRadius), 0.05, Math.sin(angle) * (radialRoadLength / 2 + roadRadius));
road.rotation.y = -angle;
scene.add(road);
const glowGeoLeft = new THREE.BoxGeometry(radialRoadLength, 0.05, 0.2);
const glowLeft = new THREE.Mesh(glowGeoLeft, glowMaterial);
glowLeft.position.set(road.position.x + Math.sin(angle) * (radialRoadWidth / 2 + 0.1), 0.06, road.position.z - Math.cos(angle) * (radialRoadWidth / 2 + 0.1));
glowLeft.rotation.y = road.rotation.y;
scene.add(glowLeft);
const glowGeoRight = new THREE.BoxGeometry(radialRoadLength, 0.05, 0.2);
const glowRight = new THREE.Mesh(glowGeoRight, glowMaterial);
glowRight.position.set(road.position.x - Math.sin(angle) * (radialRoadWidth / 2 + 0.1), 0.06, road.position.z + Math.cos(angle) * (radialRoadWidth / 2 + 0.1));
glowRight.rotation.y = road.rotation.y;
scene.add(glowRight);
}
// ===== 树木 =====
const treeTrunkGeometry = new THREE.CylinderGeometry(0.3, 0.3, 2, 8);
const treeLeavesGeometry = new THREE.ConeGeometry(1.5, 3, 8);
const treeTrunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
const treeLeavesMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22 });
function createTree(x, z) {
const trunk = new THREE.Mesh(treeTrunkGeometry, treeTrunkMaterial);
trunk.position.set(x, 1, z);
scene.add(trunk);
const leaves = new THREE.Mesh(treeLeavesGeometry, treeLeavesMaterial);
leaves.position.set(x, 3.5, z);
scene.add(leaves);
}
const innerForestRadius = 15;
const outerForestRadius = roadRadius - roadWidth / 2 - 2;
const numForestTrees = 150;
for (let i = 0; i < numForestTrees; i++) {
const angle = Math.random() * Math.PI * 2;
const radius = innerForestRadius + Math.random() * (outerForestRadius - innerForestRadius);
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
createTree(x, z);
}
const treeSpacing = 6;
const radialOffset = radialRoadWidth / 2 + 1;
for (let angle of radialAngles) {
for (let i = 1; i < radialRoadLength / treeSpacing; i++) {
const dist = i * treeSpacing;
const xLeft = Math.cos(angle) * (dist + roadRadius) + Math.sin(angle) * radialOffset;
const zLeft = Math.sin(angle) * (dist + roadRadius) - Math.cos(angle) * radialOffset;
createTree(xLeft, zLeft);
const xRight = Math.cos(angle) * (dist + roadRadius) - Math.sin(angle) * radialOffset;
const zRight = Math.sin(angle) * (dist + roadRadius) + Math.cos(angle) * radialOffset;
createTree(xRight, zRight);
}
}
// ===== 云朵 =====
const cloudMaterial = new THREE.MeshStandardMaterial({
color: 0xF5F5F5,
transparent: true,
opacity: 0.7,
roughness: 0.8,
metalness: 0.1
}); // 浅灰色半透明云朵,带光影
const clouds = [];
const numClouds = 30; // 增加云朵数量
function createCloud(x, z, y) {
const cloud = new THREE.Group();
const cloudGeo = new THREE.SphereGeometry(2, 8, 8); // 低多边形球体
const segments = 5 + Math.floor(Math.random() * 4); // 每朵云 5-8 个球体
for (let i = 0; i < segments; i++) {
const segment = new THREE.Mesh(cloudGeo, cloudMaterial);
segment.position.set(
(Math.random() - 0.5) * 5, // 更宽的偏移
(Math.random() - 0.5) * 3, // 更宽的垂直偏移
(Math.random() - 0.5) * 5
);
segment.scale.set(
0.7 + Math.random() * 0.8, // 更宽的缩放范围
0.5 + Math.random() * 0.5,
0.7 + Math.random() * 0.8
);
cloud.add(segment);
}
cloud.position.set(x, y, z);
cloud.userData = {
velocity: new THREE.Vector3((Math.random() - 0.5) * 0.02, 0, (Math.random() - 0.5) * 0.02), // 漂移速度
rotationSpeed: (Math.random() - 0.5) * 0.001 // 随机旋转速度
};
clouds.push(cloud);
scene.add(cloud);
}
// 在宽广区域生成云朵
const cloudRadius = 100;
for (let i = 0; i < numClouds; i++) {
const angle = Math.random() * Math.PI * 2;
const radius = 20 + Math.random() * cloudRadius;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
const y = 20 + Math.random() * 30; // 高度 20-50
createCloud(x, z, y);
}
// ===== 优化车流 =====
const cars = [];
const carGeometry = new THREE.BoxGeometry(1.5, 0.7, 0.8);
const numCars = 100;
const laneOffset = radialRoadWidth / 4;
const innerRadius = roadRadius - roadWidth / 4;
const outerRadius = roadRadius + roadWidth / 4;
for (let i = 0; i < numCars; i++) {
const carMaterial = new THREE.MeshStandardMaterial({
color: new THREE.Color(Math.random(), Math.random(), Math.random()),
emissive: new THREE.Color(Math.random(), Math.random(), Math.random()),
emissiveIntensity: 0.8
});
const car = new THREE.Mesh(carGeometry, carMaterial);
const initialPath = Math.random() < 0.5 ? 'ring' : 'radial';
let initialAngle, initialDirection, initialRadius, initialPosition, initialLane;
if (initialPath === 'ring') {
initialAngle = Math.random() * Math.PI * 2;
initialDirection = Math.random() < 0.5 ? 1 : -1;
initialRadius = initialDirection === 1 ? outerRadius : innerRadius;
car.userData = {
path: 'ring',
angle: initialAngle,
speed: (0.0015 + Math.random() * 0.0025) * initialDirection,
radius: initialRadius,
lane: 0
};
} else {
initialAngle = radialAngles[Math.floor(Math.random() * numRadials)];
initialDirection = Math.random() < 0.5 ? 1 : -1;
initialPosition = Math.random() * radialRoadLength;
initialLane = initialDirection === 1 ? laneOffset : -laneOffset;
car.userData = {
path: 'radial',
angle: initialAngle,
position: initialPosition,
speed: (0.05 + Math.random() * 0.03) * initialDirection,
lane: initialLane
};
}
scene.add(car);
cars.push(car);
}
// ===== 烟花代码 =====
const fireworks = [];
const particleGeometry = new THREE.SphereGeometry(0.08, 8, 8);
const innerCircleRadius = roadRadius - roadWidth / 2;
function createExplosion(originPosition, color) {
const numParticles = 60 + Math.floor(Math.random() * 30);
const explosionRadius = 0.5 + Math.random();
for (let i = 0; i < numParticles; i++) {
const particleMaterial = new THREE.MeshBasicMaterial({ color: color });
const particle = new THREE.Mesh(particleGeometry, particleMaterial);
particle.position.copy(originPosition);
const velocity = new THREE.Vector3(
(Math.random() - 0.5) * explosionRadius,
(Math.random() - 0.5) * explosionRadius,
(Math.random() - 0.5) * explosionRadius
);
particle.userData = {
velocity: velocity,
life: 1.5 + Math.random() * 1,
state: 'exploded'
};
fireworks.push(particle);
scene.add(particle);
}
}
function launchFirework(startPosition) {
const rocketMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const rocket = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.3, 0.1), rocketMaterial);
rocket.position.copy(startPosition);
const targetHeight = 12 + Math.random() * 6;
const color = new THREE.Color().setHSL(Math.random(), 1, 0.5).getHex();
rocket.userData = {
velocity: new THREE.Vector3(0, 0.35 + Math.random() * 0.25, 0),
state: 'rising',
targetY: targetHeight,
color: color
};
fireworks.push(rocket);
scene.add(rocket);
}
function launchInitialFireworks() {
for (let i = 0; i < 15; i++) {
const angle = (i / 15) * Math.PI * 2;
const radius = Math.random() * innerCircleRadius;
const position = new THREE.Vector3(Math.cos(angle) * radius, 0.1, Math.sin(angle) * radius);
launchFirework(position);
}
}
function launchIntermittentFirework() {
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * innerCircleRadius;
const position = new THREE.Vector3(Math.cos(angle) * radius, 0.1, Math.sin(angle) * radius);
launchFirework(position);
}
function launchTieredFireworks() {
const numTiers = 6;
const delayPerTier = 400;
const baseHeight = 12;
const heightStep = 3;
for (let i = 0; i < numTiers; i++) {
setTimeout(() => {
const angle = (i / numTiers) * Math.PI * 2;
const radius = innerCircleRadius * 0.8;
const position = new THREE.Vector3(Math.cos(angle) * radius, 0.1, Math.sin(angle) * radius);
const rocketMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const rocket = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.3, 0.1), rocketMaterial);
rocket.position.copy(position);
rocket.userData = {
velocity: new THREE.Vector3(0, 0.35, 0),
state: 'rising',
targetY: baseHeight + i * heightStep,
color: new THREE.Color().setHSL(Math.random(), 1, 0.5).getHex()
};
fireworks.push(rocket);
scene.add(rocket);
}, i * delayPerTier);
}
}
function updateFireworks() {
for (let i = fireworks.length - 1; i >= 0; i--) {
const p = fireworks[i];
if (p.userData.state === 'rising') {
p.position.add(p.userData.velocity);
if (p.position.y >= p.userData.targetY) {
createExplosion(p.position, p.userData.color);
scene.remove(p);
fireworks.splice(i, 1);
}
} else if (p.userData.state === 'exploded') {
p.userData.life -= 0.015;
p.position.add(p.userData.velocity);
p.material.opacity = Math.max(0, p.userData.life / 2);
p.material.transparent = true;
p.userData.velocity.y -= 0.002;
if (p.userData.life <= 0) {
scene.remove(p);
fireworks.splice(i, 1);
}
}
}
}
// 相机
camera.position.set(45, 45, 45);
camera.lookAt(0, 7.5, 0);
// 动画
function animate() {
requestAnimationFrame(animate);
// 更新云朵位置和旋转
clouds.forEach(cloud => {
cloud.position.add(cloud.userData.velocity);
cloud.rotation.y += cloud.userData.rotationSpeed; // 微小旋转
// 云朵超出边界时回绕
if (cloud.position.x > cloudRadius) cloud.position.x -= 2 * cloudRadius;
if (cloud.position.x < -cloudRadius) cloud.position.x += 2 * cloudRadius;
if (cloud.position.z > cloudRadius) cloud.position.z -= 2 * cloudRadius;
if (cloud.position.z < -cloudRadius) cloud.position.z += 2 * cloudRadius;
});
// 更新车辆
cars.forEach(car => {
if (car.userData.path === 'ring') {
car.userData.angle += car.userData.speed;
const currentAngle = car.userData.angle % (Math.PI * 2);
for (const radialAngle of radialAngles) {
if (Math.abs(currentAngle - radialAngle) < 0.05 && Math.random() < 0.01) {
const newDirection = car.userData.speed > 0 ? 1 : -1;
car.userData.path = 'radial';
car.userData.angle = radialAngle;
car.userData.position = newDirection > 0 ? 0 : radialRoadLength;
car.userData.speed = (0.05 + Math.random() * 0.03) * newDirection;
car.userData.lane = car.userData.speed > 0 ? laneOffset : -laneOffset;
break;
}
}
car.position.set(
Math.cos(car.userData.angle) * car.userData.radius,
0.4,
Math.sin(car.userData.angle) * car.userData.radius
);
car.rotation.y = -car.userData.angle + Math.PI / 2;
} else if (car.userData.path === 'radial') {
car.userData.position += car.userData.speed;
if ((car.userData.speed > 0 && car.userData.position > radialRoadLength) || (car.userData.speed < 0 && car.userData.position < 0)) {
const newDirection = car.userData.speed > 0 ? 1 : -1;
car.userData.path = 'ring';
car.userData.angle = car.userData.angle + (Math.PI / 2);
car.userData.speed = (0.0015 + Math.random() * 0.0025) * newDirection;
car.userData.radius = newDirection > 0 ? outerRadius : innerRadius;
}
const baseX = Math.cos(car.userData.angle) * (car.userData.position + roadRadius);
const baseZ = Math.sin(car.userData.angle) * (car.userData.position + roadRadius);
const offsetX = Math.sin(car.userData.angle) * car.userData.lane;
const offsetZ = -Math.cos(car.userData.angle) * car.userData.lane;
car.position.set(baseX + offsetX, 0.4, baseZ + offsetZ);
car.rotation.y = -car.userData.angle + (car.userData.speed > 0 ? 0 : Math.PI);
}
});
// 间歇性发射烟花
if (Math.random() < 0.005) {
launchIntermittentFirework();
}
// 更新烟花
updateFireworks();
// 场景缓慢旋转
scene.rotation.y += 0.002;
renderer.render(scene, camera);
}
// 初始烟花
launchInitialFireworks();
animate();
// 按钮事件
document.getElementById('controls').addEventListener('click', launchTieredFireworks);
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
显示效果
版本11.0:添加动态热气球
<!DOCTYPE html>
<html>
<head>
<title>3D凯旋门与畅通道路的建筑群 - 细节版</title>
<style>
body { margin: 0; overflow: hidden; font-family: sans-serif; }
canvas { display: block; }
#controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
border: none;
background-color: rgba(0, 0, 0, 0.2);
color: white;
border-radius: 5px;
}
</style>
</head>
<body>
<button id="controls">启动阶梯式烟花</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
<script>
// 场景 & 相机 & 渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x87CEEB, 1); // 天蓝色背景
document.body.appendChild(renderer.domElement);
// 灯光
scene.add(new THREE.AmbientLight(0x404040, 1.2));
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(30, 50, 30);
scene.add(directionalLight);
// 地面
const groundGeometry = new THREE.PlaneGeometry(2000, 2000);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x333333 }); // 公路灰色
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
// 凯旋门材质
const archMaterial = new THREE.MeshStandardMaterial({color: 0xFFFFFF, roughness: 0.8, metalness: 0.2}); // 白色石材
// 凯旋门主体
const base = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1.5, 6.6), archMaterial);
base.position.y = 0.75; scene.add(base);
const pillarGeometry = new THREE.BoxGeometry(2.2, 12, 2.2);
const pillars = [
[-5.65, 7.5, -2.2], [5.65, 7.5, -2.2],
[-5.65, 7.5, 2.2], [5.65, 7.5, 2.2]
];
pillars.forEach(p => {
const m = new THREE.Mesh(pillarGeometry, archMaterial);
m.position.set(...p);
scene.add(m);
});
const midBeam = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1, 6.6), archMaterial);
midBeam.position.y = 10; scene.add(midBeam);
const attic = new THREE.Mesh(new THREE.BoxGeometry(13.5, 1.5, 6.6), archMaterial);
attic.position.y = 13.5; scene.add(attic);
const quadrigaMaterial = new THREE.MeshPhongMaterial({color: 0xB8860B}); // 金色雕塑
const quadriga = new THREE.Mesh(new THREE.BoxGeometry(2, 1, 2), quadrigaMaterial);
quadriga.position.set(0, 14.5, 0); scene.add(quadriga);
const friezeGeometry = new THREE.BoxGeometry(12.5, 0.4, 0.4);
const frieze = new THREE.Mesh(friezeGeometry, archMaterial);
frieze.position.set(0, 12.8, 3.1); scene.add(frieze);
const friezeBack = frieze.clone(); friezeBack.position.z = -3.1; scene.add(friezeBack);
const friezeDetailGeometry = new THREE.BoxGeometry(0.5, 0.3, 0.2);
for (let i = -5; i <= 5; i += 2) {
const detail = new THREE.Mesh(friezeDetailGeometry, quadrigaMaterial);
detail.position.set(i, 12.8, 3.2); scene.add(detail);
const detailBack = detail.clone(); detailBack.position.z = -3.2; scene.add(detailBack);
}
const sculptureGeometry = new THREE.BoxGeometry(2.2, 3, 0.3);
const sculptureMaterial = new THREE.MeshPhongMaterial({color: 0xB8860B});
const sculptures = [
[-5.65, 3, -3.2], [5.65, 3, -3.2],
[-5.65, 3, 3.2], [5.65, 3, 3.2]
];
sculptures.forEach(p => {
const s = new THREE.Mesh(sculptureGeometry, sculptureMaterial);
s.position.set(...p); scene.add(s);
});
// ===== 法式建筑群 =====
const frenchRoofMaterial = new THREE.MeshPhongMaterial({color: 0x708090, shininess: 10}); // 曼萨德屋顶灰色
const frenchBodyMaterial = new THREE.MeshPhongMaterial({color: 0xE0CDA9, roughness: 0.6}); // 米色石材
const frenchDetailMaterial = new THREE.MeshPhongMaterial({color: 0xD4AF37, metalness: 0.5}); // 金色装饰
const frenchWindowMaterial = new THREE.MeshBasicMaterial({color: 0xADD8E6}); // 浅蓝色玻璃
function createFrenchBuilding(x, z, baseHeight) {
const building = new THREE.Group();
// 底部基座(矩形石材基础)
const baseGeo = new THREE.BoxGeometry(5, 0.8, 5);
const base = new THREE.Mesh(baseGeo, frenchBodyMaterial);
base.position.y = 0.4;
building.add(base);
// 主体(多层对称结构)
const bodyHeight = baseHeight * 0.7;
const bodyGeo = new THREE.BoxGeometry(4.5, bodyHeight, 4.5);
const body = new THREE.Mesh(bodyGeo, frenchBodyMaterial);
body.position.y = 0.8 + bodyHeight / 2;
building.add(body);
// 柱子(简化科林斯柱式,带柱头)
const columnHeight = bodyHeight;
const columnGeo = new THREE.CylinderGeometry(0.2, 0.2, columnHeight, 12);
const capitalGeo = new THREE.BoxGeometry(0.4, 0.3, 0.4);
const positions = [
[-2, -2], [2, -2],
[-2, 2], [2, 2]
];
positions.forEach(pos => {
const column = new THREE.Mesh(columnGeo, frenchDetailMaterial);
column.position.set(pos[0], 0.8 + columnHeight / 2, pos[1]);
building.add(column);
const capital = new THREE.Mesh(capitalGeo, frenchDetailMaterial);
capital.position.set(pos[0], 0.8 + columnHeight + 0.15, pos[1]);
building.add(capital);
});
// 窗户(每层多个拱形窗,细节丰富)
const windowGeo = new THREE.BoxGeometry(0.8, 1.5, 0.1);
const archGeo = new THREE.TorusGeometry(0.4, 0.1, 8, 12, Math.PI);
const frameGeo = new THREE.BoxGeometry(0.9, 1.6, 0.12);
const numStories = 3;
for (let story = 0; story < numStories; story++) {
const storyY = 0.8 + (bodyHeight / numStories) * (story + 0.5);
for (let side = 0; side < 4; side++) {
const angle = side * Math.PI / 2;
const windowMesh = new THREE.Mesh(windowGeo, frenchWindowMaterial);
windowMesh.position.set(Math.cos(angle) * 2.3, storyY, Math.sin(angle) * 2.3);
windowMesh.rotation.y = -angle;
building.add(windowMesh);
// 拱形顶部
const arch = new THREE.Mesh(archGeo, frenchDetailMaterial);
arch.position.set(0, 0.8, 0);
arch.rotation.x = Math.PI / 2;
windowMesh.add(arch);
// 窗框
const frame = new THREE.Mesh(frameGeo, frenchDetailMaterial);
frame.position.set(0, 0, -0.01);
windowMesh.add(frame);
// 窗格(垂直和水平分隔)
const paneGeo = new THREE.BoxGeometry(0.05, 1.5, 0.05);
for (let p = -0.3; p <= 0.3; p += 0.3) {
const vPane = new THREE.Mesh(paneGeo, frenchDetailMaterial);
vPane.position.set(p, 0, 0.05);
windowMesh.add(vPane);
}
for (let q = -0.6; q <= 0.6; q += 0.6) {
const hPane = new THREE.Mesh(paneGeo, frenchDetailMaterial);
hPane.rotation.z = Math.PI / 2;
hPane.position.set(0, q, 0.05);
windowMesh.add(hPane);
}
}
}
// 曼萨德屋顶(带天窗)
const roofHeight = baseHeight * 0.3;
const roofGeo = new THREE.BoxGeometry(4.5, roofHeight, 4.5);
const roof = new THREE.Mesh(roofGeo, frenchRoofMaterial);
roof.position.y = 0.8 + bodyHeight + roofHeight / 2;
building.add(roof);
// 屋顶天窗
const dormerGeo = new THREE.BoxGeometry(1, 1, 0.5);
const dormerRoofGeo = new THREE.ConeGeometry(0.6, 0.8, 4);
for (let side = 0; side < 4; side++) {
const angle = side * Math.PI / 2;
const dormer = new THREE.Mesh(dormerGeo, frenchBodyMaterial);
dormer.position.set(Math.cos(angle) * 1.8, 0.8 + bodyHeight + roofHeight / 2, Math.sin(angle) * 1.8);
dormer.rotation.y = -angle;
building.add(dormer);
const dormerRoof = new THREE.Mesh(dormerRoofGeo, frenchRoofMaterial);
dormerRoof.position.set(0, 0.9, 0);
dormer.add(dormerRoof);
const dormerWindow = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.8, 0.1), frenchWindowMaterial);
dormerWindow.position.set(0, 0, 0.26);
dormer.add(dormerWindow);
}
// 装饰线脚和檐口
const corniceGeo = new THREE.BoxGeometry(5, 0.3, 5);
const cornice = new THREE.Mesh(corniceGeo, frenchDetailMaterial);
cornice.position.y = 0.8 + bodyHeight;
building.add(cornice);
building.position.set(x, 0, z);
scene.add(building);
}
const rings = 5, baseRadius = 25, ringSpacing = 10;
const radialAngles = [];
const numRadials = 8;
for (let i = 0; i < numRadials; i++) {
radialAngles.push((i / numRadials) * Math.PI * 2);
}
for (let ring = 2; ring <= rings; ring++) {
const radius = baseRadius + (ring - 1) * ringSpacing;
const numBuildings = 8 + ring * 4;
for (let i = 0; i < numBuildings; i++) {
const angle = (i / numBuildings) * Math.PI * 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
let onRadialRoad = false;
for (const roadAngle of radialAngles) {
const dx = x;
const dz = z;
const distFromRoadCenter = Math.abs(Math.sin(roadAngle) * dx - Math.cos(roadAngle) * dz);
if (distFromRoadCenter < 6) {
onRadialRoad = true;
break;
}
}
if (onRadialRoad) continue;
const height = 6 + Math.random() * 8;
createFrenchBuilding(x, z, height);
}
}
// ===== 道路 =====
const roadMaterial = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.9 });
const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 1 });
const roadRadius = baseRadius - 3;
const roadWidth = 6;
const roadGeometry = new THREE.RingGeometry(roadRadius - roadWidth / 2, roadRadius + roadWidth / 2, 128);
const roadMesh = new THREE.Mesh(roadGeometry, roadMaterial);
roadMesh.rotation.x = -Math.PI / 2;
scene.add(roadMesh);
const glowRingGeometry = new THREE.RingGeometry(roadRadius + roadWidth / 2, roadRadius + roadWidth / 2 + 0.3, 128);
const glowRingMesh = new THREE.Mesh(glowRingGeometry, glowMaterial);
glowRingMesh.rotation.x = -Math.PI / 2;
scene.add(glowRingMesh);
const radialRoadLength = 60;
const radialRoadWidth = 5;
for (let angle of radialAngles) {
const roadGeo = new THREE.BoxGeometry(radialRoadLength, 0.1, radialRoadWidth);
const road = new THREE.Mesh(roadGeo, roadMaterial);
road.position.set(Math.cos(angle) * (radialRoadLength / 2 + roadRadius), 0.05, Math.sin(angle) * (radialRoadLength / 2 + roadRadius));
road.rotation.y = -angle;
scene.add(road);
const glowGeoLeft = new THREE.BoxGeometry(radialRoadLength, 0.05, 0.2);
const glowLeft = new THREE.Mesh(glowGeoLeft, glowMaterial);
glowLeft.position.set(road.position.x + Math.sin(angle) * (radialRoadWidth / 2 + 0.1), 0.06, road.position.z - Math.cos(angle) * (radialRoadWidth / 2 + 0.1));
glowLeft.rotation.y = road.rotation.y;
scene.add(glowLeft);
const glowGeoRight = new THREE.BoxGeometry(radialRoadLength, 0.05, 0.2);
const glowRight = new THREE.Mesh(glowGeoRight, glowMaterial);
glowRight.position.set(road.position.x - Math.sin(angle) * (radialRoadWidth / 2 + 0.1), 0.06, road.position.z + Math.cos(angle) * (radialRoadWidth / 2 + 0.1));
glowRight.rotation.y = road.rotation.y;
scene.add(glowRight);
}
// ===== 树木 =====
const treeTrunkGeometry = new THREE.CylinderGeometry(0.3, 0.3, 2, 8);
const treeLeavesGeometry = new THREE.ConeGeometry(1.5, 3, 8);
const treeTrunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
const treeLeavesMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22 });
function createTree(x, z) {
const trunk = new THREE.Mesh(treeTrunkGeometry, treeTrunkMaterial);
trunk.position.set(x, 1, z);
scene.add(trunk);
const leaves = new THREE.Mesh(treeLeavesGeometry, treeLeavesMaterial);
leaves.position.set(x, 3.5, z);
scene.add(leaves);
}
const innerForestRadius = 15;
const outerForestRadius = roadRadius - roadWidth / 2 - 2;
const numForestTrees = 150;
for (let i = 0; i < numForestTrees; i++) {
const angle = Math.random() * Math.PI * 2;
const radius = innerForestRadius + Math.random() * (outerForestRadius - innerForestRadius);
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
createTree(x, z);
}
const treeSpacing = 6;
const radialOffset = radialRoadWidth / 2 + 1;
for (let angle of radialAngles) {
for (let i = 1; i < radialRoadLength / treeSpacing; i++) {
const dist = i * treeSpacing;
const xLeft = Math.cos(angle) * (dist + roadRadius) + Math.sin(angle) * radialOffset;
const zLeft = Math.sin(angle) * (dist + roadRadius) - Math.cos(angle) * radialOffset;
createTree(xLeft, zLeft);
const xRight = Math.cos(angle) * (dist + roadRadius) - Math.sin(angle) * radialOffset;
const zRight = Math.sin(angle) * (dist + roadRadius) + Math.cos(angle) * radialOffset;
createTree(xRight, zRight);
}
}
// ===== 云朵 =====
const cloudMaterial = new THREE.MeshStandardMaterial({
color: 0xF5F5F5,
transparent: true,
opacity: 0.7,
roughness: 0.8,
metalness: 0.1
}); // 浅灰色半透明云朵,带光影
const clouds = [];
const numClouds = 30; // 增加云朵数量
function createCloud(x, z, y) {
const cloud = new THREE.Group();
const cloudGeo = new THREE.SphereGeometry(2, 8, 8); // 低多边形球体
const segments = 5 + Math.floor(Math.random() * 4); // 每朵云 5-8 个球体
for (let i = 0; i < segments; i++) {
const segment = new THREE.Mesh(cloudGeo, cloudMaterial);
segment.position.set(
(Math.random() - 0.5) * 5, // 更宽的偏移
(Math.random() - 0.5) * 3, // 更宽的垂直偏移
(Math.random() - 0.5) * 5
);
segment.scale.set(
0.7 + Math.random() * 0.8, // 更宽的缩放范围
0.5 + Math.random() * 0.5,
0.7 + Math.random() * 0.8
);
cloud.add(segment);
}
cloud.position.set(x, y, z);
cloud.userData = {
velocity: new THREE.Vector3((Math.random() - 0.5) * 0.02, 0, (Math.random() - 0.5) * 0.02), // 漂移速度
rotationSpeed: (Math.random() - 0.5) * 0.001 // 随机旋转速度
};
clouds.push(cloud);
scene.add(cloud);
}
// 在宽广区域生成云朵
const cloudRadius = 100;
for (let i = 0; i < numClouds; i++) {
const angle = Math.random() * Math.PI * 2;
const radius = 20 + Math.random() * cloudRadius;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
const y = 20 + Math.random() * 30; // 高度 20-50
createCloud(x, z, y);
}
// ===== 热气球 =====
const balloonMaterial = new THREE.MeshStandardMaterial({ roughness: 0.4, metalness: 0.2 });
const basketMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.6 });
const ropeMaterial = new THREE.LineBasicMaterial({ color: 0x333333, linewidth: 2 });
const graffitiMaterial = new THREE.MeshBasicMaterial({ color: 0xFFFFFF, transparent: true, opacity: 0.8 });
const hotAirBalloons = [];
const numBalloons = 8; // 增加到8个热气球
function createStarShape() {
const shape = new THREE.Shape();
const outerRadius = 0.8;
const innerRadius = 0.4;
for (let i = 0; i < 10; i++) {
const angle = (i / 10) * Math.PI * 2;
const radius = i % 2 === 0 ? outerRadius : innerRadius;
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
if (i === 0) shape.moveTo(x, y);
else shape.lineTo(x, y);
}
shape.closePath();
return shape;
}
function createHotAirBalloon() {
const balloon = new THREE.Group();
// 气球主体(球体,精细立体)
const balloonGeo = new THREE.SphereGeometry(3, 32, 32);
const balloonMesh = new THREE.Mesh(balloonGeo, balloonMaterial.clone());
const hue = Math.random() < 0.5 ? Math.random() * 60 / 360 : (180 + Math.random() * 120) / 360; // 橙黄或蓝紫
balloonMesh.material.color.setHSL(hue, 0.5 + Math.random() * 0.2, 0.6 + Math.random() * 0.2);
balloonMesh.position.y = 4;
balloon.add(balloonMesh);
// 刻度纹理(垂直和水平线)
for (let i = 0; i < 12; i++) { // 12条经线
const linePoints = [
new THREE.Vector3(0, -3, 0),
new THREE.Vector3(0, 3, 0)
];
const lineGeo = new THREE.BufferGeometry().setFromPoints(linePoints);
const line = new THREE.Line(lineGeo, ropeMaterial);
line.rotation.y = i * (Math.PI / 6);
balloonMesh.add(line);
}
for (let y = -2; y <= 2; y += 1) { // 4条纬线
if (y === 0) continue;
const radius = Math.sqrt(9 - y * y);
const circlePoints = [];
for (let i = 0; i <= 32; i++) {
const angle = (i / 32) * Math.PI * 2;
circlePoints.push(new THREE.Vector3(Math.cos(angle) * radius, y, Math.sin(angle) * radius));
}
const circleGeo = new THREE.BufferGeometry().setFromPoints(circlePoints);
const circle = new THREE.Line(circleGeo, ropeMaterial);
balloonMesh.add(circle);
}
// 涂鸦(圆形或星形)
const numGraffiti = 3 + Math.floor(Math.random() * 3);
for (let i = 0; i < numGraffiti; i++) {
const isCircle = Math.random() < 0.5;
const graffitiGeo = isCircle ? new THREE.CircleGeometry(0.5 + Math.random() * 0.5, 8) : new THREE.ShapeGeometry(createStarShape());
const graffiti = new THREE.Mesh(graffitiGeo, graffitiMaterial.clone());
if (!isCircle) graffiti.material.color.setHex(0xCCCCCC);
const theta = Math.random() * Math.PI;
const phi = Math.random() * Math.PI * 2;
const r = 3.01; // 稍超出球体表面
graffiti.position.set(
r * Math.sin(theta) * Math.cos(phi),
r * Math.cos(theta),
r * Math.sin(theta) * Math.sin(phi)
);
graffiti.lookAt(balloonMesh.position);
balloonMesh.add(graffiti);
}
// 篮子
const basketGeo = new THREE.BoxGeometry(1.5, 1, 1.5);
const basket = new THREE.Mesh(basketGeo, basketMaterial);
basket.position.y = -1;
balloon.add(basket);
// 连接绳
const ropeGeo = new THREE.CylinderGeometry(0.05, 0.05, 5, 8);
for (let i = 0; i < 4; i++) {
const rope = new THREE.Mesh(ropeGeo, basketMaterial);
const angle = i * (Math.PI / 2);
rope.position.set(Math.cos(angle) * 0.6, -0.5, Math.sin(angle) * 0.6);
balloon.add(rope);
}
// 随机初始角度和高度
balloon.userData = {
angle: Math.random() * Math.PI * 2,
speed: 0.001 + Math.random() * 0.001,
trajectoryRadius: 50 + Math.random() * 20,
height: 20 + Math.random() * 30
};
hotAirBalloons.push(balloon);
scene.add(balloon);
}
// 生成热气球
for (let i = 0; i < numBalloons; i++) {
createHotAirBalloon();
}
// ===== 优化车流 =====
const cars = [];
const carGeometry = new THREE.BoxGeometry(1.5, 0.7, 0.8);
const numCars = 100;
const laneOffset = radialRoadWidth / 4;
const innerRadius = roadRadius - roadWidth / 4;
const outerRadius = roadRadius + roadWidth / 4;
for (let i = 0; i < numCars; i++) {
const carMaterial = new THREE.MeshStandardMaterial({
color: new THREE.Color(Math.random(), Math.random(), Math.random()),
emissive: new THREE.Color(Math.random(), Math.random(), Math.random()),
emissiveIntensity: 0.8
});
const car = new THREE.Mesh(carGeometry, carMaterial);
const initialPath = Math.random() < 0.5 ? 'ring' : 'radial';
let initialAngle, initialDirection, initialRadius, initialPosition, initialLane;
if (initialPath === 'ring') {
initialAngle = Math.random() * Math.PI * 2;
initialDirection = Math.random() < 0.5 ? 1 : -1;
initialRadius = initialDirection === 1 ? outerRadius : innerRadius;
car.userData = {
path: 'ring',
angle: initialAngle,
speed: (0.0015 + Math.random() * 0.0025) * initialDirection,
radius: initialRadius,
lane: 0
};
} else {
initialAngle = radialAngles[Math.floor(Math.random() * numRadials)];
initialDirection = Math.random() < 0.5 ? 1 : -1;
initialPosition = Math.random() * radialRoadLength;
initialLane = initialDirection === 1 ? laneOffset : -laneOffset;
car.userData = {
path: 'radial',
angle: initialAngle,
position: initialPosition,
speed: (0.05 + Math.random() * 0.03) * initialDirection,
lane: initialLane
};
}
scene.add(car);
cars.push(car);
}
// ===== 烟花代码 =====
const fireworks = [];
const particleGeometry = new THREE.SphereGeometry(0.08, 8, 8);
const innerCircleRadius = roadRadius - roadWidth / 2;
function createExplosion(originPosition, color) {
const numParticles = 60 + Math.floor(Math.random() * 30);
const explosionRadius = 0.5 + Math.random();
for (let i = 0; i < numParticles; i++) {
const particleMaterial = new THREE.MeshBasicMaterial({ color: color });
const particle = new THREE.Mesh(particleGeometry, particleMaterial);
particle.position.copy(originPosition);
const velocity = new THREE.Vector3(
(Math.random() - 0.5) * explosionRadius,
(Math.random() - 0.5) * explosionRadius,
(Math.random() - 0.5) * explosionRadius
);
particle.userData = {
velocity: velocity,
life: 1.5 + Math.random() * 1,
state: 'exploded'
};
fireworks.push(particle);
scene.add(particle);
}
}
function launchFirework(startPosition) {
const rocketMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const rocket = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.3, 0.1), rocketMaterial);
rocket.position.copy(startPosition);
const targetHeight = 12 + Math.random() * 6;
const color = new THREE.Color().setHSL(Math.random(), 1, 0.5).getHex();
rocket.userData = {
velocity: new THREE.Vector3(0, 0.35 + Math.random() * 0.25, 0),
state: 'rising',
targetY: targetHeight,
color: color
};
fireworks.push(rocket);
scene.add(rocket);
}
function launchInitialFireworks() {
for (let i = 0; i < 15; i++) {
const angle = (i / 15) * Math.PI * 2;
const radius = Math.random() * innerCircleRadius;
const position = new THREE.Vector3(Math.cos(angle) * radius, 0.1, Math.sin(angle) * radius);
launchFirework(position);
}
}
function launchIntermittentFirework() {
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * innerCircleRadius;
const position = new THREE.Vector3(Math.cos(angle) * radius, 0.1, Math.sin(angle) * radius);
launchFirework(position);
}
function launchTieredFireworks() {
const numTiers = 6;
const delayPerTier = 400;
const baseHeight = 12;
const heightStep = 3;
for (let i = 0; i < numTiers; i++) {
setTimeout(() => {
const angle = (i / numTiers) * Math.PI * 2;
const radius = innerCircleRadius * 0.8;
const position = new THREE.Vector3(Math.cos(angle) * radius, 0.1, Math.sin(angle) * radius);
const rocketMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const rocket = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.3, 0.1), rocketMaterial);
rocket.position.copy(position);
rocket.userData = {
velocity: new THREE.Vector3(0, 0.35, 0),
state: 'rising',
targetY: baseHeight + i * heightStep,
color: new THREE.Color().setHSL(Math.random(), 1, 0.5).getHex()
};
fireworks.push(rocket);
scene.add(rocket);
}, i * delayPerTier);
}
}
function updateFireworks() {
for (let i = fireworks.length - 1; i >= 0; i--) {
const p = fireworks[i];
if (p.userData.state === 'rising') {
p.position.add(p.userData.velocity);
if (p.position.y >= p.userData.targetY) {
createExplosion(p.position, p.userData.color);
scene.remove(p);
fireworks.splice(i, 1);
}
} else if (p.userData.state === 'exploded') {
p.userData.life -= 0.015;
p.position.add(p.userData.velocity);
p.material.opacity = Math.max(0, p.userData.life / 2);
p.material.transparent = true;
p.userData.velocity.y -= 0.002;
if (p.userData.life <= 0) {
scene.remove(p);
fireworks.splice(i, 1);
}
}
}
}
// 相机
camera.position.set(45, 45, 45);
camera.lookAt(0, 7.5, 0);
// 动画
function animate() {
requestAnimationFrame(animate);
// 更新云朵位置和旋转
clouds.forEach(cloud => {
cloud.position.add(cloud.userData.velocity);
cloud.rotation.y += cloud.userData.rotationSpeed; // 微小旋转
// 云朵超出边界时回绕
if (cloud.position.x > cloudRadius) cloud.position.x -= 2 * cloudRadius;
if (cloud.position.x < -cloudRadius) cloud.position.x += 2 * cloudRadius;
if (cloud.position.z > cloudRadius) cloud.position.z -= 2 * cloudRadius;
if (cloud.position.z < -cloudRadius) cloud.position.z += 2 * cloudRadius;
});
// 更新热气球位置
hotAirBalloons.forEach(balloon => {
balloon.userData.angle += balloon.userData.speed;
const trajRadius = balloon.userData.trajectoryRadius;
balloon.position.x = Math.cos(balloon.userData.angle) * trajRadius;
balloon.position.z = Math.sin(balloon.userData.angle) * trajRadius;
balloon.position.y = balloon.userData.height + Math.sin(balloon.userData.angle * 5) * 1; // 轻微上下浮动
});
// 更新车辆
cars.forEach(car => {
if (car.userData.path === 'ring') {
car.userData.angle += car.userData.speed;
const currentAngle = car.userData.angle % (Math.PI * 2);
for (const radialAngle of radialAngles) {
if (Math.abs(currentAngle - radialAngle) < 0.05 && Math.random() < 0.01) {
const newDirection = car.userData.speed > 0 ? 1 : -1;
car.userData.path = 'radial';
car.userData.angle = radialAngle;
car.userData.position = newDirection > 0 ? 0 : radialRoadLength;
car.userData.speed = (0.05 + Math.random() * 0.03) * newDirection;
car.userData.lane = car.userData.speed > 0 ? laneOffset : -laneOffset;
break;
}
}
car.position.set(
Math.cos(car.userData.angle) * car.userData.radius,
0.4,
Math.sin(car.userData.angle) * car.userData.radius
);
car.rotation.y = -car.userData.angle + Math.PI / 2;
} else if (car.userData.path === 'radial') {
car.userData.position += car.userData.speed;
if ((car.userData.speed > 0 && car.userData.position > radialRoadLength) || (car.userData.speed < 0 && car.userData.position < 0)) {
const newDirection = car.userData.speed > 0 ? 1 : -1;
car.userData.path = 'ring';
car.userData.angle = car.userData.angle + (Math.PI / 2);
car.userData.speed = (0.0015 + Math.random() * 0.0025) * newDirection;
car.userData.radius = newDirection > 0 ? outerRadius : innerRadius;
}
const baseX = Math.cos(car.userData.angle) * (car.userData.position + roadRadius);
const baseZ = Math.sin(car.userData.angle) * (car.userData.position + roadRadius);
const offsetX = Math.sin(car.userData.angle) * car.userData.lane;
const offsetZ = -Math.cos(car.userData.angle) * car.userData.lane;
car.position.set(baseX + offsetX, 0.4, baseZ + offsetZ);
car.rotation.y = -car.userData.angle + (car.userData.speed > 0 ? 0 : Math.PI);
}
});
// 间歇性发射烟花
if (Math.random() < 0.005) {
launchIntermittentFirework();
}
// 更新烟花
updateFireworks();
// 场景缓慢旋转
scene.rotation.y += 0.002;
renderer.render(scene, camera);
}
// 初始烟花
launchInitialFireworks();
animate();
// 按钮事件
document.getElementById('controls').addEventListener('click', launchTieredFireworks);
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
显示效果