相信很多使用three.js在web端进行场景渲染的朋友们一定会有和我一样的疑问。在Unity,UE4中渲染效果那么好的模型和场景放到three.js中为什么那么丑?

我在有一段时间也曾经被这个问题困扰过,虽然浏览内核利用GPU可能没有直接在系统层利用GPU的性能来的那么澎湃,但是总不至于同一个模型,渲染出来的效果天差地别吧。直到我在这几年一步一步使用three.js才逐渐明白如何有效地提升渲染效果的细节,本文将慢慢的说出一些细节的关键。

在本文中所使用的three.js版本为r144。

1 THREE.WebGLRenderer的参数设置

THREE.WebGLRenderer对象的参数设置非常影响渲染效果。

其官方文档链接如下:https://threejs.org/docs/#api/zh/renderers/WebGLRenderer

要想获得比较好的渲染效果,对THREE.WebGLRenderer可进行以下参数设置。

var renderer;
renderer = new THREE.WebGLRenderer({
      antialias:true
});
renderer.physicallyCorrectLights = true;
renderer.setPixelRatio( window.devicePixelRatio * 2);
renderer.setSize(window.innerWidth,window.innerHeight);
renderer.gammaOutput = true;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
renderer.outputEncoding = THREE.sRGBEncoding;   
document.body.appendChild(renderer.domElement);

上述设置中,

1. 抗锯齿

renderer = new THREE.WebGLRenderer({
      antialias:true,
      //alpha:true
});

antialias设置为true表示开启抗锯齿。

2. 物理灯光

renderer.physicallyCorrectLights = true;

表示将启用正确的物理灯光。

3. 像素采样率

renderer.setPixelRatio( window.devicePixelRatio * 2);

表示使用2倍设备像素点采样,这有利于消除锯齿获得更加高质量的效果。

4. 阴影贴图

renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

上述设置表示启用阴影贴图,并将阴影贴图类型设置为THREE.PCFSoftShadowMap,其中不同阴影贴图类型的设置如下:

  • THREE.BasicShadowMap:能够给出没有经过过滤的阴影映射,速度最快,但质量最差
  • THREE.PCFShadowMap:默认值,使用Percentage-Closer Filtering (PCF)算法来过滤阴影映射
  • THREE.PCFSoftShadowMap:和PCFShadowMap一样使用 Percentage-Closer Filtering (PCF) 算法过滤阴影映射,但在使用低分辨率阴影图时具有更好的软阴影
  • THREE.VSMShadowMap:使用Variance Shadow Map (VSM)算法来过滤阴影映射。当使用VSMShadowMap时,所有阴影接收者也将会投射阴影

5. 色彩映射

renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;

设置这个属性主要是为了在普通计算机显示器或者移动设备屏幕等低动态范围介质上,模拟、逼近高动态范围(HDR)效果。

这个属性如果要生效就是需要将hdr贴图作为场景的环境贴图,获取IBL的光照效果。使用hdr贴图作为场景的环境贴图也将大幅度提升场景或者模型的渲染效果。

其中toneMapping算法可选择:

  • THREE.NoToneMapping
  • THREE.LinearToneMapping
  • THREE.ReinhardToneMapping
  • THREE.CineonToneMapping
  • THREE.ACESFilmicToneMapping

这里我们使用的是效果最好的ACES算法,当然你也可以自定义自己的tonemapping算法,如何自定义可参考官方示例。

toneMappingExposure表示曝光级别,值越大曝光程度越高,场景中的光线越充足,模型就越亮。

6. 渲染器输出编码

renderer.outputEncoding = THREE.sRGBEncoding;

设置渲染器的输出编码,默认的输出编码为THREE.LinearEncoding,我们这里设置为THREE.sRGBEncoding

2 调整贴图的encoding

three.js将贴图的编码都默认设置为THREE.LinearEncoding,导致图片色彩失真(色彩不像正常那么鲜艳,会灰蒙蒙的),所以务必将场景中的所有贴图的编码都调整为THREE.sRGBEncoding

1. 贴图修改encoding

const textureLoader = new THREE.TextureLoader();
textureLoader.load( "./model_D.png", function(texture){
     texture.encoding = THREE.sRGBEncoding;
});

2. 环境贴图修改encoding

const hdrUrl = './res/hdr/ballroom_1k.hdr'
new THREE.RGBELoader().load(hdrUrl, texture => {
    const gen = new THREE.PMREMGenerator(renderer)
    envMap = gen.fromEquirectangular(texture).texture;
    envMap.encoding = THREE.sRGBEncoding;
    scene.environment = envMap;
    scene.background = envMap;

    texture.dispose();
    gen.dispose();
})

3 场景中的光照调整

3.1 固定光源

3.1.1 环境光 AmbientLight

环境光会均匀的照亮场景中的所有物体,环境光不能产生阴影,因为环境光没有方向。

const light = new THREE.AmbientLight( 0x404040 ); // soft white light
scene.add( light );

3.1.2 半球光 HemisphereLight

半球光同样也是照亮场景中的所有物体,并且光照颜色会从天空光照颜色慢慢的渐变到地面光照颜色。

const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 );
scene.add( light );

3.1.3 平行光,定向光 DirectionalLight

平行光是沿着特定方向发射的光。这种光的表现像是无限远,发出的平行光线,常常用来模拟太阳光。

平行光可以产生阴影,所以我们可以对平行光的阴影进行更多的设置。

const directional_light = new THREE.DirectionalLight( 0xffffff, 1 );
directional_light.position.set( 0, 1, 0 ); 
directional_light.castShadow = true; 
scene.add( directional_light );

directional_light.shadow.mapSize.width = 512 * 2; 
directional_light.shadow.mapSize.height = 512 * 2; 
directional_light.shadow.bias = 0.05;
directional_light.shadow.normalBias = 0.05;
directional_light.shadow.camera.near = 0.5; 
directional_light.shadow.camera.far = 500; 

3.1.4 点光源 PointLight

点光源为为一个点向各个方向发射的光源,可以理解为一个灯泡发出的光。

点光源同样也可以产生阴影,所以我们也可以对点光源的阴影进行更多设置。

const point_light = new THREE.PointLight( 0xffffff, 1, 100 );
point_light.position.set( 0, 10, 4 );
point_light.castShadow = true;
scene.add( point_light );

point_light.shadow.mapSize.width = 512 * 2; 
point_light.shadow.mapSize.height = 512 * 2; 
point_light.shadow.bias = 0.05;
point_light.shadow.normalBias = 0.05;
point_light.shadow.camera.near = 0.5; 
point_light.shadow.camera.far = 500; 

3.1.5 聚光灯 SpotLight

聚光灯光线从一个点沿一个方向射出,随着光线照射的变远,光线圆锥体的尺寸也逐渐增大。

聚光灯同样也可以产生阴影,所以我们也可以对点光源的阴影进行更多设置。

const spot_light = new THREE.SpotLight( 0xffffff );
spot_light.position.set( 100, 1000, 100 );
spot_light.castShadow = true;
scene.add( spot_light );

spot_light.shadow.mapSize.width = 512 * 2; 
spot_light.shadow.mapSize.height = 512 * 2; 
spot_light.shadow.camera.near = 500;
spot_light.shadow.camera.far = 4000;
spot_light.shadow.camera.fov = 30;

3.1.6 区域光 RectAreaLight

区域光不支持阴影,因此没有更多的阴影设置。

const width = 10;
const height = 10;
const intensity = 1;
const rect_light = new THREE.RectAreaLight( 0xffffff, intensity,  width, height );
rect_light.position.set( 5, 5, 0 );
rect_light.lookAt( 0, 0, 0 );
scene.add( rect_light )

3.2 IBL(基于图像的光照)

基于图像的光照(Image based lighting, IBL)通过一张环境贴图(Environment Map)来保存物体周围的环境信息,并通过一系列处理模拟真实环境中的光照效果。IBL与固定光源相比能够提供更加真实的光照效果,所以如果你用固定光源得到的效果很差,可以使用一张好的hdr作为场景的环境贴图,相信渲染效果会更加真实。

这里我们一般使用hdr作为我们的环境贴图,找好符合场景渲染要求的hdr环境贴图,然后设置场景的toneMapping,

renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;

然后加载hdr环境贴图,如下:

const hdrUrl = './res/hdr/ballroom_1k.hdr'
new THREE.RGBELoader().load(hdrUrl, texture => {
    const gen = new THREE.PMREMGenerator(renderer)
    envMap = gen.fromEquirectangular(texture).texture;
    envMap.encoding = THREE.sRGBEncoding;
    envMap.material = THREE.EquirectangularReflectionMapping;
    scene.environment = envMap;
    scene.background = envMap;

    texture.dispose();
    gen.dispose();
})

然后修改场景中所有的mesh的材质的环境贴图

scene.traverse(child => {
            if (child instanceof THREE.Mesh) {
                child.material.envMap = envMap;
                child.material.envMapIntensity = envMapIntensity;
                child.material.needsUpdate = true;
                child.castShadow = true;
                child.receiveShadow = true;
            }
        })

4 加载模型的设置

在加载模型时也需要对模型进行一些设置才能有好的效果,如何设置可参考以下代码

new THREE.GLTFLoader().load('./example.glb', result => {
    model = result.scene || result.scenes[0];

    model.traverse(child => {
        if ( child.isMesh ) {
            child.castShadow = true;
            child.receiveShadow = true;

            if(child.material.map)
            {
                child.material.map.encoding = THREE.sRGBEncoding;
                child.material.map.anisotropy = 1;
            }

            if (child.material.emissiveMap)
            {
                child.material.emissiveMap.encoding = THREE.sRGBEncoding;
            }

            if (child.material.map || child.material.emissiveMap)
            {
                child.material.needsUpdate = true;
            }

            if(envMap)
            {
                child.material.envMap = envMap;
                child.material.envMapIntensity = 1;
            }

        }
    });

在加载模型完成之后,需要遍历模型的所有网格,并将所有网格材质的贴图的encoding修改为THREE.sRGBEncoding,然后设置所有网格材质贴图的环境贴图以及强度。

5 三维模型

如果想要获取逼真的渲染效果,建议使用gltf格式的三维模型文件。如果你的模型不是gltf格式的可以先导入Blender,然后新增材质并添加模型的纹理,基本上在Blender中是什么渲染效果那么导入到three.js中就可以维持相同的渲染效果。

参考链接