1. Three.js 多相机切换与画中画效果

在Three.js中实现多相机切换和画中画(Picture-in-Picture,PiP)效果是一个常见的需求,常用于监控、多视角展示等场景。以下是一个完整的实现方案:

1.1. 基础场景设置

首先创建包含多个相机的场景:

javascript

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 场景、渲染器
const scene = new THREE.Scene();
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 主相机(默认视角)
const mainCamera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
mainCamera.position.set(5, 5, 5);
mainCamera.lookAt(0, 0, 0);

// 辅助相机(用于画中画)
const pipCamera = new THREE.PerspectiveCamera(
  60,
  1, // 初始宽高比,稍后调整
  0.1,
  1000
);
pipCamera.position.set(0, 10, 0);
pipCamera.lookAt(0, 0, 0);

// 鸟瞰相机
const topCamera = new THREE.PerspectiveCamera(
  90,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
topCamera.position.set(0, 20, 0);
topCamera.lookAt(0, 0, 0);
topCamera.up.set(0, 0, -1);

// 轨道控制器(仅控制主相机)
const controls = new OrbitControls(mainCamera, renderer.domElement);
controls.enableDamping = true;

// 添加到数组便于管理
const cameras = [mainCamera, pipCamera, topCamera];
let activeCameraIndex = 0;

// 添加一些示例物体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

// 添加灯光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);

1.2. 相机切换功能

javascript

// 相机切换函数
function switchCamera(index) {
  if (index >= 0 && index < cameras.length) {
    activeCameraIndex = index;
    updateCameraIndicator();
  }
}

// 键盘切换相机
document.addEventListener('keydown', (event) => {
  switch (event.key) {
    case '1':
      switchCamera(0); // 主相机
      break;
    case '2':
      switchCamera(1); // 画中画相机
      break;
    case '3':
      switchCamera(2); // 鸟瞰相机
      break;
  }
});

// UI指示器
function updateCameraIndicator() {
  const cameraNames = ['主相机', '画中画相机', '鸟瞰相机'];
  console.log(`当前相机: ${cameraNames[activeCameraIndex]}`);
}

1.3. 画中画渲染系统

javascript

class PictureInPictureSystem {
  constructor(renderer, mainScene, pipCamera) {
    this.renderer = renderer;
    this.scene = mainScene;
    this.pipCamera = pipCamera;

    // 创建画中画容器
    this.pipContainer = document.createElement('div');
    this.pipContainer.style.cssText = `
      position: absolute;
      top: 20px;
      right: 20px;
      width: 300px;
      height: 200px;
      border: 2px solid white;
      border-radius: 8px;
      overflow: hidden;
      background: rgba(0, 0, 0, 0.7);
      z-index: 100;
    `;

    // 创建画中画Canvas
    this.pipCanvas = document.createElement('canvas');
    this.pipContext = this.pipCanvas.getContext('2d');
    this.pipContainer.appendChild(this.pipCanvas);
    document.body.appendChild(this.pipContainer);

    // 调整画中画大小
    this.setSize(300, 200);

    // 控制按钮
    this.createControls();
  }

  setSize(width, height) {
    this.pipCanvas.width = width;
    this.pipCanvas.height = height;
    this.pipCamera.aspect = width / height;
    this.pipCamera.updateProjectionMatrix();
  }

  createControls() {
    const controlsDiv = document.createElement('div');
    controlsDiv.style.cssText = `
      position: absolute;
      bottom: 5px;
      left: 0;
      right: 0;
      display: flex;
      justify-content: center;
      gap: 10px;
    `;

    // 切换视角按钮
    const views = [
      { name: '俯视', pos: [0, 10, 0], target: [0, 0, 0] },
      { name: '侧面', pos: [10, 0, 0], target: [0, 0, 0] },
      { name: '正面', pos: [0, 0, 10], target: [0, 0, 0] }
    ];

    views.forEach((view, index) => {
      const btn = document.createElement('button');
      btn.textContent = view.name;
      btn.style.cssText = `
        padding: 5px 10px;
        background: rgba(255, 255, 255, 0.2);
        color: white;
        border: 1px solid white;
        border-radius: 4px;
        cursor: pointer;
      `;
      btn.onclick = () => {
        this.pipCamera.position.set(...view.pos);
        this.pipCamera.lookAt(...view.target);
      };
      controlsDiv.appendChild(btn);
    });

    // 关闭按钮
    const closeBtn = document.createElement('button');
    closeBtn.textContent = '×';
    closeBtn.style.cssText = `
      position: absolute;
      top: 5px;
      right: 5px;
      width: 24px;
      height: 24px;
      background: rgba(255, 0, 0, 0.7);
      color: white;
      border: none;
      border-radius: 50%;
      cursor: pointer;
    `;
    closeBtn.onclick = () => {
      this.pipContainer.style.display = 'none';
    };

    this.pipContainer.appendChild(controlsDiv);
    this.pipContainer.appendChild(closeBtn);
  }

  render() {
    // 保存主渲染器状态
    const originalViewport = this.renderer.getViewport(new THREE.Vector4());
    const originalScissor = this.renderer.getScissor(new THREE.Vector4());

    // 设置画中画渲染区域
    const width = this.pipCanvas.width;
    const height = this.pipCanvas.height;

    // 直接渲染到临时Canvas
    this.renderer.setViewport(0, 0, width, height);
    this.renderer.setScissor(0, 0, width, height);
    this.renderer.setScissorTest(true);

    // 渲染画中画场景
    this.renderer.render(this.scene, this.pipCamera);

    // 将WebGL输出复制到2D Canvas
    this.pipContext.drawImage(
      this.renderer.domElement,
      0, 0, width, height,
      0, 0, width, height
    );

    // 恢复主渲染器状态
    this.renderer.setViewport(originalViewport);
    this.renderer.setScissor(originalScissor);
    this.renderer.setScissorTest(false);
  }
}

1.4. 主渲染循环

javascript

// 初始化画中画系统
const pipSystem = new PictureInPictureSystem(renderer, scene, pipCamera);

// 动画循环
function animate() {
  requestAnimationFrame(animate);

  // 更新控制器
  controls.update();

  // 旋转示例物体
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;

  // 获取当前活动相机
  const activeCamera = cameras[activeCameraIndex];

  // 渲染主场景
  renderer.setViewport(0, 0, window.innerWidth, window.innerHeight);
  renderer.setScissor(0, 0, window.innerWidth, window.innerHeight);
  renderer.render(scene, activeCamera);

  // 如果当前不是画中画相机,则渲染画中画
  if (activeCameraIndex !== 1) {
    pipSystem.render();
  }

  // 更新相机Frustum可视化
  updateCameraHelpers();
}

// 窗口大小调整
window.addEventListener('resize', () => {
  const width = window.innerWidth;
  const height = window.innerHeight;

  // 更新主相机
  mainCamera.aspect = width / height;
  mainCamera.updateProjectionMatrix();

  // 更新其他相机
  topCamera.aspect = width / height;
  topCamera.updateProjectionMatrix();

  renderer.setSize(width, height);
});

// 相机辅助可视化(可选)
const cameraHelpers = [];
function createCameraHelpers() {
  cameras.forEach(camera => {
    const helper = new THREE.CameraHelper(camera);
    scene.add(helper);
    cameraHelpers.push(helper);
    helper.visible = false;
  });
}

function updateCameraHelpers() {
  cameraHelpers.forEach(helper => {
    helper.update();
  });
}

// 初始化
createCameraHelpers();
animate();

1.5. 进阶功能:多视口渲染

javascript

class MultiViewportRenderer {
  constructor(renderer, scene) {
    this.renderer = renderer;
    this.scene = scene;
    this.viewports = [];
  }

  addViewport(camera, x, y, width, height, name) {
    const viewport = {
      camera,
      x, y, width, height,
      name,
      enabled: true
    };
    this.viewports.push(viewport);
    return viewport;
  }

  render() {
    // 清除整个屏幕
    this.renderer.clear();

    // 渲染每个视口
    this.viewports.forEach(vp => {
      if (!vp.enabled) return;

      // 设置视口
      this.renderer.setViewport(vp.x, vp.y, vp.width, vp.height);
      this.renderer.setScissor(vp.x, vp.y, vp.width, vp.height);
      this.renderer.setScissorTest(true);

      // 渲染该视口
      this.renderer.render(this.scene, vp.camera);

      // 绘制边框
      this.renderer.clearDepth();
      this.renderer.setScissorTest(false);
    });

    // 重置为全屏
    this.renderer.setViewport(0, 0, window.innerWidth, window.innerHeight);
  }
}

// 使用示例
const multiViewport = new MultiViewportRenderer(renderer, scene);
multiViewport.addViewport(mainCamera, 0, 0, window.innerWidth/2, window.innerHeight, '主视角');
multiViewport.addViewport(topCamera, window.innerWidth/2, 0, window.innerWidth/2, window.innerHeight/2, '鸟瞰');
multiViewport.addViewport(pipCamera, window.innerWidth/2, window.innerHeight/2, window.innerWidth/2, window.innerHeight/2, '画中画');

1.6. 性能优化建议

  1. 共享渲染目标:对于静态场景,考虑使用WebGLRenderTarget缓存渲染结果

  2. LOD优化:根据视口大小调整细节层次

  3. 视锥体剔除:确保只渲染可见物体

  4. 按需渲染:非活动视口可以降低渲染频率

这个实现提供了完整的相机切换和画中画功能,可以根据具体需求调整相机参数、布局和交互方式。