Skip to content

68. CubeCamera 实现镜子效果

Published:

通过环境贴图 envMap,我们能让物体映射出周围的环境来:

2025-04-09 19.16.46.gif

但我们之前用的是加载的 6 张纹理图片。

如果场景中有其他物体呢?

如何把这些物体也照出来?

这就需要用到 CubeCamera 了:

image.png

创建 CubeCamera 来照前后左右上下 6 个面的照片,把结果保存到 WebGLCubeRenderTarget,然后作为贴图设置到 envMap。

这样,物体表面就能映射出实时的环境来。

我们试一下:

npx create-vite cube-camera-envmap

image.png 进入项目,安装依赖:

npm install
npm install --save three
npm install --save-dev @types/three

改下 src/main.js

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

const scene = new THREE.Scene();
scene.add(mesh);

const directionLight = new THREE.DirectionalLight(0xffffff, 2);
directionLight.position.set(500, 400, 300);
scene.add(directionLight);

const ambientLight = new THREE.AmbientLight();
scene.add(ambientLight);

const width = window.innerWidth;
const height = window.innerHeight;

const helper = new THREE.AxesHelper(500);
// scene.add(helper);

const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 10000);
camera.position.set(500, 1000, 1000);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer({
  antialias: true
});
renderer.setSize(width, height)

function render() {
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

render();

document.body.append(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);

创建 Scene、Light、Camera、Renderer。

改下 style.css

body {
  margin: 0;
}

写下 mesh.js

import * as THREE from 'three';

const group = new THREE.Group();

const geometry = new THREE.PlaneGeometry(1000, 1000);
const material = new THREE.MeshStandardMaterial({
    color: 'white'
});
const mesh = new THREE.Mesh(geometry, material);
group.add(mesh);

const geometry2 = new THREE.SphereGeometry(100);
const material2 = new THREE.MeshStandardMaterial({
    color: 'lightgreen'
});
const mesh2 = new THREE.Mesh(geometry2, material2);
mesh2.position.set(0, 0, 500);
group.add(mesh2);

export default group;

画了一个平面、一个球。

跑下看下效果:

npm run dev

image.png

2025-04-12 13.23.06.gif

然后我们让这个平面变成镜面:

image.png

设置粗糙度为 0,金属度设为 1,这样就是镜面的效果。

2025-04-12 13.24.16.gif

现在是黑的是因为还没有 envMap 环境贴图,环境都是黑的。

我们先找 6 张天空盒图片作为环境贴图看下效果:

上节的那 6 张图片拿过来:

image.png

放到 public 目录下:

image.png

用 CubeTextureLoader 加载天空盒的 6 张图作为环境贴图:

image.png

const textureCube = new THREE.CubeTextureLoader()
    .setPath('./city/')
    .load(['px.png', 'nx.png', 'py.png', 'ny.png', 'pz.png', 'nz.png']);

const geometry = new THREE.PlaneGeometry(1000, 1000)
const material = new THREE.MeshStandardMaterial({
    color: 'white',
    metalness: 1,
    roughness: 0,
    envMap: textureCube
});

顺便设置下 scene.background

image.png

const textureCube = new THREE.CubeTextureLoader()
    .setPath('./city/')
    .load(['px.png', 'nx.png', 'py.png', 'ny.png', 'pz.png', 'nz.png']);
scene.background = textureCube;

2025-04-12 13.26.07.gif

可以看到,现在能照出环境了,但只是贴图,并不是真实的环境。

比如这个角度看不到小球:

image.png

如果想实时照出真实环境,就需要 CubeCamera 来实时拍这 6 张图了。

image.png

const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256);
export const cubeCamera = new THREE.CubeCamera( 1, 1000, cubeRenderTarget );

const geometry = new THREE.PlaneGeometry(1000, 1000, 10)
const material = new THREE.MeshStandardMaterial({
    color: 'white',
    metalness: 1,
    roughness: 0,
    envMap: cubeRenderTarget.texture
});
const mesh = new THREE.Mesh(geometry, material);
group.add(mesh);

这里创建 CubeCamera,它只有近裁截面、远裁截面 2 个参数,我们指定 1 到 1000 的范围。

然后指定渲染目标,渲染到 WebGLCubeRenderTarget 对象。

参数 128 是 size,也就是 128 * 128 像素,一般设置 2 的多少次方,比如 32、64、128、256、512、1024 这种

渲染出来后把 cubeRenderTarget.texture 作为 envMap 就好了。

因为每一帧都得渲染,我们要在渲染循环里调用下 update:

image.png

cubeCamera.position.copy(mesh.children[0].position)
cubeCamera.update(renderer, scene);

因为是从镜子的角度去看周围,这里 CubeCamera 自然是放在镜子的位置。

看下效果:

2025-04-12 13.29.05.gif

现在就可以看到小球了。

我们把 size 改成 32 试一下:

image.png

32 * 32 的相片非常模糊:

image.png

改成 512 * 512 就清楚多了:

image.png

image.png

然后我们让小球动一下:

引入 tween.js

npm install --save @tweenjs/tween.js

image.png

let r = 800;
export const ballTween = new Tween({ angle: 0}).to({
    angle: Math.PI
}, 5000)
.repeat(Infinity)
.onUpdate(obj => {
    mesh2.position.x = Math.cos(obj.angle) * r;
    mesh2.position.z = Math.sin(obj.angle) * r;
}).start();

angle 从 0 到 Math.PI,然后算出 cos、sin 作为 x、z,五秒一次,一直重复。

image.png

function render(time) {
    cubeCamera.position.copy(mesh.children[0].position)
    cubeCamera.update(renderer, scene);
    ballTween.update(time);
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

每帧都要 update 下数值。

2025-04-12 13.40.18.gif

这样,镜子就完成了。

案例代码上传了小册仓库

总结

这节我们实现了镜子的效果。

不能用 CubeTextureLoader 加载的图片作为环境贴图 envMap 了,而是要用 CubeCamera 来在物体的位置实时拍摄 6 张图。

CubeCamera 拍摄的照片存在 WebGLCubeRenderTarget 上,size 一般是 2 的多少次方,比如 64、128、256、512 等。

并且每帧调用下 cubeCamera.update 来重新拍摄照片。

这样就能实现镜子的效果,实时映射出周围的物体。

评论