Skip to content

69. Reflector 实现镜子效果

Published:

上节我们用 CuebeCamera 实现了镜子的效果:

2025-04-12 13.40.18.gif

就是把材质的环境贴图 envMap 换成用 CubeCamera 实时拍的 6 个面的照片。

那如果两个镜子面对面,镜中是什么样的呢?

我们来试一下:

这里会用到新的 api,所以我们新建个项目:

npx create-vite reflector-mirror

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(300, 300, 300);
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();

function createMirror(z, rotationY) {
    const geometry = new THREE.PlaneGeometry(1000, 1000);
    const material = new THREE.MeshStandardMaterial({
        color: 'white',
        side: THREE.DoubleSide
    });
    const mesh = new THREE.Mesh(geometry, material);
    mesh.position.z = z;
    mesh.rotateY(rotationY);
    return mesh;
}

function createBall() {
    const geometry = new THREE.SphereGeometry(100);
    const material = new THREE.MeshStandardMaterial({
        color: 'lightgreen'
    });
    const mesh = new THREE.Mesh(geometry, material);
    return mesh;
}

group.add(createMirror(-500, 0));
group.add(createMirror(500, Math.PI));
group.add(createBall());

export default group;

画了两个平面、一个球。

跑下看下效果:

npm run dev

image.png

2025-04-13 13.17.23.gif

然后我们加上两个 CubeCamera,每个环境贴图都是单独的 CubeCamera 来拍的:

image.png

image.png

创建两个 CubeCamera 和 WebGLCubeRenderTarget

给函数增加两个参数,传入 name 和 envMap:

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

const cubeRenderTarget2 = new THREE.WebGLCubeRenderTarget(512);
export const cubeCamera2 = new THREE.CubeCamera( 1, 1000, cubeRenderTarget2 );

group.add(createMirror('mirror1', -500, 0, cubeRenderTarget.texture));
group.add(createMirror('mirror2', 500, Math.PI, cubeRenderTarget2.texture));
function createMirror(name, z, rotationY, texture) {
    const geometry = new THREE.PlaneGeometry(1000, 1000);
    const material = new THREE.MeshStandardMaterial({
        color: 'white',
        side: THREE.DoubleSide,
        roughness: 0,
        metalness: 1,
        envMap: texture
    });
    const mesh = new THREE.Mesh(geometry, material);
    mesh.name = name;
    mesh.position.z = z;
    mesh.rotateY(rotationY);
    return mesh;
}

然后在 main.js 里每帧要 update 一下 CubeCamera,并且 position 要和镜子的位置同步:

image.png

const mirror1 = mesh.getObjectByName('mirror1');
const mirror2 = mesh.getObjectByName('mirror2');

function render() {
    cubeCamera.position.copy(mirror1.position);
    cubeCamera.update(renderer, scene);

    cubeCamera2.position.copy(mirror2.position);
    cubeCamera2.update(renderer, scene);

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

render();

看下效果:

2025-04-13 13.38.09.gif

现在两边都是可以看到小球的。

现在太黑了,我们加载一下上节的天空盒图片做背景:

image.png

放到 public 目录下:

image.png

代码里加载下:

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-13 13.40.54.gif

可以看到,两面镜子都照出了小球,但看到的对面镜子都是没有 envMap 的状态。

为什么呢?

因为 envMap 是 CubeCamera 拍摄物体四周环境得到的,没拍完自然就没有 envMap,所以拍的时候对面的镜子是还没有环境贴图的状态。

其实 CubeCamera 并不是专门用来实现镜面效果的,它只是用来拍环境贴图 envMap 的。

如果你想实现镜子,最好用 Reflector 来做,它是专门用来实现镜面效果的。

注释掉 CubeCamera 的代码:

image.png

创建 mesh2.js

import * as THREE from 'three';
import { Reflector } from 'three/examples/jsm/Addons.js';

const group = new THREE.Group();

function createMirror(name, z, rotationY) {
    const geometry = new THREE.PlaneGeometry(1000, 1000);
    const mesh = new Reflector(geometry);
    mesh.name = name;
    mesh.position.z = z;
    mesh.rotateY(rotationY);
    return mesh;
}

function createBall() {
    const geometry = new THREE.SphereGeometry(100);
    const material = new THREE.MeshStandardMaterial({
        color: 'lightgreen'
    });
    const mesh = new THREE.Mesh(geometry, material);
    return mesh;
}
group.add(createMirror('mirror1', -500, 0));
group.add(createMirror('mirror2', 500, Math.PI));
group.add(createBall());

export default group;

image.png

和之前代码的差别是不创建 Mesh 了,而是创建一个 Reflector,他是专门用来做镜面效果的,不用指定材质。

看下效果:

image.png

2025-04-13 14.11.41.gif

这样就实现了一个镜面效果。

用起来很简单,因为它都给你封装好了。

现在镜子里有点模糊:

image.png

这与设备像素比 dpr 有关,也就是一个像素代表几个物理像素。

默认镜面按照 dpr 为 1 来拍,

这样调节下:

image.png

const mesh = new Reflector(geometry, {
    textureWidth: window.innerWidth * window.devicePixelRatio,
    textureHeight: window.innerHeight * window.devicePixelRatio,
});

就清晰多了:

image.png

对比下刚才的:

image.png

很明显。

当然,两个镜子并不会无限反射下去,这样太消耗性能了,默认反射几次就不反射了。

你还可以给镜子设置一个颜色:

image.png

image.png

这样就是一个蓝色的镜子。

案例代码上传了小册仓库

总结

这节我们分别用 CubeCamera 和 Reflector 实现了相对的两个镜子的效果。

CubeCamera 拍的 6 张照片作为 envMap 可以实现镜面效果,但它并不能照出对面镜子的环境贴图,所以对面镜子是原本的颜色。

Reflector 是专门用来做镜面效果的,它可以实现两个镜子的相互反射,比较逼真。

所以,如果是设置 envMap,可以用 CubeCamera 来拍,比如汽车车身、车窗反射的光线。但如果是专门实现镜子,还是用 Reflector 来做更好。

评论