Three.js阴影贴图

生成阴影贴图的步骤如下:

  • 从光位置视点(阴影相机)创建深度图。
  • 从相机的角度进行屏幕渲染
  • 在每个像素点,将阴影相机的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);

    ...
}
 

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/519802.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

html5如何在使用原生开发的情况下实现组件化

我们知道如何在vue/react中使用组件化开发&#xff0c;那么如果只是一个简单的界面&#xff0c;一个HTML就搞定的事情&#xff0c;你还会去新建一个vue/react项目吗&#xff1f; 在使用原生HTML开发时&#xff0c;我们也会遇到一些常见的功能、模块&#xff0c;那么如何在原生…

【APUE】网络socket编程温度采集智能存储与上报项目技术------多路复用

作者简介&#xff1a; 一个平凡而乐于分享的小比特&#xff0c;中南民族大学通信工程专业研究生在读&#xff0c;研究方向无线联邦学习 擅长领域&#xff1a;驱动开发&#xff0c;嵌入式软件开发&#xff0c;BSP开发 作者主页&#xff1a;一个平凡而乐于分享的小比特的个人主页…

无锡国家集成电路设计中心某公司的单锂小电机直流电机H桥驱动电路

H桥驱动 L9110S是一款直流电机驱动电路&#xff0c;适合单节锂电池应用。输出电流0.4A。价格约3毛。 推荐原因&#xff1a; 某些人应该知道这个地方&#xff0c;大多数人应该不知道这个地方&#xff0c;所以推荐一下。 这个地方去过几次&#xff0c;某公司与某方走的“近”&…

在同一个局域网如何共享打印机和文件

1.在连接了打印机的主机上设置 1.1启用windows共享 打开网络与共享中心&#xff0c;点击“更改高级共享设置” 选择&#xff1a; “启用网络发现”“启用文件和打印机共享”“启用共享以便可以访问网络的用户可以读取和写入公用文件夹中的文件” 打开控制面板&#xff0c;选…

C#将Console写至文件,且文件固定最大长度

参考文章 将C#的Console.Write同步到控制台和log文件输出 业务需求 在生产环境中&#xff0c;控制台窗口不便展示出来。 为了在生产环境中&#xff0c;完整记录控制台应用的输出&#xff0c;选择将其输出到文件中。 但是&#xff0c;一次性存储所有输出的话&#xff0c;文件会…

NASA数据集——加拿大西北地区(NWT)2014 年被野火烧毁的北方森林的实地数据

ABoVE: Characterization of Carbon Dynamics in Burned Forest Plots, NWT, Canada, 2014 简介 文件修订日期&#xff1a;2019-04-12 数据集版本: 1 摘要 该数据集提供了加拿大西北地区&#xff08;NWT&#xff09;2014 年被野火烧毁的北方森林的实地数据。在 2015 年的实…

(学习日记)2024.04.03:UCOSIII第三十一节:信号量函数接口讲解

写在前面&#xff1a; 由于时间的不足与学习的碎片化&#xff0c;写博客变得有些奢侈。 但是对于记录学习&#xff08;忘了以后能快速复习&#xff09;的渴望一天天变得强烈。 既然如此 不如以天为单位&#xff0c;以时间为顺序&#xff0c;仅仅将博客当做一个知识学习的目录&a…

Linux (Ubuntu)- mysql8 部署

目录 1.基本部署 2.修改密码 3.开启root可远程连接配置 1.基本部署 01》》先查看OS类型&#xff0c;如果是Ubuntu在往下边看 rootspray:/etc/mysql/mysql.conf.d# lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: …

12-1-CSS 常用样式属性

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;HTML5和CSS3悦读 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; 文章目录 CSS 常用样式属性1 CSS 三角形2 CSS 用户界面样式2.1 什么是界面样式2.2 鼠标…

【Spring】AOP——使用@around实现面向切面的方法增强

工作业务中&#xff0c;有大量分布式加锁的重复代码&#xff0c;存在两个问题&#xff0c;一是代码重复率高&#xff0c;二是容易产生霰弹式修改&#xff0c;使用注解和AOP可以实现代码复用&#xff0c;简化分布式锁加锁和解锁流程。 around注解是AspectJ框架提供的&#xff0c…

RK3568测试

作者简介&#xff1a; 一个平凡而乐于分享的小比特&#xff0c;中南民族大学通信工程专业研究生在读&#xff0c;研究方向无线联邦学习 擅长领域&#xff1a;驱动开发&#xff0c;嵌入式软件开发&#xff0c;BSP开发 作者主页&#xff1a;一个平凡而乐于分享的小比特的个人主页…

Prometheus+grafana环境搭建Docker服务(docker+二进制两种方式安装)(八)

由于所有组件写一篇幅过长&#xff0c;所以每个组件分一篇方便查看&#xff0c;前七篇链接如下 Prometheusgrafana环境搭建方法及流程两种方式(docker和源码包)(一)-CSDN博客 Prometheusgrafana环境搭建rabbitmq(docker二进制两种方式安装)(二)-CSDN博客 Prometheusgrafana环…

java数据结构与算法刷题-----LeetCode172. 阶乘后的零

java数据结构与算法刷题目录&#xff08;剑指Offer、LeetCode、ACM&#xff09;-----主目录-----持续更新(进不去说明我没写完)&#xff1a;https://blog.csdn.net/grd_java/article/details/123063846 文章目录 数学&#xff1a;阶乘的10因子个数数学优化:思路转变为求5的倍数…

【蓝桥杯选拔赛真题57】C++字符串反转 第十四届蓝桥杯青少年创意编程大赛 算法思维 C++编程选拔赛真题解

目录 C字符串反转 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 七、推荐资料 C字符串反转 第十四届蓝桥杯青少年创意编程大赛C选拔赛真题 一、题目要求 1、编程实现 给定一个只包含大写字母"M…

C++进阶--C++11(2)

C11第一篇 C11是C编程语言的一个版本&#xff0c;于2011年发布。C11引入了许多新特性&#xff0c;为C语言提供了更强大和更现代化的编程能力。 可变参数模板 在C11中&#xff0c;可变参数模板可以定义接受任意数量和类型参数的函数模板或类模板。它可以表示0到任意个数&…

蓝桥杯 第2155题质因数个数 C++ Java Python

题目 思路和解题方法 目标是计算给定数 n 的质因数个数。可以使用了试除法来找到 n 的所有质因数 读取输入的数 n。从 2 开始遍历到 sqrt(n)&#xff0c;对于每个数 i&#xff1a; 如果 n 能被 i 整除&#xff0c;则进行以下操作&#xff1a; 将 n 除以 i&#xff0c;直到 n 不…

leetcode.面试题 02.07. 链表相交

题目 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交&#xff1a; 思路 假a在链表A上移动,b在链表B上移动&#xff0c;a移动完在B上开始&…

鸿蒙Lottie动画-实现控制动画的播放、暂停、倍速播放、播放顺序

介绍 本示例展示了lottie对动画的操作功能。引入Lottie模块&#xff0c;实现控制动画的播放、暂停、倍速播放、播放顺序、播放到指定帧停止或从指定帧开始播放、侦听事件等功能&#xff0c;动画资源路径必须是json格式。 效果预览 使用说明&#xff1a; 进入页面默认开始201…

铸铁平台的平面度

铸铁平台的平面度是指平台的表面平整程度&#xff0c;即平台表面与其理论平面之间的最大偏差。平台的平面度通常使用国际标准符号GD&T中的平面度符号&#xff08;ⓨ&#xff09;表示&#xff0c;单位为毫米&#xff08;mm&#xff09;或微米&#xff08;μm&#xff09;。 …

docker搭建EFK(未完待续)

目录 elasticsearch1.创建网络2.拉取镜像3.创建容器如果出现启动失败&#xff0c;提示目录挂载失败&#xff0c;可以考虑如下措施 开放防火墙端口4.验证安装成功重置es密码关闭https连接创建kibana用户创建新账户给账户授权 kibana1.创建容器2.验证安装成功3.es为kibana创建用户…