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. 绘制模型和阴影
相关推荐
逆旅行天涯3 分钟前
【Threejs】从零开始(六)--GUI调试开发3D效果
前端·javascript·3d
m0_7482552624 分钟前
easyExcel导出大数据量EXCEL文件,前端实现进度条或者遮罩层
前端·excel
web147862107231 小时前
C# .Net Web 路由相关配置
前端·c#·.net
m0_748247801 小时前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
飞的肖1 小时前
前端使用 Element Plus架构vue3.0实现图片拖拉拽,后等比压缩,上传到Spring Boot后端
前端·spring boot·架构
青灯文案11 小时前
前端 HTTP 请求由 Nginx 反向代理和 API 网关到后端服务的流程
前端·nginx·http
m0_748254881 小时前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl
ZJ_.1 小时前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
GIS开发特训营2 小时前
Vue零基础教程|从前端框架到GIS开发系列课程(七)响应式系统介绍
前端·vue.js·前端框架·gis开发·webgis·三维gis
Cachel wood2 小时前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架