终极版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>
全部集成功能汇总
- 基础沙盘场景
园区草地、十字主干道、6大功能建筑、道路绿化树 - 动态交通
彩色小车自动沿路循环行驶 - 建筑交互
俯视视角点击建筑,弹出名称+详细介绍,可关闭 - 双视角自由切换
- 全局俯视:拖拽旋转、滚轮缩放、全局浏览
- 第一人称漫游:WASD行走、空格升空、Shift降落
- 昼夜光影一键切换
- 白天:明亮太阳光、自然园区实景
- 夜晚:整体环境变暗、楼宇室内灯光自动亮起、夜景氛围感十足
- 远景雾化
远近层次分明,画面更真实 - 全屏自适应
任意窗口大小自动适配
操作说明
- 右上角第一个按钮:切换漫游/俯视视角
- 右上角第二个按钮:切换白天/夜晚夜景
- 仅俯视视角可以点击建筑查看信息
- 漫游模式支持自由穿行整个园区
这套已经是中小型园区数字沙盘完整可用版本,直接就能用于演示、项目展示、教学演示。