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 clipgl_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_viewerconst 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 1bias = 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#L24bvec4 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);...
}

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

隐私计算实训营第七讲-隐语SCQL的架构详细拆解

隐私计算实训营第七讲-隐语SCQL的架构详细拆解 文章目录 隐私计算实训营第七讲-隐语SCQL的架构详细拆解1.SCQL Overview1.1 多方数据分析场景1.2 多方数据分析技术路线1.2.1 TEE SQL方案1.2.2 MPC SQL方案 1.3 Secure Collaborative Query Language(SCQL)1.3.1 SCQL 系统组件1.…

rust项目组织结构和集成测试举例

概述 在学习rust的过程中&#xff0c;当项目结构略微复杂的时候&#xff0c;写集成测试的时候发现总是不能引用项目中的代码&#xff0c;导致编写测试用例失败。查阅了教程&#xff0c;一般举例都很简单。查阅了谷歌和百度以及ai&#xff0c;也没有找到满意的答案。这里记录一…

用户体验:探讨Facebook如何优化用户体验

在数字化时代&#xff0c;用户体验是社交媒体平台成功与否的关键因素之一。作为全球最大的社交媒体平台之一&#xff0c;Facebook一直在努力优化用户体验&#xff0c;从功能设计到内容呈现再到隐私保护&#xff0c;不断提升用户满意度。本文将深入探讨Facebook如何优化用户体验…

【EasyExcel】—— 实现excel动态表头设置、多个sheet

引入jar <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.1.0</version></dependency>代码 public static void main(String[] args) {//选择存储地址String fileName "/User…

Linux基础概念

Linux Linux 和 UNIX 中的文件系统是一个以 / 为根的树状式文件结构&#xff0c;/ 是 Linux 和 UNIX 中的根目录&#xff0c;同样它也是文件系统的起点。所有的文件和目录都位于 / 路径下&#xff0c;包括经常听到的 /usr、/etc、/bin、/home 等。在早期的 UNIX 系统中&#x…

论文阅读RangeDet: In Defense of Range View for LiDAR-based 3D Object Detection

文章目录 RangeDet: In Defense of Range View for LiDAR-based 3D Object Detection问题笛卡尔坐标结构图Meta-Kernel Convolution RangeDet: In Defense of Range View for LiDAR-based 3D Object Detection 论文&#xff1a;https://arxiv.org/pdf/2103.10039.pdf 代码&…

数据结构初阶:顺序表和链表

线性表 线性表 ( linear list ) 是 n 个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使 用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串 ... 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的, 线性…

Python读取Excel根据每行信息生成一个PDF——并自定义添加文本,可用于制作准考证

文章目录 有点小bug的:最终代码(无换行):有换行最终代码无bug根据Excel自动生成PDF,目录结构如上 有点小bug的: # coding=utf-8 import pandas as pd from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import letter from reportlab.pdfbase import pdf…

linux通过进程pid查询容器docker

我遇到的问题是在docker中启动了进行&#xff0c;占用显卡&#xff0c;如下nvidis-smi查看&#xff1a; 现在要查询pid16325属于哪个容器ID&#xff0c;指令&#xff1a; ps -e -o pid,cmd,comm,cgroup | grep 16325查到如下结果&#xff0c;其中12:cpuset:/docker/ 后面的 8…

js实现websocket断线重连功能

在项目开发中我们可能经常要使用websocket技术&#xff0c;当连接发生断线后&#xff0c;如果不进行页面刷新将不能正常接收来自服务端的推送消息。为了有效避免这种问题&#xff0c;我们需要在客户端做断线重连处理。当网络或服务出现问题后&#xff0c;客户端会不断检测网络状…

玩转ChatGPT:Kimi测评(科研写作)

一、写在前面 ChatGPT作为一款领先的语言模型&#xff0c;其强大的语言理解和生成能力&#xff0c;让无数用户惊叹不已。然而&#xff0c;使用的高门槛往往让国内普通用户望而却步。 最近&#xff0c;一款由月之暗面科技有限公司开发的智能助手——Kimi&#xff0c;很火爆哦。…

Linux基础篇:VMware centos7虚拟机网络配置——桥接模式

VMware centos7虚拟机网络配置——桥接模式 1 搞清楚什么是桥接模式 桥接模式允许虚拟机直接连接到物理网络&#xff0c;就像它是物理网络中的一个独立设备一样。在这种模式下&#xff0c;虚拟机将具有与宿主机相同网络中的其他设备相同的网络访问权限。虚拟机将获得一个独立…

Navicat for MySQL 15免费注册方法

一、效果图如下&#xff1a; 注&#xff1a;此方法仅用于非商业用途&#xff0c;请勿传播&#xff0c;否则后果自负。 二、下载安装 下载安装包&#xff0c;分为32位和6位&#xff0c;下载文件名&#xff1a;Navicat for MySQL 15.zip&#xff08;https://download.csdn.net/…

Flutter 开发学习笔记(4):widget布局容器学习

文章目录 前言相关链接Widget 有状态和无状态Flutter 代码风格去掉烦人的括号后缀提示代码缩进 Flutter 布局最简单的布局widgets和Material widgets Dark语法习惯Flutter 布局默认布局Center居中Padding 填充Align对齐默认居中顶部底部右上角 通用 WidgetContainer处于性能原因…

K8S Pod 的生命周期

本文会介绍 1个 POD 从启动到被关闭删除&#xff0c; 有什么事情发生&#xff0c; 和有什么组件被参与进来 容器环境初始化阶段 apiserver 接受到创建容器的指令时&#xff0c; 在构建容器之前会有一些环境的设置阶段&#xff0c; 例如node 选择&#xff0c; image 镜像下载等…

路由和远程访问是什么?

路由和远程访问在现代互联网时代中&#xff0c;扮演着至关重要的角色。它们为我们提供了便捷的信息传递途径&#xff0c;让不同地区的电脑、设备以及人们之间能够轻松进行通信和交流。 对于路由来说&#xff0c;它是连接互联网上的各个网络的核心设备。一台路由器可以将来自不同…

【Qt】使用Qt实现Web服务器(九):EventSource+JSON实现工业界面数据刷新

1、效果 效果如下,实时刷新温度、湿度 2、源码 2.1 index.html <html><body> <!-- 页面布局,本人对HTML标签不熟悉,凑合看吧 --> <div><label for

前后端开发之——文章分类管理

原文地址&#xff1a;前后端开发之——文章分类管理 - Pleasure的博客 下面是正文内容&#xff1a; 前言 上回书说到 文章管理系统之添加文章分类。就是通过点击“新建文章分类”按钮从而在服务端数据库中增加一个文章分类。 对于文章分类这个对象&#xff0c;增删改查属于配…

PyQt ui2py 使用PowerShell将ui文件转为py文件并且将导入模块PyQt或PySide转换为qtpy模块开箱即用

前言 由于需要使用不同的qt环境&#xff08;PySide&#xff0c;PyQt&#xff09;所以写了这个脚本&#xff0c;使用找到的随便一个uic命令去转换ui文件&#xff0c;然后将导入模块换成qtpy这个通用库(支持pyside2-6&#xff0c;pyqt5-6)&#xff0c;老版本的是Qt.py(支持pysid…

Appium无线自动化实用教程

文章目录 简介核心特点工作原理使用Appium进行自动化测试的一般步骤 环境设置安装和启动Appium Server使用Node.js和npm安装Appium Server&#xff1a;启动Appium Server:命令行启动使用Appium Desktop安装和启动Appium Server&#xff1a;使用代码启动appium server 编写测试代…