在Three.js中实现多相机切换和画中画(Picture-in-Picture,PiP)效果是一个常见的需求,常用于监控、多视角展示等场景。以下是一个完整的实现方案:
首先创建包含多个相机的场景:
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);
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]}`);
}
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);
}
}
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();
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, '画中画');
共享渲染目标:对于静态场景,考虑使用WebGLRenderTarget缓存渲染结果
LOD优化:根据视口大小调整细节层次
视锥体剔除:确保只渲染可见物体
按需渲染:非活动视口可以降低渲染频率
这个实现提供了完整的相机切换和画中画功能,可以根据具体需求调整相机参数、布局和交互方式。