《终极版3D数字沙盘:双视角漫游+建筑点击信息+昼夜一键切换》

终极版3D数字沙盘【双视角漫游+建筑点击信息+昼夜一键切换】

直接复制保存 sandbox-final.html 打开即用

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D数字沙盘 昼夜完整版</title>
    <style>
        *{margin:0;padding:0;overflow:hidden;}
        body{background:#0a101f;font-family:微软雅黑;}
        .tip{position:absolute;top:10px;left:10px;color:#fff;font-size:14px;z-index:999;background:rgba(0,0,0,0.5);padding:8px;border-radius:6px;}
        .info-box{
            position:absolute;top:120px;left:20px;width:280px;padding:15px;
            background:rgba(255,255,255,0.95);border-radius:8px;
            display:none;z-index:999;box-shadow:0 0 15px #000;
        }
        .info-box h3{color:#222;margin-bottom:10px;border-bottom:1px solid #ccc;padding-bottom:5px;}
        .info-box p{color:#555;line-height:1.6;font-size:13px;}
        .close{position:absolute;right:10px;top:8px;cursor:pointer;font-size:18px;color:#666;}
        .btn-group{position:absolute;top:10px;right:20px;z-index:999;display:flex;gap:10px;}
        .mode,.dayBtn{padding:6px 12px;border-radius:4px;cursor:pointer;color:#fff;border:none;font-size:14px;}
        .mode{background:#2266cc;}
        .dayBtn{background:#e63946;}
    </style>
</head>
<body>
<div class="tip">鼠标拖拽旋转 | 滚轮缩放 | 右键平移<br>WASD行走 空格上升 Shift下降</div>
<div class="btn-group">
    <button class="mode" id="switchMode">切换:第一人称漫游</button>
    <button class="dayBtn" id="switchDay">切换:夜晚模式</button>
</div>
<div class="info-box" id="infoBox">
    <span class="close" onclick="hideInfo()">×</span>
    <h3 id="buildName">建筑名称</h3>
    <p id="buildInfo">建筑详情信息</p>
</div>

<script type="importmap">
{
  "imports": {
    "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
    "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
  }
}
</script>

<script type="module">
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'

// 全局状态
let isFirstPerson = false;
let isNightMode = false;
const scene = new THREE.Scene()

// 相机
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.set(60, 80, 100)

// 渲染器
const renderer = new THREE.WebGLRenderer({antialias:true})
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.shadowMap.enabled = true
document.body.appendChild(renderer.domElement)

// 轨道控制器
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.06
controls.maxPolarAngle = Math.PI / 2 - 0.1
controls.minDistance = 30
controls.maxDistance = 200

// ========== 灯光组(昼夜切换核心) ==========
// 环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4)
// 太阳光
const dirLight = new THREE.DirectionalLight(0xffffff, 1.2)
dirLight.position.set(50, 100, 50)
dirLight.castShadow = true
dirLight.shadow.mapSize.set(2048,2048)
// 夜晚楼宇点灯光
const buildingLightArr = []
function createBuildingLight(x,y,z){
    const pointLight = new THREE.PointLight(0xffdd66,0,15)
    pointLight.position.set(x,y,z)
    scene.add(pointLight)
    buildingLightArr.push(pointLight)
}
// 预先布置建筑室内灯光
createBuildingLight(-35,12,25)
createBuildingLight(-20,8,30)
createBuildingLight(25,14,-20)
createBuildingLight(30,10,25)
createBuildingLight(-25,12,-30)
createBuildingLight(15,10,35)

// 初始化白天灯光
scene.add(ambientLight)
scene.add(dirLight)
setDayMode()

// 昼夜切换函数
function setDayMode(){
    isNightMode = false
    document.getElementById('switchDay').innerText = '切换:夜晚模式'
    scene.background.set(0x0a101f)
    scene.fog = new THREE.Fog(0x0a101f,80,300)
    ambientLight.intensity = 0.4
    dirLight.intensity = 1.2
    dirLight.color.set(0xffffff)
    buildingLightArr.forEach(light=>light.intensity=0)
}
function setNightMode(){
    isNightMode = true
    document.getElementById('switchDay').innerText = '切换:白天模式'
    scene.background.set(0x000515)
    scene.fog = new THREE.Fog(0x000515,50,200)
    ambientLight.intensity = 0.08
    dirLight.intensity = 0.15
    dirLight.color.set(0x8899bb)
    buildingLightArr.forEach(light=>light.intensity=1.2)
}
document.getElementById('switchDay').addEventListener('click',()=>{
    isNightMode ? setDayMode() : setNightMode()
})

// ========== 沙盘地面道路 ==========
const groundGeo = new THREE.PlaneGeometry(200, 200)
const groundMat = new THREE.MeshStandardMaterial({color:0x2e7d32})
const ground = new THREE.Mesh(groundGeo, groundMat)
ground.rotation.x = -Math.PI / 2
ground.receiveShadow = true
scene.add(ground)

const roadGeo = new THREE.PlaneGeometry(160,12)
const roadMat = new THREE.MeshStandardMaterial({color:0x444444})
const road1 = new THREE.Mesh(roadGeo, roadMat)
road1.rotation.x = -Math.PI/2
road1.position.y = 0.1
scene.add(road1)
const road2 = new THREE.Mesh(roadGeo.clone(), roadMat)
road2.rotation.x = -Math.PI/2
road2.rotation.z = Math.PI/2
road2.position.y = 0.1
scene.add(road2)

// ========== 建筑创建 + 信息绑定 ==========
const buildGroup = new THREE.Group()
scene.add(buildGroup)
const buildList = []

function createBuilding(x,z,w,h,d,color,name,desc){
    const geo = new THREE.BoxGeometry(w, h, d)
    const mat = new THREE.MeshStandardMaterial({color})
    const build = new THREE.Mesh(geo, mat)
    build.position.set(x, h/2, z)
    build.castShadow = true
    build.receiveShadow = true
    build.userData = {name,desc}
    buildGroup.add(build)
    buildList.push(build)
}

createBuilding(-35,25,12,18,10,0x3498db,"研发大楼","占地面积1200㎡,共6层,园区核心办公研发区域")
createBuilding(-20,30,10,12,8,0x9b59b6,"员工公寓","住宿生活区,配套食堂、便利店、休闲广场")
createBuilding(25,-20,15,22,12,0xe67e22,"生产车间","标准化工业厂房,智能生产线作业区")
createBuilding(30,25,8,15,8,0xe74c3c,"综合服务楼","园区行政、安保、会议接待中心")
createBuilding(-25,-30,14,20,10,0x1abc9c,"仓储中心","物资存放、物料周转、物流集散中心")
createBuilding(15,35,10,16,9,0xf1c40f,"文体活动中心","运动场馆、团建活动、休闲娱乐场所")

// ========== 绿化树木 ==========
function createTree(x,z){
    const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.4,0.4,3,8),new THREE.MeshStandardMaterial({color:0x8b4513}))
    trunk.position.set(x,1.5,z)
    const crown = new THREE.Mesh(new THREE.SphereGeometry(2.5,12,12),new THREE.MeshStandardMaterial({color:0x27ae60}))
    crown.position.set(x,4,z)
    scene.add(trunk,crown)
}
createTree(-50,0);createTree(50,0);createTree(0,-50);createTree(0,50)

// ========== 动态车流 ==========
const carList = []
function createCar(){
    const car = new THREE.Mesh(new THREE.BoxGeometry(3,1.2,1.8),new THREE.MeshStandardMaterial({color:Math.random()*0xffffff}))
    car.position.set(-80,0.6,0)
    car.userData.speed = 0.3 + Math.random()*0.3
    scene.add(car)
    carList.push(car)
}
for(let i=0;i<5;i++) createCar()

// ========== 建筑点击弹窗 ==========
const raycaster = new THREE.Raycaster()
const mouse = new THREE.Vector2()
const infoBox = document.getElementById('infoBox')
const buildName = document.getElementById('buildName')
const buildInfo = document.getElementById('buildInfo')

window.hideInfo = function(){
    infoBox.style.display = 'none'
}
function showInfo(name,info){
    buildName.innerText = name
    buildInfo.innerText = info
    infoBox.style.display = 'block'
}

window.addEventListener('click',e=>{
    if(isFirstPerson) return
    mouse.x = (e.clientX / window.innerWidth) * 2 - 1
    mouse.y = - (e.clientY / window.innerHeight) * 2 + 1
    raycaster.setFromCamera(mouse,camera)
    const res = raycaster.intersectObjects(buildList)
    if(res.length>0){
        const data = res[0].object.userData
        showInfo(data.name,data.desc)
    }
})

// ========== 视角切换 + 第一人称行走 ==========
const switchBtn = document.getElementById('switchMode')
let moveForward = false,moveBackward=false,moveLeft=false,moveRight=false
let moveUp=false,moveDown=false
const moveSpeed = 0.8

switchBtn.addEventListener('click',()=>{
    isFirstPerson = !isFirstPerson
    if(isFirstPerson){
        switchBtn.innerText = '切换:全局俯视视角'
        controls.enabled = false
        camera.position.set(0,12,0)
    }else{
        switchBtn.innerText = '切换:第一人称漫游'
        controls.enabled = true
        camera.position.set(60,80,100)
    }
})

document.addEventListener('keydown',e=>{
    switch(e.code){
        case 'KeyW':moveForward=true;break
        case 'KeyS':moveBackward=true;break
        case 'KeyA':moveLeft=true;break
        case 'KeyD':moveRight=true;break
        case 'Space':moveUp=true;break
        case 'ShiftLeft':moveDown=true;break
    }
})
document.addEventListener('keyup',e=>{
    switch(e.code){
        case 'KeyW':moveForward=false;break
        case 'KeyS':moveBackward=false;break
        case 'KeyA':moveLeft=false;break
        case 'KeyD':moveRight=false;break
        case 'Space':moveUp=false;break
        case 'ShiftLeft':moveDown=false;break
    }
})

// ========== 窗口自适应 ==========
window.addEventListener('resize',()=>{
    camera.aspect = window.innerWidth / window.innerHeight
    camera.updateProjectionMatrix()
    renderer.setSize(window.innerWidth, window.innerHeight)
})

// ========== 主动画循环 ==========
const clock = new THREE.Clock()
function animate(){
    requestAnimationFrame(animate)
    const delta = clock.getDelta()

    // 车辆循环行驶
    carList.forEach(car=>{
        car.position.x += car.userData.speed
        if(car.position.x > 80) car.position.x = -80
    })

    // 第一人称移动逻辑
    if(isFirstPerson){
        const dir = new THREE.Vector3()
        camera.getWorldDirection(dir)
        dir.y = 0
        dir.normalize()
        const right = new THREE.Vector3()
        right.crossVectors(camera.up,dir).normalize()

        if(moveForward) camera.position.addScaledVector(dir,moveSpeed*delta*60)
        if(moveBackward) camera.position.addScaledVector(dir,-moveSpeed*delta*60)
        if(moveLeft) camera.position.addScaledVector(right,moveSpeed*delta*60)
        if(moveRight) camera.position.addScaledVector(right,-moveSpeed*delta*60)
        if(moveUp) camera.position.y += moveSpeed*delta*60
        if(moveDown) camera.position.y -= moveSpeed*delta*60
    }

    controls.update()
    renderer.render(scene,camera)
}
animate()
</script>
</body>
</html>

全部集成功能汇总

  1. 基础沙盘场景
    园区草地、十字主干道、6大功能建筑、道路绿化树
  2. 动态交通
    彩色小车自动沿路循环行驶
  3. 建筑交互
    俯视视角点击建筑,弹出名称+详细介绍,可关闭
  4. 双视角自由切换
  • 全局俯视:拖拽旋转、滚轮缩放、全局浏览
  • 第一人称漫游:WASD行走、空格升空、Shift降落
  1. 昼夜光影一键切换
  • 白天:明亮太阳光、自然园区实景
  • 夜晚:整体环境变暗、楼宇室内灯光自动亮起、夜景氛围感十足
  1. 远景雾化
    远近层次分明,画面更真实
  2. 全屏自适应
    任意窗口大小自动适配

操作说明

  1. 右上角第一个按钮:切换漫游/俯视视角
  2. 右上角第二个按钮:切换白天/夜晚夜景
  3. 仅俯视视角可以点击建筑查看信息
  4. 漫游模式支持自由穿行整个园区

这套已经是中小型园区数字沙盘完整可用版本,直接就能用于演示、项目展示、教学演示。

相关推荐
云空4 小时前
《Three.js 3D实例大全》
开发语言·javascript·3d·three.js
云空5 小时前
《Three.js 3D坦克对战【AI单机版】》
javascript·人工智能·3d·three.js
杀生丸学AI6 小时前
【三维重建】ArtiFixer:自回归扩散增强与扩展3DGS(NVIDIA)
人工智能·3d·数据挖掘·aigc·三维重建·扩散模型·视觉大模型
苏州邦恩精密9 小时前
蔡司3D扫描仪厂家如何应用于新能源行业检测
人工智能·机器学习·3d·自动化·制造
云飞云共享云桌面10 小时前
河北钣金工厂10个solidworks设计共享一台高性能服务器的云桌面方案
运维·服务器·3d·自动化·负载均衡·制造
蓝速科技10 小时前
蓝速科技 3D 全息舱 AI 数字人校园智慧升级实效展示
人工智能·科技·3d
苏州邦恩精密11 小时前
蔡司3D扫描仪厂家如何应用于航空航天制造
大数据·数据库·人工智能·3d·自动化·制造
cd_949217211 天前
为什么AI3D创作中贴图材质尤为重要?因为它决定模型能否成为可用资产
人工智能·3d·贴图
李伟_Li慢慢1 天前
12-mini版WebGLObjects
前端·three.js