用three.js做一个3D汉诺塔游戏(下)

时间:2024-04-10 10:05:21

为桌台添加材质纹理

为物体添加适当的材质纹理,可以使其视觉效果产生质的飞跃。接下来,我们将为桌台添加一种木质纹理,用到的纹理贴图来自Pixabay.com

我们使用 TextureLoader 来加载纹理贴图,其 load 方法第1个参数为贴图的 URL 字符串,该方法返回一个纹理对象,可直接赋值给材质对象的颜色贴图属性 map。代码实现如下:

class Table {
  constructor({ width, height, depth }) {
    const geometry = new THREE.BoxGeometry(width, height, depth);
    // 纹理贴图
    const url = 'https://cdn.pixabay.com/photo/2016/12/26/13/47/fresno-1932211_1280.jpg';
    const material = new THREE.MeshLambertMaterial({ 
      color: '#cccca6',
      map: new THREE.TextureLoader().load(url)  // 纹理贴图
    });
    
    return new THREE.Mesh(geometry, material);
  }
}

然而,我们发现这样做并不完美:由于纹理贴图存在网络加载延时,所以在贴图加载完成前,桌台始终是黑色的,只有贴图加载完成后,桌台才一瞬间有了外观。如下图所示:

对此,我们需要在技术上做出一些改进,来解决纹理贴图加载前后变化的突兀感。这里有2种改进方案:

  1. 预加载,等纹理贴图加载完成后,再生成带纹理效果的桌台;
  2. 渐进式加载,桌台先显示默认颜色,等纹理贴图加载完成后,再附加纹理效果。

这里我们选择方案2,因为方案2不会阻塞桌台的渲染,有着更好的用户体验。渐进式加载的原理就是在贴图加载完成后,标记材质对象的 needsUpdate 属性为 true,这样渲染器会在下一个渲染循环动态更新材质的纹理。核心代码如下:

const material = new THREE.MeshLambertMaterial({ color: '#cccca6' });

// 动态更新材质纹理
new THREE.TextureLoader().load(url, (texture) => {
  material.needsUpdate = true;
  material.map = texture;
});

加载效果如下图所示:

优化光照效果

在 three.js 中,反光材质的物体表面会因为光照的不同而呈现出不同的明暗效果,其中光源的强弱、照射面和光线夹角等参数都会对物体的渲染效果产生影响。目前我们的场景效果并不理想:柱杆看上去灰蒙蒙的,盘子则是透出一股廉价的塑料味,都缺乏真实感。正所谓,效果不够,光照来凑,我们来调整光源参数,优化光照效果,让场景更加自然、真实。

让我们先对 Lights 类进行改造,新增一个距离参数,作为调整光源位置的基准值。我们将桌台长度、柱杆高度和桌台宽度分别作为光源在 x、y、z 方向上的位置基准值进行传递,以便于更加精确地设置光源位置,达到更好的照明效果。

class Lights {
  constructor({ directionX, directionY, directionZ }) {...}
}

const presenter = {
	init() {
    ...
    const lights = new Lights({
      directionX: model.tableSize.width,
      directionY: model.pillarSize.height,
      directionZ: model.tableSize.depth
    });
  }
  ...
};

接下来,我们需要对之前已有的平行光源位置进行调整。为了更直观地调试光照效果,我们可以添加 DirectionalLightHelper 来帮助我们更好地观察光源位置平面和光照方向。

class Lights {
  constructor({ directionX, directionY, directionZ }) {
    const ambientLight = new THREE.AmbientLight('#fff', 1);  // 环境光

    const directLight = new THREE.DirectionalLight('#fff', 3);  // 平行光
    directLight.position.set(-directionX / 3, directionY * 4, directionZ * 1.5);

    const directLightHelper = new THREE.DirectionalLightHelper(directLight, 1, '#f00');

    return [ambientLight, directLight, directLightHelper];
  }
}

经过这一步平行光源的位置调整,我们看到柱杆和盘子已经变得光滑透亮。(下图中的红线为平行光源辅助观察线)

最后我们再添加一个米黄色的聚光灯光源,中和下场景的“高冷”基调。

class Lights {
  constructor(...) {
    ...
    const spotLight = new THREE.SpotLight('#fdf4d5');
    spotLight.position.set(5, directionY * 4, 0);
    spotLight.angle = Math.PI / 2;  // 光线照射范围角度
    spotLight.power = 2000;  // 光源功率(流明)
    const spotLightHelper = new THREE.SpotLightHelper(spotLight, '#00f');

    return [ambientLight, directLight, directLightHelper, spotLight, spotLightHelper];
  }
}

完成后的效果如下图所示:(下图中的蓝线为聚光灯光源辅助观察线)

开启阴影效果

伴随着光源一起的自然是阴影,开启阴影能显著增强物体的立体效果。在现实世界中,阴影的产生需要光源、被照射物体和显示阴影的地方,这三者缺一不可。

在 three.js 中,出于性能考虑,实时渲染的阴影默认是关闭的,如果想要实现阴影效果,需要进行一番设置。与现实世界阴影的生成相似,这些设置都与光源、被照射物和阴影显示物有关,下面我们来逐一进行设置。

  1. 渲染器 开启阴影渲染支持

    const rendererView = {
      init(...) {
        ...
        this.renderer.shadowMap.enabled = true;
        this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
      },
      ...
    };
    

    THREE.PCFSoftShadowMap 是 Three.js 中的一种阴影映射技术,它使用了 Percentage Closer Filtering (PCF) 和 Soft Shadows 技术来实现更加真实的阴影效果。PCF 技术可以减少阴影的锯齿感,Soft Shadows 技术可以让阴影边缘更加柔和。

  2. 光源 开启阴影投射,使被照射物体产生阴影

    directLight.castShadow = true;  // 为平行光源开启阴影投射
    
  3. 调整光源的 阴影相机 参数,控制阴影的渲染范围到合适大小

    超出阴影相机范围的阴影不会被渲染,所以要将阴影相机的范围扩大到能完整包含柱杆和盘子。使用 CameraHelper 辅助对象可以帮助我们更好的观测阴影相机的视野范围。

    directLight.shadow.camera.left = -directionX;
    directLight.shadow.camera.right = directionX;
    directLight.shadow.camera.top = directionZ;
    directLight.shadow.camera.bottom = -directionZ;
    
    const shadowCamera = new THREE.CameraHelper(directLight.shadow.camera);
    
  4. 允许 被照射物体 产生阴影

    这里设置允许柱杆和盘子产生阴影,并且允许其他物体产生的阴影可以投射到它们表面(接收阴影)。需要注意,castShadow 和 receiveShadow 要设置到 Mesh 对象上,不能设置到 Group 上。

    /* 柱杆 */
    class Pillar {
      constructor(...) {
        ...
        const body = new THREE.Mesh(geometry, material);
        body.castShadow = true;  // 允许产生阴影
        body.receiveShadow = true;  // 允许接收阴影
        ...
      },
      ...
    }
    
    /* 盘子阴影设置同上 */
    clas