升级版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. 双视角一键切换
-
自由俯视视角(默认)
- 鼠标拖拽旋转、滚轮缩放、右键平移
- 全局俯瞰整个园区沙盘
-
第一人称步行漫游
W前进S后退A左移D右移- 空格键向上飞行,左Shift向下降落
- 身临其境逛园区、穿行楼栋之间
现有完整场景
✅ 绿色草地园区基底 + 十字交通主干道
✅ 六大功能分区建筑(研发/公寓/车间/服务楼等)
✅ 道路两侧绿化树木
✅ 动态车流自动行驶
✅ 真实太阳光阴影+远景雾化效果
✅ 视角无缝切换+人物自由行走
✅ 建筑点位信息弹窗展示
后续可继续扩展
- 给建筑添加楼层分层结构
- 加入园区灯光夜景模式
- 添加监控摄像头图标、设备点位标注
- 接入实时数据面板显示园区能耗、人流
- 增加路线规划、自动巡线漫游