NSDT工具推荐Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割

我过去做过阴影贴图,但随着时间的推移我忘记了它,所以我想我会留下这个作为记录。

简而言之,生成阴影贴图的步骤如下:

  • 从光位置视点(阴影相机)创建深度图。
  • 从相机的角度进行屏幕渲染
  • 在每个像素点,将阴影相机的MVP矩阵计算出的深度值与深度图值进行比较
  • 如果深度图值较低,则说明该像素点存在阴影 ,因此渲染阴影。

1、创建深度贴图

基本上,当从头开始做阴影贴图时,你只需要准备三件事:灯光位置、阴影相机和深度贴图,但由于我认为使用可以可视化深度图的ShadowMapViewer更容易理解,所以我们将准备灯光 以及在 Three.js 中以常规方式添加阴影。

1.1 定向光

首先,制作灯光。

const light = new THREE.DirectionalLight( 0xffffff, 1.0 );
// The light is directed from the light's position to the origin of the world coordinates.
light.position.set(-30, 40, 10);

scene.add(light);

DirectionalLight 有一个阴影参数,因此请附加一个从光源位置查看的阴影相机和一个从阴影相机角度写入深度值的 fbo。

1.2 阴影相机

由于光线是定向的,因此使用 OrthographicCamera 作为阴影相机来创建平行投影的深度图。

重要的是,必须设置相机范围(视锥体),以便完全包含要阴影的所有对象。
如果阴影相机范围太宽,深度图将不准确,因此最好将其设置在尽可能渲染阴影的最低范围,不要太宽或太窄,以创建漂亮的阴影。

const frustumSize = 80;

light.shadow.camera = new THREE.OrthographicCamera(
    -frustumSize / 2,
    frustumSize / 2,
    frustumSize / 2,
    -frustumSize / 2,
    1,
    80
);


// Same position as LIGHT position.
light.shadow.camera.position.copy(light.position);
light.shadow.camera.lookAt(scene.position);
scene.add(light.shadow.camera);

1.3 深度贴图

接下来,为阴影相机视点准备深度图。

低分辨率会使图像变得粗糙,因此我们这次准备2048 x 2048 fbo。

通常,使用 16 位或 32 位纹理,因为最好以尽可能高的精度写入深度值,但由于 WebGL 尚不兼容尚不支持浮动纹理的设备,因为某些设备尚不支持 支持浮点纹理,我们将使用 8 位纹理的所有四个通道来存储单个 32 位值(在本例中为深度值)。

这次我们将使用 Three.js 的 ShaderChunk 来方便转换。

light.shadow.mapSize.x = 2048;
light.shadow.mapSize.y = 2048;

const pars = { 
    minFilter: THREE.NearestFilter,
    magFilter: THREE.NearestFilter, 
    format: THREE.RGBAFormat
};

light.shadow.map = new THREE.WebGLRenderTarget( light.shadow.mapSize.x, this.light.shadow.mapSize.y, pars );

1.4 用于渲染深度图的材质

准备在深度图上写入时要使用的材质。

基本上顶点是一样的,深度值是在片段着色器中输入的。

const shadowMaterial = new THREE.ShaderMaterial({
    vertexShader: vertexShader,
    fragmentShader: shadowFragmentShader
});

顶点着色器:

void main(){
    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
}

要写入的深度贴图是8位纹理,但我们要输入的数据是32位。 我们在two.js的shaderChunk中使用packDepthToRGBA来存储使用rgba通道的深度值。

gl_FragCoord.z 包含从 0 到 1 的深度值,因此请按原样输入这些值。

片元着色器:

// https://github.com/mrdoob/three.js/blob/master/src/renderers/shaders/ShaderChunk/packing.glsl.js#L18
#include <packing>

void main(){
    // gl_FragCoord.z contains depth values from 0 to 1 in the viewing frustum range of the shadow camera.
    // 0 for near clip, 1 for far clip
    gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
}

1.5 写入深度值

将 ShadowMaterial 设置为网格并渲染为深度图。

由于需要为阴影相机视点创建深度图,因此将深度图指定为“renderTarget”,将shadowCamera指定为“camera”。

// update every frame
mesh.material = shadowMaterial;
renderer.setRenderTarget(light.shadow.map);
renderer.render(scene, light.shadow.camera);

现在我们有了深度图渲染。准备一个阴影图查看器来查看深度图的外观以进行调试。

// https://threejs.org/examples/?q=shadow#webgl_shadowmap_viewer

const depthViewer = new ShadowMapViewer(light);
depthViewer.size.set( 300, 300 );

...
// render to canvas
renderer.setRenderTarget(null);
depthViewer.render( renderer );

距离阴影相机越近,深度值越小,因此结果本质上是相反的,但它是检查深度图是否正确渲染的好工具。

2、比较深度并创建阴影

创建深度图后,就可以进行屏幕渲染了。

2.1 屏幕渲染材质

准备用于屏幕渲染的材质。

灯光位置和深度图被放入uniform变量中,阴影相机投影矩阵和视图矩阵也被放入uniform变量中,因为在阴影相机的MVP矩阵中计算的深度也必须在该着色器中计算并与 深度图。

const uniforms = {
    uColor: {
        value: new THREE.Color(color)
    },
    uLightPos: {
        value: light.position
    },
    uDepthMap: {
        value: light.shadow.map.texture
    },
    uShadowCameraP: {
        value: light.shadow.camera.projectionMatrix
    },
    uShadowCameraV: {
        value: light.shadow.camera.matrixWorldInverse
    },
}
const material = new THREE.ShaderMaterial({
    vertexShader,
    fragmentShader,
    uniforms,
});

在顶点着色器中添加一些代码。

uniform mat4 uShadowCameraP;
uniform mat4 uShadowCameraV;

varying vec4 vShadowCoord;

varying vec3 vNormal;

void main(){
    vNormal = normal;
    vec3 pos = position;

    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(pos, 1.0);
    // Coordinates from the shadow camera viewpoint
    // Pass to fragment shader and compare with depth map.
    vShadowCoord = uShadowCameraP * uShadowCameraV * modelMatrix * vec4(pos, 1.0);
}

vShadowCoord 的结果是剪辑空间中的坐标系,因此 vShadowCoord.xyz / vShadowCoord.w 的范围从 (-1, -1, -1) 到 (1,1,1)。如果你想了解更多关于 MVP 矩阵的信息,可以点击这里

vShadowCoord.z / vShadowCoord.w 将是深度值,因此让它在 0 和 1 之间转换并将其与深度图进行比较。并让 vShadowCoord.xy / vShadowCoord.w 在 (0, 0) 和 (1, 1) 之间转换为 uv 以引用深度图。

之所以使用MVP矩阵计算得到的结果作为uv,是因为我们可以参考与生成深度图的像素同一点的深度值。

由于深度图值是之前通过以 rgba 分布 32 位数据输入的,因此在引用时需要将它们恢复为原始值。

此解码使用来自 Three.js 中相同 ShaderChunk 的 unpackRGBAToDepth。片元着色器代码如下:

uniform vec3 uColor;
uniform sampler2D uDepthMap;
uniform vec3 uLightPos;

varying vec3 vNormal;
varying vec4 vShadowCoord;

// https://github.com/mrdoob/three.js/blob/master/src/renderers/shaders/ShaderChunk/packing.glsl.js#L24
#include <packing>

void main(){
    vec3 shadowCoord = vShadowCoord.xyz / vShadowCoord.w * 0.5 + 0.5;

    float depth_shadowCoord = shadowCoord.z;

    vec2 depthMapUv = shadowCoord.xy;
    float depth_depthMap = unpackRGBAToDepth(texture2D(uDepthMap, depthMapUv));

    // Compare and if the depth value is smaller than the value in the depth map, then there is an occluder and the shadow is drawn.
    float shadowFactor = step(depth_shadowCoord, depth_depthMap);

    // check the result of the shadow factor.
    gl_fragColor = vec4(vec3(shadowFactor), 1.0);
}

在循环函数中,将屏幕渲染过程放在深度图渲染之后

// in the loop function
// Writing into the depth map
mesh.material = shaderMaterial;
renderer.setRenderTarget(light.shadow.map);
renderer.render(scene, light.shadow.camera);

// put a material for screen rendering and render it to canvas.
mesh.material = material;
renderer.setRenderTarget(null);
renderer.render(scene, camera);

2.2 调整深度值比较

当显示shadowFactor(比较深度值的结果)时,会生成阴影,但会创建额外的图案。这称为阴影痘痘(shadow acne),必须通过减去考虑到这一点的偏差来比较深度值。片元着色器如下:

void main(){
    ...
    float cosTheta = dot(normalize(uLightPos), vNormal);
    float bias = 0.005 * tan(acos(cosTheta)); // cosTheta is dot( n,l ), clamped between 0 and 1
    bias = clamp(bias, 0.0, 0.01);
    
    float shadowFactor = step(depth_shadowCoord - bias, depth_depthMap);

    gl_fragColor = vec4(vec3(shadowFactor), 1.0);
}

痘痘消失了。

阴影区域不干净,但定向光过程可以将其覆盖,所以我将保持原样。影子相机视锥体的外侧被切掉了,所以我只处理那部分。片元着色器代码如下:

void main(){

    // Assume no shadow except for viewing frustum.
    // https://github.com/mrdoob/three.js/blob/master/src/renderers/shaders/ShaderChunk/packing.glsl.js#L24
    bvec4 inFrustumVec = bvec4 ( shadowCoord.x >= 0.0, shadowCoord.x <= 1.0, shadowCoord.y >= 0.0, shadowCoord.y <= 1.0 );
    bool inFrustum = all( inFrustumVec );

    bvec2 frustumTestVec = bvec2( inFrustum, shadowCoord.z <= 1.0 );
    bool frustumTest = all( frustumTestVec );

    if(frustumTest == false){
        shadowFactor = 1.0;
    }

    float difLight = max(0.0, cosTheta);

    float shading = shadowFactor * difLight;

    gl_fragColor = vec4(vec3(shading), 1.0);
}

乘以定向光然后完成着色。

应用于基于着色的 uColor。片元着色器如下:

void main(){
    color = mix(uColor - 0.1, uColor + 0.1, shading);
    gl_fragColor = vec4(color, 1.0);
}

3、额外的方法 - 剔除正面

我还将向拟展示如何无偏差地绘制阴影。

渲染深度图时,仅渲染背面网格,而在屏幕渲染期间渲染正面网格。

这种方法不需要拉偏,因为痘痘不会出现。但是,只能使用闭合网格,因此如果放置平面,则不会创建其阴影。在此演示中,此方法有效,因为只有闭合网格。

const shaderMaterial = new THREE.ShaderMaterial({
    vertexShader: vertexShader,
    fragmentShader: shadowFragmentShader,
    side: THREE.BackSide
});

片元着色器:

void main(){
    vec3 shadowCoord = (vShadowCoord.xyz / vShadowCoord.w * 0.5 + 0.5);

    float depth_shadowCoord = shadowCoord.z;
    float depth_depthMap = unpackRGBAToDepth(texture2D(uDepthMap, shadowCoord.xy));

    // Acne does not arise, so no bias is required.
    float shadowFactor = step(depth_shadowCoord, depth_depthMap);

    ...
}
 

这是将正确的对象从盒子替换为平面的比较。

对于其他封闭物体没有区别,但是对于平面来说,剔除正面的方法对于阴影不起作用。

4、结束语

我认为第一种方法更通用,哪个更好,但我认为这是关于偏差的敏感部分。似乎有很多方法可以计算偏差,opengl教程中提出的方法效果最好。

我想在不行的情况下使用正面剔除的方法会更好,那时,用薄盒子代替平面。


原文链接:Shadow Mapping In Three.js

BimAnt翻译整理,转载请标明出处