

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>多楼层室内定位可视化 Demo(A*避障)</title>
<style>
body { margin: 0; overflow: hidden; }
#layerControls { position: absolute; top: 10px; left: 10px; z-index: 100; background: rgba(255,255,255,0.9); padding: 10px; border-radius: 5px; }
#layerControls button { display: block; margin-bottom: 5px; width: 120px; }
</style>
</head>
<body>
<div id="layerControls">
<button data-layer="all">显示全部</button>
<button data-layer="0">楼层 1</button>
<button data-layer="1">楼层 2</button>
<button data-layer="2">楼层 3</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.125.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.125.2/examples/js/controls/OrbitControls.js"></script>
<script>
// ==================== THREE.js 基础 ====================
let scene, camera, renderer, controls;
let floors = [], floorMaterials = [];
let beacons = [], movingPoint;
let gridSize = 40, cellSize = 20; // 栅格参数
let mapGrid = []; // 每层栅格地图
let path = [], pathIndex = 0; // A*路径
let targetBeaconIndex = 0;
init();
animate();
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 1, 5000);
camera.position.set(500, 800, 1500);
renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
const gridHelper = new THREE.GridHelper(gridSize*cellSize, gridSize, 0x888888, 0xcccccc);
scene.add(gridHelper);
// 创建楼层
for(let i=0;i<3;i++){
const floorGroup = new THREE.Group();
const width = gridSize*cellSize, height=50, depth=gridSize*cellSize;
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshBasicMaterial({color:0x00ff00, transparent:true, opacity:0.2});
floorMaterials.push(material);
const mesh = new THREE.Mesh(geometry, material);
mesh.position.y = 50 + i*100;
floorGroup.add(mesh);
const edges = new THREE.EdgesGeometry(geometry);
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({color:0x00aa00}));
line.position.copy(mesh.position);
floorGroup.add(line);
scene.add(floorGroup);
floors.push(floorGroup);
// 生成栅格地图(0通行,1障碍)
let layerGrid = Array.from({length:gridSize},()=>Array(gridSize).fill(0));
// 随机添加障碍物(墙)
for(let w=0; w<60; w++){
const x = Math.floor(Math.random()*gridSize);
const z = Math.floor(Math.random()*gridSize);
layerGrid[x][z] = 1;
const wallGeo = new THREE.BoxGeometry(cellSize, 50, cellSize);
const wallMat = new THREE.MeshBasicMaterial({color:0x444444});
const wall = new THREE.Mesh(wallGeo, wallMat);
wall.position.set((x-gridSize/2)*cellSize+cellSize/2, 50 + i*100, (z-gridSize/2)*cellSize+cellSize/2);
scene.add(wall);
}
mapGrid.push(layerGrid);
// 蓝牙信标
const beaconGeo = new THREE.SphereGeometry(8,12,12);
const beaconMat = new THREE.MeshBasicMaterial({color:0x0000ff});
for(let b=0;b<5;b++){
let bx, bz;
do{
bx = Math.floor(Math.random()*gridSize);
bz = Math.floor(Math.random()*gridSize);
}while(layerGrid[bx][bz]===1); // 避开障碍
const beacon = new THREE.Mesh(beaconGeo, beaconMat);
beacon.position.set((bx-gridSize/2)*cellSize+cellSize/2, 50 + i*100, (bz-gridSize/2)*cellSize+cellSize/2);
scene.add(beacon);
beacons.push({mesh: beacon, floor: i, gridX: bx, gridZ: bz});
}
}
// 移动小球
const pointGeo = new THREE.SphereGeometry(10,16,16);
const pointMat = new THREE.MeshBasicMaterial({color:0xff0000});
movingPoint = new THREE.Mesh(pointGeo, pointMat);
movingPoint.position.set(0, 50, 0);
scene.add(movingPoint);
setNextTarget();
// 楼层按钮
document.querySelectorAll('#layerControls button').forEach(btn=>{
btn.addEventListener('click',()=>{
const layer = btn.getAttribute('data-layer');
if(layer==='all'){
floors.forEach(f=>f.visible=true);
floorMaterials.forEach(m=>m.color.set(0x00ff00));
movingPoint.visible = true;
beacons.forEach(b=>b.mesh.visible=true);
}else{
floors.forEach((f,i)=>f.visible=i==layer);
floorMaterials.forEach((m,i)=>m.color.set(i==layer?0xffaa00:0x00ff00));
movingPoint.position.y = 50 + layer*100;
movingPoint.position.x = 0;
movingPoint.position.z = 0;
movingPoint.visible = true;
beacons.forEach(b=>b.mesh.visible=(b.floor==layer));
}
});
});
window.addEventListener('resize',()=>{camera.aspect=window.innerWidth/window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight);});
}
// ==================== A* 路径规划 ====================
function setNextTarget(){
if(beacons.length===0) return;
targetBeacon = beacons[targetBeaconIndex % beacons.length];
const layerGrid = mapGrid[targetBeacon.floor];
path = findPath(layerGrid,
gridPos(movingPoint.position.x),
gridPos(movingPoint.position.z),
targetBeacon.gridX,
targetBeacon.gridZ
);
pathIndex=0;
targetBeaconIndex++;
}
function gridPos(coord){ return Math.floor((coord + gridSize*cellSize/2)/cellSize); }
function coordPos(grid){ return (grid - gridSize/2)*cellSize + cellSize/2; }
function updateMovingPoint(){
if(!path || pathIndex>=path.length) {
setNextTarget();
return;
}
const target = path[pathIndex];
const tx = coordPos(target.x);
const tz = coordPos(target.z);
const speed = 4;
const dx = tx - movingPoint.position.x;
const dz = tz - movingPoint.position.z;
const dist = Math.sqrt(dx*dx + dz*dz);
if(dist<speed){
movingPoint.position.x = tx;
movingPoint.position.z = tz;
pathIndex++;
}else{
movingPoint.position.x += dx/dist*speed;
movingPoint.position.z += dz/dist*speed;
}
}
// ==================== 简单 A* 算法 ====================
function findPath(grid, startX, startZ, endX, endZ){
const openList=[], closedList=[];
const nodes = [];
for(let x=0;x<gridSize;x++){
nodes[x]=[];
for(let z=0;z<gridSize;z++){
nodes[x][z]={x,z,g:0,h:0,f:0,parent:null,walkable:grid[x][z]===0};
}
}
function heuristic(a,b){ return Math.abs(a.x-b.x)+Math.abs(a.z-b.z); }
openList.push(nodes[startX][startZ]);
while(openList.length>0){
openList.sort((a,b)=>a.f-b.f);
const current = openList.shift();
closedList.push(current);
if(current.x===endX && current.z===endZ) {
const ret=[];
let c = current;
while(c){ ret.push({x:c.x,z:c.z}); c=c.parent; }
return ret.reverse();
}
const dirs=[[1,0],[-1,0],[0,1],[0,-1]];
for(const d of dirs){
const nx=current.x+d[0], nz=current.z+d[1];
if(nx<0||nz<0||nx>=gridSize||nz>=gridSize) continue;
const neighbor = nodes[nx][nz];
if(!neighbor.walkable || closedList.includes(neighbor)) continue;
const g = current.g+1;
if(!openList.includes(neighbor) || g<neighbor.g){
neighbor.g = g;
neighbor.h = heuristic(neighbor,{x:endX,z:endZ});
neighbor.f = neighbor.g + neighbor.h;
neighbor.parent = current;
if(!openList.includes(neighbor)) openList.push(neighbor);
}
}
}
return []; // 没路
}
// ==================== 动画循环 ====================
function animate(){
requestAnimationFrame(animate);
updateMovingPoint();
renderer.render(scene, camera);
controls.update();
}
</script>
</body>
</html>