《升级版3D数字沙盘(点击弹窗+第一人称漫游双功能)》

升级版3D数字沙盘(点击弹窗+第一人称漫游双功能)

保存为sandbox-pro.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;}
        .mode{position:absolute;top:10px;right:20px;color:#fff;background:#2266cc;padding:6px 12px;border-radius:4px;cursor:pointer;z-index:999;}
    </style>
</head>
<body>
<div class="tip">鼠标拖拽旋转 | 滚轮缩放 | 右键平移<br>WASD行走 空格上升 Shift下降</div>
<div class="mode" id="switchMode">切换:自由俯视视角</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;
const scene = new THREE.Scene()
scene.background = new THREE.Color(0x0a101f)
scene.fog = new THREE.Fog(0x0a101f, 80, 300)

// 相机
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)
scene.add(ambientLight)
const dirLight = new THREE.DirectionalLight(0xffffff, 1.2)
dirLight.position.set(50, 100, 50)
dirLight.castShadow = true
dirLight.shadow.mapSize.set(2048,2048)
scene.add(dirLight)

// ========== 沙盘地面道路 ==========
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. 建筑点击弹窗信息

  • 点击任意楼栋,左侧自动弹出名称+详细介绍
  • 点击关闭按钮即可收起面板
  • 俯视视角下可点击,漫游模式屏蔽点击

2. 双视角一键切换

  1. 自由俯视视角(默认)

    • 鼠标拖拽旋转、滚轮缩放、右键平移
    • 全局俯瞰整个园区沙盘
  2. 第一人称步行漫游

    • W前进 S后退 A左移 D右移
    • 空格键向上飞行,左Shift向下降落
    • 身临其境逛园区、穿行楼栋之间

现有完整场景

✅ 绿色草地园区基底 + 十字交通主干道

✅ 六大功能分区建筑(研发/公寓/车间/服务楼等)

✅ 道路两侧绿化树木

✅ 动态车流自动行驶

✅ 真实太阳光阴影+远景雾化效果

✅ 视角无缝切换+人物自由行走

✅ 建筑点位信息弹窗展示

后续可继续扩展

  1. 给建筑添加楼层分层结构
  2. 加入园区灯光夜景模式
  3. 添加监控摄像头图标、设备点位标注
  4. 接入实时数据面板显示园区能耗、人流
  5. 增加路线规划、自动巡线漫游
相关推荐
Strayer11 小时前
拓扑管网 3D 可视化大屏demo:科技感(地图 + 拓扑 + 3D 空间)
前端·three.js·数据可视化
Quz17 小时前
QML 文字入场动画:逐字弹出、逐字飞入、3D 飞入
qt·3d·动画·qml
蓝速科技2 天前
蓝速科技 3D 全息舱与 AI 数字人酒店降本增效实战
人工智能·科技·3d
在下胡三汉2 天前
GLB与GLTF:有什么区别——你应该用哪一个?
3d·gltf
玖玥拾2 天前
Unity 3D 笔记(八)ScrollRect 滚动视图、NavMesh 自动寻路系统
笔记·3d·unity
极客侃科技2 天前
如何把真人动作迁移到3D角色上?用V2Fun完成视频动捕和动作应用
3d
2601_956414142 天前
AI3D工作流应该包括哪些环节?用V2Fun从生成推进到可动资产
人工智能·3d
2601_956414143 天前
不用下载客户端的3D AI工具有哪些?2026网页端工具对比测评
人工智能·3d
蓝速科技3 天前
蓝速科技 3D 全息舱 AI 数字人景区全天候无人讲解方案
人工智能·科技·3d