《升级版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. 增加路线规划、自动巡线漫游
相关推荐
张人玉20 小时前
基于 Python 机器学习 与 PyQt5 桌面框架开发的可视化分析系统——基于大数据的网上购物消费行为分析可视化大屏
python·qt·机器学习·echarts·three.js
lilian2331 天前
HarmonyOS 7 新特性(十三)|3DGS 模型加载、交互与性能回退
3d·交互·harmonyos
xiaominlaopodaren1 天前
three.js地图视口瓦片(一):屏幕角点射线
javascript·gis·three.js
DolitD1 天前
云流技术深度剖析:单服务器下如何实现3D应用的多实例并发?
java·服务器·前端·3d·云原生·云计算
Liudef061 天前
腾讯混元Hunyuan3D-Part:重新定义3D部件生成的革命性架构
人工智能·3d·架构·腾讯混元hunyuan3d
2601_967659892 天前
大学实训室网络仿真环境搭建:Ranplan 3D射线跟踪双版本技术解析
网络·3d
七77.2 天前
SceneAssistant: A Visual Feedback Agent for Open-Vocabulary 3D Scene Generation
3d·agent·世界模型
rockingdingo3 天前
Codex Claude 智能体做3D/潮玩/IP设计——装上Craftsman Agent工匠智能体Skills 游泳男孩IP案例分享
网络协议·tcp/ip·3d
3D可视化大侠3 天前
数字孪生时空数据可视化:CIMPro 孪大师中的轨迹与粒子实现
3d·信息可视化
LONGZETECH3 天前
新能源汽车动力电池实训教学痛点与虚拟仿真技术解决方案
c语言·3d·unity·架构·汽车·汽车教学软件