在Three.js的3D场景中,倒影效果能够显著提升视觉真实感和场景的沉浸体验。本篇教程将深入讲解倒影实现的核心原理和技术细节。

先来看下效果:

什么是倒影?

倒影本质上是物体在反射平面(通常是地面)上的镜像投影。在3D图形学中,我们通过数学变换来模拟这种视觉效果。

基本思路

想象一下现实中的倒影:你站在湖边,水面上会出现你的倒影。在3D世界里,我们可以这样模拟:

  1. 复制一个一模一样的物体
  2. 把它上下翻转
  3. 放到"地面"的下方
  4. 让它半透明,看起来像倒影

核心知识解析

创建倒影物体

首先,我们需要复制原来的物体:

function createAdvancedReflection(originalMesh) {
  const reflectionGeometry = originalMesh.geometry.clone();
  const reflectionMaterial = createGradientReflectionMaterial(originalMesh.material);
  
  const reflectionMesh = new THREE.Mesh(reflectionGeometry, reflectionMaterial);
  // ...
}

这里用了clone()来复制几何体和材质,这样倒影和原物体就不会互相影响了。

翻转物体

接下来就是关键的翻转操作:

reflectionMesh.scale.y = -1;

这行代码把物体在Y轴方向缩放了-1倍,相当于上下翻转。原来朝上的面现在朝下了,就像镜子里的效果一样。

摆放位置

翻转完了,还要把倒影放到正确的位置。对于立方体这种简单的情况:

reflectionMesh.position.y = -originalMesh.position.y;

如果原来的立方体在y=1.5的位置,倒影就放在y=-1.5,这样它们就关于地面(y=0)对称了。

让倒影更真实

单纯的翻转和移位还不够真实,我们需要让倒影有渐变透明的效果。现实中的倒影不是均匀的,通常是上面清晰,下面逐渐模糊消失。这需要渐变透明效果

这就需要修改材质的着色器:

function createGradientReflectionMaterial(originalMaterial) {
  const material = originalMaterial.clone();
  
  material.onBeforeCompile = (shader) => {
    // 在着色器中添加渐变效果
  };
  
  material.transparent = true;
  material.side = THREE.DoubleSide;
  return material;
}

onBeforeCompile是Three.js提供的一个钩子,让我们可以在材质编译前插入自定义代码。我们利用它来实现渐变透明效果。

我们需要知道每个像素在世界空间中的Y坐标,然后根据高度计算透明度,越接近地面的部分越透明,越远离地面的部分越不透明。

// 在顶点着色器中计算世界位置
shader.vertexShader = shader.vertexShader.replace(
    "#include <begin_vertex>",
    `
    #include <begin_vertex>
    vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
    `
);
// 在片元着色器中基于Y坐标计算透明度
shader.fragmentShader = shader.fragmentShader.replace(
    "gl_FragColor = vec4( outgoingLight, diffuseColor.a );",
    `
    float fadeDistance = 2.0;
    float fade = 1.0 - clamp((vWorldPosition.y + fadeDistance) / fadeDistance, 0.0, 1.0);
    gl_FragColor = vec4( outgoingLight, diffuseColor.a * fade * 0.6 );
    `
);

渐变算法解析:

  1. vWorldPosition.y + fadeDistance:让计算基准上移(因为倒影在地面以下)
  2. / fadeDistance:归一化到0-1范围
  3. clamp(..., 0.0, 1.0):确保值不会超出有效范围
  4. 1.0 -:反转,让接近地面的部分更透明

如何让整个场景协调?

有了基础倒影,我们还需要考虑光照和阴影,让倒影与整个场景融为一体。

阴影系统

renderer.shadowMap.enabled = true;  // 启用阴影
cube.castShadow = true;            // 物体投射阴影
ground.receiveShadow = true;       // 地面接收阴影

为什么需要阴影? 阴影为场景提供了深度和真实感,与倒影配合能创造更加convincing的视觉效果。

光照配置

const ambientLight = new THREE.AmbientLight(0x404040, 0.3);  // 环境光提供基础照明
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);  // 方向光提供主要照明和阴影

翻转后的物体可能会有显示问题,因为原本朝外的面现在朝里了。所以我们设置:

material.side = THREE.DoubleSide;

完整代码

基于以上知识点,这里是完整的可运行代码:

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
 
// 场景基础设置
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
 
// 创建渐变倒影材质
function createGradientReflectionMaterial(originalMaterial) {
  const material = originalMaterial.clone();
 
  // 添加自定义着色器
  material.onBeforeCompile = (shader) => {
    // 添加顶点变量
    shader.vertexShader =
      "varying vec3 vWorldPosition;\n" + shader.vertexShader;
    shader.vertexShader = shader.vertexShader.replace(
      "#include <begin_vertex>",
      `
            #include <begin_vertex>
            vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
            `
    );
 
    // 添加片元变量和渐变计算
    shader.fragmentShader =
      "varying vec3 vWorldPosition;\n" + shader.fragmentShader;
    shader.fragmentShader = shader.fragmentShader.replace(
      "gl_FragColor = vec4( outgoingLight, diffuseColor.a );",
      `
            // 计算渐变透明度
            float fadeDistance = 2.0;
            float fade = 1.0 - clamp((vWorldPosition.y + fadeDistance) / fadeDistance, 0.0, 1.0);
            gl_FragColor = vec4( outgoingLight, diffuseColor.a * fade * 0.6 );
            `
    );
  };
 
  material.transparent = true;
  material.side = THREE.DoubleSide; // 双面渲染确保倒影可见
  return material;
}
 
// 创建高级倒影函数
function createAdvancedReflection(originalMesh) {
  const reflectionGeometry = originalMesh.geometry.clone();
  const reflectionMaterial = createGradientReflectionMaterial(
    originalMesh.material
  );
 
  const reflectionMesh = new THREE.Mesh(reflectionGeometry, reflectionMaterial);
  reflectionMesh.scale.y = -1;
  reflectionMesh.position.copy(originalMesh.position);
 
  // 针对不同几何体的位置计算
  if (originalMesh.geometry instanceof THREE.SphereGeometry) {
    const radius = originalMesh.geometry.parameters.radius;
    reflectionMesh.position.y = -(originalMesh.position.y - radius) - radius;
  } else {
    reflectionMesh.position.y = -originalMesh.position.y;
  }
 
  return reflectionMesh;
}
 
// 创建主要对象(立方体)
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshPhongMaterial({
  color: 0xff4444,
  shininess: 100,
});
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.position.y = 1.5;  // 立方体底部距离地面1.0单位
cube.castShadow = true;
scene.add(cube);
 
// 创建倒影(使用高级渐变效果)
const reflectionCube = createAdvancedReflection(cube);
scene.add(reflectionCube);
 
// 添加反射地面
const groundGeometry = new THREE.PlaneGeometry(10, 10);
const groundMaterial = new THREE.MeshPhongMaterial({
  color: 0x222222,
  transparent: true,
  opacity: 0.9,
  shininess: 200,
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = 0;
ground.receiveShadow = true;
scene.add(ground);
 
// 照明设置
const ambientLight = new THREE.AmbientLight(0x404040, 0.3);
scene.add(ambientLight);
 
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
 
// 摄像机位置
camera.position.set(3, 3, 5);
camera.lookAt(0, 0, 0);
 
// 添加轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
 
// 动画循环
function animate() {
  requestAnimationFrame(animate);
 
  // 旋转物体和倒影保持同步
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
  reflectionCube.rotation.x += 0.01;
  reflectionCube.rotation.y += 0.01;
 
  controls.update();
  renderer.render(scene, camera);
}
 
// 窗口大小调整
window.addEventListener("resize", () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});
 
// 启动动画
animate();