Threejs 渲染阴影

阴影的使用

在 threejs 中使用阴影,要开启阴影的配置,第一步要开启WebGLRenderer 的 shadowMap 配置,接着给要生成阴影的模型设置castShadow 属性,和接收阴影的Mesh 设置 receiveShadow 属性。完成这些配置后还要注意一点,MeshBasicMaterial 是不受光影响 的,所以不能用这种材质来生成阴影。 下面我们用代码展示 相关的配置。

ini 复制代码
// 加载纹理
  const autumnTexture = new TextureLoader().load(autumn)
  // 渲染器
  const renderer = new WebGL1Renderer({ canvas })
  renderer.outputColorSpace  = LinearSRGBColorSpace;
  renderer.shadowMap.enabled = true;
  // 透视投影相机
  const camera = new PerspectiveCamera(60, canvas.width / canvas.height, 1, 2000)
  camera.position.set(
    settings.cameraX,
    settings.cameraY,
    7
  );
  camera.lookAt(0, 0, 0)
  camera.updateMatrixWorld();
  // 场景
  const scene = new Scene()
  // 光源
  const color = 0xffffff
  const intensity = 1
  const light = new DirectionalLight(color, intensity)
  light.position.set(-1, 2, 4)
  light.castShadow = true;
  const ambient = new AmbientLight(0xffffff, 0.2);
  scene.add(ambient)
  scene.add(light)
  // 球
  const sphereGeometry = new SphereGeometry(1, 64, 32)

  const planeMat = new MeshLambertMaterial();

  const plane = new Mesh(new PlaneGeometry(20, 20), planeMat);
  plane.receiveShadow = true;
  plane.rotation.x = -Math.PI * 0.5;

  scene.add(plane);
  // 材质
  const material = new MeshLambertMaterial({
    map: autumnTexture
  })
  const mesh = new Mesh(sphereGeometry, material);
  mesh.castShadow = true;
  mesh.position.set(2, 3, 4);
  scene.add(mesh);
    // 渲染
  function render(time: number) {

    mesh.rotation.y += 0.02
    renderer.render(scene, camera)
    requestAnimationFrame(render)
  }
  requestAnimationFrame(render);

上面代码运行后会得到如下图所示的结果。

说了具体的使用,下面我们就来一步步解释下,threejs 是怎么将阴影绘制出来的,在学习这些之前。我假设你已经知道如何将数据渲染到纹理中。并有学习投影纹理的原理。

根据配置的灯光,绘制阴影深度贴图

  1. 渲染模型用的材质这里以 MeshLambertMaterial 为例
  2. 灯光采用 DirectionalLight(其他 灯光只是部分参数不同,大体原理相通)

当程序执行 renderer.render(scene, camera),启动渲染的时候,threejs 会调用 projectObject 对场景的对象进行分类处理,下面是其他一部分,我们主要关注处理灯光这块,也就是 object.isLight,当片断对象是一个灯光,threejs 会加入到 WebGLRenderState 对象 的 lightArray 数组中,通过调用 pushLight 添加,如果当前灯光配置了castShadow 属性,就判断是会需要绘制阴影的灯光,还会将此灯光通过 pushShadow 添加到WebGLRenderState 的shadowArray 属性中(一个记录生成阴影的灯光的数组)

ini 复制代码
function projectObject( object, camera, groupOrder, sortObjects ) {

    if ( object.visible === false ) return;

    const visible = object.layers.test( camera.layers );

    if ( visible ) {

        if ( object.isGroup ) {

            groupOrder = object.renderOrder;

	} else if ( object.isLOD ) {

            if ( object.autoUpdate === true ) object.update( camera );

	} else if ( object.isLight ) {

            currentRenderState.pushLight( object );

            if ( object.castShadow ) {

                    currentRenderState.pushShadow( object );

            }

        }....
     }
}

当 projectObject 函数执行完毕,整个场景数据的前期准备已经 结束,首先,threejs 会根据上一步获取到的shadowArray 数据,调用 WebGLShadowMap 的render 方法开始绘制 阴影的深度贴图数据。

ini 复制代码
/**
*
* @param {*} lights 所有的会产生阴影的灯光
* @param {*} scene // 场景
* @param {*} camera 当前场景的相机
* @returns
*/
this.render = function ( lights, scene, camera ) {
    if ( scope.enabled === false ) return;
    if ( scope.autoUpdate === false && scope.needsUpdate === false ) return;

    if ( lights.length === 0 ) return;
    // 记录之前的 Framebuffer 对象,在渲染完深度
    const currentRenderTarget = _renderer.getRenderTarget();
    const activeCubeFace = _renderer.getActiveCubeFace();
    const activeMipmapLevel = _renderer.getActiveMipmapLevel();

    const _state = _renderer.state;

    // Set GL state for depth map.
    _state.setBlending( NoBlending );
    _state.buffers.color.setClear( 1, 1, 1, 1 );
    _state.buffers.depth.setTest( true );
    _state.setScissorTest( false );

    // check for shadow map type changes

    const toVSM = ( _previousType !== VSMShadowMap && this.type === VSMShadowMap );
    const fromVSM = ( _previousType === VSMShadowMap && this.type !== VSMShadowMap );

    // render depth map
    for ( let i = 0, il = lights.length; i < il; i ++ ) {

        const light = lights[ i ];
        // 在lights 目录下记录了阴影纹理生成的参数
        const shadow = light.shadow;

        if ( shadow === undefined ) {

            console.warn( 'THREE.WebGLShadowMap:', light, 'has no shadow.' );
            continue;
        }

        if ( shadow.autoUpdate === false && shadow.needsUpdate === false ) continue;

        _shadowMapSize.copy( shadow.mapSize );

        const shadowFrameExtents = shadow.getFrameExtents(); // Vector2

        _shadowMapSize.multiply( shadowFrameExtents );

        _viewportSize.copy( shadow.mapSize );

        // 调整尺寸
        if ( _shadowMapSize.x > _maxTextureSize || _shadowMapSize.y > _maxTextureSize ) {

            if ( _shadowMapSize.x > _maxTextureSize ) {

                _viewportSize.x = Math.floor( _maxTextureSize / shadowFrameExtents.x );
                _shadowMapSize.x = _viewportSize.x * shadowFrameExtents.x;
                shadow.mapSize.x = _viewportSize.x;

            }

            if ( _shadowMapSize.y > _maxTextureSize ) {

                _viewportSize.y = Math.floor( _maxTextureSize / shadowFrameExtents.y );
                _shadowMapSize.y = _viewportSize.y * shadowFrameExtents.y;
                shadow.mapSize.y = _viewportSize.y;

            }

        }

        if ( shadow.map === null || toVSM === true || fromVSM === true ) {

            const pars = ( this.type !== VSMShadowMap )
                ? { minFilter: NearestFilter, magFilter: NearestFilter }
                : {};

            if ( shadow.map !== null ) {

                shadow.map.dispose();

            }

            shadow.map = new WebGLRenderTarget( _shadowMapSize.x, _shadowMapSize.y, pars );
				
            shadow.map.texture.name = light.name + '.shadowMap';

            shadow.camera.updateProjectionMatrix();

        }


        // 设置渲染缓冲区,和深度纹理等(如果支持了 Ext WEBGL_depth_texture)
        _renderer.setRenderTarget( shadow.map );
        _renderer.clear();

        const viewportCount = shadow.getViewportCount();

        for ( let vp = 0; vp < viewportCount; vp ++ ) {

                const viewport = shadow.getViewport( vp );

                _viewport.set(
                        _viewportSize.x * viewport.x,
                        _viewportSize.y * viewport.y,
                        _viewportSize.x * viewport.z,
                        _viewportSize.y * viewport.w
                );

                _state.viewport( _viewport );

                shadow.updateMatrices( light, vp );

                _frustum = shadow.getFrustum();

                // 生成在此灯光下的模型深度数据
                renderObject( scene, camera, shadow.camera, light, this.type );

        }

        // do blur pass for VSM

        if ( shadow.isPointLightShadow !== true && this.type === VSMShadowMap ) {

            VSMPass( shadow, camera );

        }

        shadow.needsUpdate = false;

    }

    _previousType = this.type;

    scope.needsUpdate = false;

    _renderer.setRenderTarget( currentRenderTarget, activeCubeFace, activeMipmapLevel );

};

当完成了模型深度贴图绘制后,在render 中会通过 下面代码将深度贴图传入材质中,是通过调用 WebGLRenderState 实例方法 setupLights, 在此方法内会去调用 WebGLLights 下的setup 方法,最后设置相应的绘制阴影的参数进入材质的着色器。

scss 复制代码
    currentRenderState.setupLights( _this.useLegacyLights );
    
    // setupLights 函数声明
    function setupLights( useLegacyLights ) {

        lights.setup( lightsArray, useLegacyLights );

    }

WebGlLights setup 函数声明

ini 复制代码
function setup( lights, useLegacyLights ) {

    let r = 0, g = 0, b = 0;

    for ( let i = 0; i < 9; i ++ ) state.probe[ i ].set( 0, 0, 0 );

    let directionalLength = 0;
    let pointLength = 0;
    let spotLength = 0;
    let rectAreaLength = 0;
    let hemiLength = 0;

    let numDirectionalShadows = 0;
    let numPointShadows = 0;
    let numSpotShadows = 0;
    let numSpotMaps = 0;
    let numSpotShadowsWithMaps = 0;

    // ordering : [shadow casting + map texturing, map texturing, shadow casting, none ]
    lights.sort( shadowCastingAndTexturingLightsFirst );

    // artist-friendly light intensity scaling factor
    const scaleFactor = ( useLegacyLights === true ) ? Math.PI : 1;

    for ( let i = 0, l = lights.length; i < l; i ++ ) {

        const light = lights[ i ];

        const color = light.color;
        const intensity = light.intensity;
        const distance = light.distance;

        // 获取到渲染好的深度纹理
        const shadowMap = ( light.shadow && light.shadow.map )
        ? light.shadow.map.texture
        : null;

        if ( light.isAmbientLight ) {

            r += color.r * intensity * scaleFactor;
            g += color.g * intensity * scaleFactor;
            b += color.b * intensity * scaleFactor;

        } else if ( light.isLightProbe ) {

            for ( let j = 0; j < 9; j ++ ) {

                state.probe[ j ].addScaledVector( light.sh.coefficients[ j ], intensity );

            }

        } else if ( light.isDirectionalLight ) {

            const uniforms = cache.get( light );
            uniforms.color.copy( light.color ).multiplyScalar( light.intensity * scaleFactor );
            // 能生成阴影
            if ( light.castShadow ) {
                const shadow = light.shadow;
                const shadowUniforms = shadowCache.get( light );
                shadowUniforms.shadowBias = shadow.bias;
                shadowUniforms.shadowNormalBias = shadow.normalBias;
                shadowUniforms.shadowRadius = shadow.radius;
                shadowUniforms.shadowMapSize = shadow.mapSize;
                state.directionalShadow[ directionalLength ] = shadowUniforms;
                state.directionalShadowMap[ directionalLength ] = shadowMap;
                state.directionalShadowMatrix[ directionalLength ] = light.shadow.matrix;
                numDirectionalShadows ++;
            }
            
            state.directional[ directionalLength ] = uniforms;
            directionalLength ++;

        }.....

    }


}

由于示例中,我们使用的是 DirectionalLight, 其他灯光的参数配置我们就省略, 只是看这些参数可能无法理解是如何传入着色器的,我们示例中用的是MeshLamberMaterial,所以我们就从这个材质的着色器入手。 打开 src/renderer/shaders/shaderLib/meshlambert.glsl 文件

arduino 复制代码
export const fragment = /* glsl */`
#define LAMBERT

uniform vec3 diffuse;
uniform vec3 emissive;
uniform float opacity;

#include <common>
#include <packing>
#include <dithering_pars_fragment>
#include <color_pars_fragment>
#include <uv_pars_fragment>
#include <map_pars_fragment>
#include <alphamap_pars_fragment>
#include <alphatest_pars_fragment>
#include <aomap_pars_fragment>
#include <lightmap_pars_fragment>
#include <emissivemap_pars_fragment>
#include <envmap_common_pars_fragment>
#include <envmap_pars_fragment>
#include <fog_pars_fragment>
#include <bsdfs>
#include <lights_pars_begin>
#include <normal_pars_fragment>
#include <lights_lambert_pars_fragment>
#include <shadowmap_pars_fragment>
#include <bumpmap_pars_fragment>
#include <normalmap_pars_fragment>
#include <specularmap_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>

void main() {

.....

}
`;

由于获取阴影贴图的纹理是在片元着色器,这我们关注片元着色器就可以,然后重点关注下面这句引用

arduino 复制代码
#include <shadowmap_pars_fragment>

这个就是定义深度贴图数据的文件

scss 复制代码
export default /* glsl */`
#if NUM_SPOT_LIGHT_COORDS > 0
    varying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ];
#endif
#if NUM_SPOT_LIGHT_MAPS > 0
    uniform sampler2D spotLightMap[ NUM_SPOT_LIGHT_MAPS ];
#endif

// 开启了阴影 renderer.shadowMap.enabled && shadows.length > 0,
// shadows.length 产生阴影的光源数量
#ifdef USE_SHADOWMAP
    #if NUM_DIR_LIGHT_SHADOWS > 0

    uniform sampler2D directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ];
    varying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];

    struct DirectionalLightShadow {
            float shadowBias;
            float shadowNormalBias;
            float shadowRadius;
            vec2 shadowMapSize;
    };

    uniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ];

    #endif



    /* #if NUM_RECT_AREA_LIGHTS > 0
     * TODO (abelnation): create uniforms for area light shadows
     * #endif
    */

    float texture2DCompare( sampler2D depths, vec2 uv, float compare ) {

            return step( compare, unpackRGBAToDepth( texture2D( depths, uv ) ) );

    }

    vec2 texture2DDistribution( sampler2D shadow, vec2 uv ) {

        return unpackRGBATo2Half( texture2D( shadow, uv ) );

    }

float VSMShadow (sampler2D shadow, vec2 uv, float compare ){

    float occlusion = 1.0;

    vec2 distribution = texture2DDistribution( shadow, uv );

    float hard_shadow = step( compare , distribution.x ); // Hard Shadow

    if (hard_shadow != 1.0 ) {

    float distance = compare - distribution.x ;
    float variance = max( 0.00000, distribution.y * distribution.y );
    float softness_probability = variance / (variance + distance * distance );
    // Chebeyshevs inequality
    softness_probability = clamp( ( softness_probability - 0.3 ) / ( 0.95 - 0.3 ), 0.0, 1.0 );
    // 0.3 reduces light bleed
    occlusion = clamp( max( hard_shadow, softness_probability ), 0.0, 1.0 );

    }
    return occlusion;

}

float getShadow(
    sampler2D shadowMap,
    vec2 shadowMapSize,
    float shadowBias,
    float shadowRadius,
    vec4 shadowCoord
) {

float shadow = 1.0;

shadowCoord.xyz /= shadowCoord.w;
shadowCoord.z += shadowBias;

bool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0;
bool frustumTest = inFrustum && shadowCoord.z <= 1.0;

if ( frustumTest ) {

    #if defined( SHADOWMAP_TYPE_PCF )

    vec2 texelSize = vec2( 1.0 ) / shadowMapSize;

    float dx0 = - texelSize.x * shadowRadius;
    float dy0 = - texelSize.y * shadowRadius;
    float dx1 = + texelSize.x * shadowRadius;
    float dy1 = + texelSize.y * shadowRadius;
    float dx2 = dx0 / 2.0;
    float dy2 = dy0 / 2.0;
    float dx3 = dx1 / 2.0;
    float dy3 = dy1 / 2.0;

    shadow = (
        texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy0 ), shadowCoord.z ) +
        texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy0 ), shadowCoord.z ) +
        texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy0 ), shadowCoord.z ) +
        texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy2 ), shadowCoord.z ) +
        texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy2 ), shadowCoord.z ) +
        texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy2 ), shadowCoord.z ) +
        texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, 0.0 ), shadowCoord.z ) +
        texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, 0.0 ), shadowCoord.z ) +
        texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ) +
        texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, 0.0 ), shadowCoord.z ) +
        texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, 0.0 ), shadowCoord.z ) +
        texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy3 ), shadowCoord.z ) +
        texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy3 ), shadowCoord.z ) +
        texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy3 ), shadowCoord.z ) +
        texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy1 ), shadowCoord.z ) +
        texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy1 ), shadowCoord.z ) +
        texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy1 ), shadowCoord.z )
    ) * ( 1.0 / 17.0 );

    #elif defined( SHADOWMAP_TYPE_PCF_SOFT )
    vec2 texelSize = vec2( 1.0 ) / shadowMapSize;
    float dx = texelSize.x;
    float dy = texelSize.y;
    vec2 uv = shadowCoord.xy;
    vec2 f = fract( uv * shadowMapSize + 0.5 );
    uv -= f * texelSize;

    shadow = (
        texture2DCompare( shadowMap, uv, shadowCoord.z ) +
        texture2DCompare( shadowMap, uv + vec2( dx, 0.0 ), shadowCoord.z ) +
        texture2DCompare( shadowMap, uv + vec2( 0.0, dy ), shadowCoord.z ) +
        texture2DCompare( shadowMap, uv + texelSize, shadowCoord.z ) +
        mix( texture2DCompare( shadowMap, uv + vec2( -dx, 0.0 ), shadowCoord.z ),
            texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 0.0 ), shadowCoord.z ), f.x ) +
        mix( texture2DCompare( shadowMap, uv + vec2( -dx, dy ), shadowCoord.z ),
             texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, dy ), shadowCoord.z ), f.x ) +
        mix( texture2DCompare( shadowMap, uv + vec2( 0.0, -dy ), shadowCoord.z ),
         texture2DCompare( shadowMap, uv + vec2( 0.0, 2.0 * dy ), shadowCoord.z ), f.y ) +
        mix( texture2DCompare( shadowMap, uv + vec2( dx, -dy ), shadowCoord.z ),
         texture2DCompare( shadowMap, uv + vec2( dx, 2.0 * dy ), shadowCoord.z ), f.y ) +
        mix( mix( texture2DCompare( shadowMap, uv + vec2( -dx, -dy ), shadowCoord.z ),
          texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, -dy ), shadowCoord.z ), f.x ),
         mix( texture2DCompare( shadowMap, uv + vec2( -dx, 2.0 * dy ), shadowCoord.z ),
      texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 2.0 * dy ), shadowCoord.z ),f.x),f.y )
   ) * ( 1.0 / 9.0 );

    #elif defined( SHADOWMAP_TYPE_VSM )

            shadow = VSMShadow( shadowMap, shadowCoord.xy, shadowCoord.z );

    #else // no percentage-closer filtering:

            shadow = texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z );

    #endif

    }

    return shadow;

}



#endif
`;

最后调用 getShadow 方法计算阴影数据,实现阴影的渲染

根据阴影贴图渲染模型,并绘制阴影

通过这些配置后, threejs 就会切换回场景相机,开始绘制模型和阴影。

  1. 获取程序对象,
  2. 编译程序
  3. 设置着色器参数
  4. 绘制模型和阴影
相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax