【HTML】3D动态凯旋门

发布于:2025-08-20 ⋅ 阅读:(20) ⋅ 点赞:(0)

目录

版本1.0:简易版本

版本2.0:建筑渲染

版本3.0:优化建筑群

版本4.0:增加公路和车流

版本5.0:去除压在公路上的建筑

版本6.0:优化车流群

版本7.0:添加烟花效果

版本8.0:添加树木

版本9.0:美化建筑群

版本10.0:添加云朵

版本11.0:添加动态热气球


版本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 > radialRoadLengthcar.userData.position < 0path'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>

显示效果



网站公告

今日签到

点亮在社区的每一天
去签到