阴影的使用
在 threejs 中使用阴影,要开启阴影的配置,第一步要开启WebGLRenderer 的 shadowMap 配置,接着给要生成阴影的模型设置castShadow 属性,和接收阴影的Mesh 设置 receiveShadow 属性。完成这些配置后还要注意一点,MeshBasicMaterial 是不受光影响 的,所以不能用这种材质来生成阴影。 下面我们用代码展示 相关的配置。
// 加载纹理
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 是怎么将阴影绘制出来的,在学习这些之前。我假设你已经知道如何将数据渲染到纹理中。并有学习投影纹理的原理。
根据配置的灯光,绘制阴影深度贴图
- 渲染模型用的材质这里以 MeshLambertMaterial 为例
- 灯光采用 DirectionalLight(其他 灯光只是部分参数不同,大体原理相通)
当程序执行 renderer.render(scene, camera),启动渲染的时候,threejs 会调用 projectObject 对场景的对象进行分类处理,下面是其他一部分,我们主要关注处理灯光这块,也就是 object.isLight,当片断对象是一个灯光,threejs 会加入到 WebGLRenderState 对象 的 lightArray 数组中,通过调用 pushLight 添加,如果当前灯光配置了castShadow 属性,就判断是会需要绘制阴影的灯光,还会将此灯光通过 pushShadow 添加到WebGLRenderState 的shadowArray 属性中(一个记录生成阴影的灯光的数组)
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 方法开始绘制 阴影的深度贴图数据。
/**
*
* @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 方法,最后设置相应的绘制阴影的参数进入材质的着色器。
currentRenderState.setupLights( _this.useLegacyLights );
// setupLights 函数声明
function setupLights( useLegacyLights ) {
lights.setup( lightsArray, useLegacyLights );
}
WebGlLights setup 函数声明
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 文件
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() {
.....
}
`;
由于获取阴影贴图的纹理是在片元着色器,这我们关注片元着色器就可以,然后重点关注下面这句引用
#include <shadowmap_pars_fragment>
这个就是定义深度贴图数据的文件
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 就会切换回场景相机,开始绘制模型和阴影。
- 获取程序对象,
- 编译程序
- 设置着色器参数
- 绘制模型和阴影