【Web】使用Vue3+PlayCanvas开发3D游戏(六)模拟自驾场景SR+3D可视化

文章目录

一、效果

二、简介

在上一篇《【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、运行效果

  1. 3D 场景中呈现深灰色道路 + 绿色路外地面,道路两侧有白色边缘线,树木随 "车辆行进" 无缝滚动;
  2. ROS 实时摄像头画面在侧边面板显示,标注当前帧率,无明显卡顿与内存泄漏;
  3. 障碍物仅在道路范围内渲染,轿车模型无拉伸变形,不同类型障碍物(卡车、行人、路障)比例协调;
  4. 鼠标 / 键盘交互流畅,双击重置视角后树木与角色位置同步恢复,整体场景贴合自动驾驶可视化需求。

4.3、总结与展望

本篇在原有 3D 交互基础上,完成了模拟自驾场景的核心升级:通过 ROS 图片流接入实现 "视觉 + 3D" 双画面融合,通过道路精细化渲染和树木动态滚动提升场景真实感,通过障碍物精准过滤和模型比例锁定保证渲染准确性。后续拓展方向:

  1. 新增车辆速度与树木滚动速度的联动(根据真实车速调整);
  2. 新增障碍物碰撞检测与预警提示;
  3. 新增实时定位与实时俯视鸟瞰地图展示。
  4. 优化ROS数据与3D场景的时间同步;
  5. 优化全屏SR模式下的UI适配与场景交互增强;

核心代码已附在文中,可结合上一篇博客的基础框架,替换 / 新增对应模块即可实现上述效果。

相关推荐
吴所畏惧1 小时前
前端打包cdn或者dll打包方式
前端
小鲤鱼ya2 小时前
vue3 + ts + uni-app 移动端封装图片上传添加水印
前端·typescript·uni-app·vue3
qq_283720052 小时前
WebGL基础教程(十四):投影矩阵深度解析——正交 vs 透视,彻底搞懂3D视觉魔法
3d·矩阵·webgl
霍理迪2 小时前
Vue—条件渲染与循环渲染
前端·javascript·vue.js
xixixin_2 小时前
【CSS】字体大小不一致?px与vw渲染差异的底层原理与解决方案
前端·css
小J听不清2 小时前
CSS 内边距(padding)全解析:取值规则 + 表格实战
前端·javascript·css·html·css3
zhangjikuan892 小时前
在 ArkTS 中,Promise 的使用比 TypeScript 更严格(必须显式指定泛型类型)
前端·javascript·typescript
桐溪漂流2 小时前
Uni-app H5 环境下 ResizeObserver 监听 mp-html 动态高度
前端·uni-app·html
风酥糖2 小时前
Godot游戏练习01-第15节-敌人生成动画,翻转,碰撞
游戏·游戏引擎·godot