文章目录
一、效果

二、简介
在上一篇《【Web】使用 Vue3+PlayCanvas 开发 3D 游戏(五)3D 模型鼠标交互控制》中,我们实现了基础的 3D 场景搭建、模型加载和鼠标交互控制能力。本篇将聚焦模拟自动驾驶全屏场景重建(SR)+ 精细化 3D 建模 方向,在原有基础上完成核心能力升级:新增 ROS 实时画面流接入、道路场景精细化渲染、动态滚动的道路树木效果、障碍物精准过滤与渲染优化,以及模型比例锁定等关键特性,最终实现更贴近真实自驾场景的 3D 可视化效果。
三、知识点
3.1、ROS实时画面流接入与优化
3.1.1、原生WebSocket对接ROS图片流
摒弃第三方依赖,通过纯原生 WebSocket 实现与 ROS 系统的Topic话题对接,支持二进制图片数据解析:
js
// 初始化ROS图片流WebSocket
const initRosImageWebSocket = () => {
imageWsRetryCount = 0;
if (rosImageWs) {
rosImageWs.close();
rosImageWs = null;
}
const wsUrl = 'ws://XXXX:XXXX/usb_cam/image_raw/compressed';
rosImageWs = new WebSocket(wsUrl);
rosImageWs.binaryType = 'arraybuffer';
rosImageWs.onopen = () => {
console.log('ROS图片流WebSocket连接成功');
imageWsRetryCount = 0;
rosImageWs.send(JSON.stringify({
op: 'subscribe',
topic: '/usb_cam/image_raw/compressed'
}));
};
rosImageWs.onmessage = (event) => {
try {
let rawData = event.data;
if (rawData instanceof ArrayBuffer) {
rawData = new TextDecoder().decode(rawData);
}
const rosMsg = JSON.parse(rawData);
const base64Image = rosMsg.msg?.data;
if (!base64Image) return;
// Base64转二进制流
const binaryStr = atob(base64Image);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) {
bytes[i] = binaryStr.charCodeAt(i);
}
renderRosImage(bytes.buffer);
} catch (error) {
console.error('处理ROS图片数据失败:', error);
}
};
// 错误处理、重连逻辑...
};
3.1.2、图片渲染节流+内存优化
针对 ROS 图片流高频推送特性,实现渲染节流(限制 20fps)和URL 内存释放,避免浏览器内存泄漏:
js
// ROS图片流渲染(节流+内存优化)
const renderRosImage = (data) => {
const now = Date.now();
// 节流控制:避免高频渲染
if (now - lastRenderTime < RENDER_INTERVAL) {
return;
}
const timeDiff = now - lastRenderTime;
lastRenderTime = now;
try {
const blob = new Blob([data], { type: 'image/jpeg' });
const imageUrl = URL.createObjectURL(blob);
if (rosImage.value) {
// 内存优化:释放旧URL
if (rosImage.value.src && imageUrlPool.has(rosImage.value.src)) {
URL.revokeObjectURL(rosImage.value.src);
imageUrlPool.delete(rosImage.value.src);
}
rosImage.value.src = imageUrl;
imageUrlPool.add(imageUrl);
// 计算并平滑FPS显示
if (timeDiff > 0) {
const currentFps = Math.round(1000 / timeDiff);
fpsCache.push(currentFps);
if (fpsCache.length > MAX_FPS_CACHE) fpsCache.shift();
const avgFps = Math.round(fpsCache.reduce((sum, fps) => sum + fps, 0) / fpsCache.length);
imageFps.value = Math.max(1, Math.min(60, avgFps));
}
}
} catch (error) {
console.error('解析ROS图片失败:', error);
imageFps.value = 0;
}
};
3.2、道路场景精细化重构
3.2.1、分层道路渲染(修复颜色与层级问题)
重构地面与道路的渲染逻辑,通过分层建模解决道路颜色被覆盖、层级错乱问题,同时增加道路边缘线增强视觉区分:
js
const createGround = () => {
// 1. 路外地面(绿色大范围背景)
const outerGround = new pc.Entity('outer-ground');
outerGround.addComponent('model', { type: 'box' });
outerGround.setLocalScale(400, 0.1, ROAD_LENGTH);
outerGround.setPosition(0, -0.1, 0);
const outerMat = new pc.StandardMaterial();
outerMat.diffuse = GROUND_COLOR; // 绿色地面
outerMat.roughness = 0.9;
outerMat.update();
outerGround.model.material = outerMat;
app.root.addChild(outerGround);
// 2. 道路(深灰色,强制深度写入保证层级)
const road = new pc.Entity('road');
road.addComponent('model', { type: 'box' });
road.setLocalScale(ROAD_WIDTH, 0.06, ROAD_LENGTH);
road.setPosition(0, -0.05, 0); // 提高层级避免被地面覆盖
const roadMat = new pc.StandardMaterial();
roadMat.diffuse = ROAD_COLOR; // 固定深灰色
roadMat.roughness = 0.8;
roadMat.depthWrite = true; // 强制深度写入
roadMat.update();
road.model.material = roadMat;
app.root.addChild(road);
// 3. 道路白色边缘线(增强视觉区分)
const createRoadLine = (xOffset) => {
const line = new pc.Entity('road-line');
line.addComponent('model', { type: 'box' });
line.setLocalScale(0.2, 0.07, ROAD_LENGTH);
line.setPosition(xOffset, -0.04, 0);
const lineMat = new pc.StandardMaterial();
lineMat.diffuse = new pc.Color(1, 1, 1); // 白色
lineMat.roughness = 0.5;
lineMat.depthWrite = true;
lineMat.update();
line.model.material = lineMat;
app.root.addChild(line);
};
const halfRoadWidth = ROAD_WIDTH / 2;
createRoadLine(halfRoadWidth - 0.1); // 右侧边缘线
createRoadLine(-halfRoadWidth + 0.1); // 左侧边缘线
};
3.2.2、动态滚动的道路树木效果
新增道路两侧树木的无缝滚动效果,模拟车辆行进时的视觉位移,同时支持树木位置重置与视角联动:
js
// 核心配置(树木滚动)
const TREE_SPACING = 50; // 树木间隔
const TREE_OFFSET = ROAD_WIDTH / 2 + 5; // 树木离道路边缘距离
const TREE_SCROLL_SPEED = 8; // 滚动速度(m/s)
let treeOffsetZ = 0; // 树木滚动偏移量
// 创建道路两侧树木(支持滚动)
const createRoadTrees = () => {
if (!treeTemplate) return;
// 清空现有树木
treeEntities.forEach(tree => {
app.root.removeChild(tree);
tree.destroy();
});
treeEntities = [];
// 生成双倍数量树木,支持无缝滚动
const startZ = -ROAD_LENGTH;
const endZ = ROAD_LENGTH;
// 左侧树木
for (let z = startZ; z <= endZ; z += TREE_SPACING) {
const treeEntity = makeTree(app.root, {
x: -TREE_OFFSET, y: 0, z: z
});
if (treeEntity) {
treeEntity.originalZ = z;
treeEntities.push(treeEntity);
}
}
// 右侧树木(错位排列)
for (let z = startZ + TREE_SPACING/2; z <= endZ; z += TREE_SPACING) {
const treeEntity = makeTree(app.root, {
x: TREE_OFFSET, y: 0, z: z
});
if (treeEntity) {
treeEntity.originalZ = z;
treeEntities.push(treeEntity);
}
}
};
// 更新树木位置实现无缝滚动
const updateTreePositions = () => {
if (treeEntities.length === 0) return;
const halfLength = ROAD_LENGTH / 2;
treeEntities.forEach(tree => {
const currentZ = tree.originalZ + treeOffsetZ;
// 超出范围时重置位置
let newZ = currentZ;
if (currentZ > halfLength) {
newZ = currentZ - ROAD_LENGTH * 2;
} else if (currentZ < -halfLength) {
newZ = currentZ + ROAD_LENGTH * 2;
}
tree.setPosition(tree.getPosition().x, tree.getPosition().y, newZ);
});
};
// 帧更新中驱动滚动
app.on('update', dt => {
// 树木滚动更新
treeOffsetZ += TREE_SCROLL_SPEED * dt;
updateTreePositions();
});
3.3、障碍物渲染精准化优化
3.3.1、道路范围过滤(替代距离过滤)
调整障碍物过滤逻辑,仅渲染道路范围内的障碍物,贴合自驾场景的真实感知:
js
const handleObstacleData = (obstacleData) => {
if (!obstacleData.id) return;
// ROS坐标系转换
const playerPos = player.getPosition();
const rosX = obstacleData.position.x;
const rosY = obstacleData.position.z;
const rotatedX = rosY;
const rotatedZ = -rosX;
const relativeX = -rotatedX - playerPos.x;
const relativeZ = rotatedZ - playerPos.z;
// 仅检查是否在道路范围内(X轴)
const halfRoadWidth = ROAD_WIDTH / 2;
const isInRoad = Math.abs(relativeX) <= halfRoadWidth;
if (obstacleData.isActive && isInRoad) {
updateObstacle(obstacleData, relativeX, relativeZ);
} else {
removeObstacle(obstacleData.id);
}
};
3.3.2、模型比例锁定(避免拉伸变形)
新增模型比例锁定常量,强制轿车模型等比缩放,解决不同障碍物模型拉伸变形问题:
js
// 模型比例锁定常量
const CAR_MODEL_SCALE = 0.4; // 轿车基础缩放
const CAR_ASPECT_RATIO = 1.8; // 轿车宽长比
// 更新障碍物时强制等比缩放
const updateObstacle = (data, relativeX, relativeZ) => {
// ... 省略其他逻辑 ...
if (data.size) {
const modelType = mapTypeToModel(data.type);
if (modelType === 'car') {
// 轿车强制等比缩放,忽略异常尺寸
obstacleEntity.children[0].setLocalScale(
CAR_MODEL_SCALE,
CAR_MODEL_SCALE,
CAR_MODEL_SCALE / CAR_ASPECT_RATIO
);
}
}
};
3.4、交互体验增强
3.4.1、视角重置联动树木滚动
优化resetCameraView方法,重置视角时同步重置树木滚动偏移,保证场景一致性:
javascript
const resetCameraView = () => {
cameraDistance = DEFAULT_CAMERA_DISTANCE;
cameraYaw = DEFAULT_CAMERA_YAW;
cameraPitch = DEFAULT_CAMERA_PITCH;
treeOffsetZ = 0; // 重置树木偏移
if (player) {
player.setPosition(0, 0.1, 0);
playerX.value = 0;
}
updateCameraPosition();
updateTreePositions(); // 刷新树木位置
};
3.4.2、实时场景信息展示
在页面中新增场景关键参数展示,便于调试与场景状态感知:
javascript
<div class="game-info">
当前角色位置:X: {{ playerX.toFixed(1) }} | 已加载障碍物:{{ obstacleCount }}
<br>路宽:{{ ROAD_WIDTH }}m | 小车行驶速度:{{ TREE_SCROLL_SPEED }}m/s
<br>操作说明:左键旋转 | 右键平移 | 滚轮缩放 | 左键双击恢复视角
</div>
<div class="ros-image-panel">
<h4>ROS实时画面 ({{ imageFps }} FPS)</h4>
<img ref="rosImage" class="ros-image" alt="ROS 摄像头画面" />
</div>
四、整体总结
4.1、架构升级
| 模块 | 上一版本功能 | 本版本新增 / 优化点 |
|---|---|---|
| ROS | 对接仅支持障碍物数据 | WebSocket新增 ROS 图片流 WebSocket、图片渲染节流、内存优化、FPS 实时显示 |
| 道路场景 | 基础地面 + 道路渲染 | 分层渲染修复颜色 / 层级问题、新增道路边缘线、道路两侧动态滚动树木 |
| 障碍物渲染 | 距离过滤、基础模型加载 | 道路范围精准过滤、模型比例锁定(等比缩放)、障碍物类型映射优化 |
| 交互控制 | 鼠标旋转 / 平移 / 缩放、双击重置视角 | 视角重置联动树木滚动、角色移动限制在道路内、键盘控制与树木滚动联动 |
| 性能优化 | 基础 WebSocket 重连 | 图片 URL 池管理(避免内存泄漏)、障碍物重复数据过滤、渲染节流 |
4.2、运行效果
- 3D 场景中呈现深灰色道路 + 绿色路外地面,道路两侧有白色边缘线,树木随 "车辆行进" 无缝滚动;
- ROS 实时摄像头画面在侧边面板显示,标注当前帧率,无明显卡顿与内存泄漏;
- 障碍物仅在道路范围内渲染,轿车模型无拉伸变形,不同类型障碍物(卡车、行人、路障)比例协调;
- 鼠标 / 键盘交互流畅,双击重置视角后树木与角色位置同步恢复,整体场景贴合自动驾驶可视化需求。
4.3、总结与展望
本篇在原有 3D 交互基础上,完成了模拟自驾场景的核心升级:通过 ROS 图片流接入实现 "视觉 + 3D" 双画面融合,通过道路精细化渲染和树木动态滚动提升场景真实感,通过障碍物精准过滤和模型比例锁定保证渲染准确性。后续拓展方向:
- 新增车辆速度与树木滚动速度的联动(根据真实车速调整);
- 新增障碍物碰撞检测与预警提示;
- 新增实时定位与实时俯视鸟瞰地图展示。
- 优化ROS数据与3D场景的时间同步;
- 优化全屏SR模式下的UI适配与场景交互增强;
核心代码已附在文中,可结合上一篇博客的基础框架,替换 / 新增对应模块即可实现上述效果。