Direct9学习之--------------------------实时阴影的另一种实现ShadowMap

本文主要是介绍Direct9学习之--------------------------实时阴影的另一种实现ShadowMap,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

ShadowMap

1、原理

阴影实时渲染是计算机图形学的高级技术。它能提高场景的真实感。两种通用的阴影渲染技术分别是地图阴影(shadow map)和体积阴影(shadow volumes)。地图阴影的优势在于效率很高,因为地图阴影只需要渲染场景两次(一般来说)。并且不需要进行几何处理和产生额外的mesh。无论多复杂的场景,使用地图阴影总能保持很好的性能。

地图阴影的概念很直观。首先,从光线的方向把整个场景渲染到一张纹理上。通过特定的顶点和像素渲染器渲染得到的这张纹理就被叫做地图阴影(shadow map)。这张纹理的特点是其上每一个像素记录的不是该像素的颜色,而是像素的深度。记某个像素点的深度为Di。

接下来再一次渲染场景,本次渲染过程中,计算每一个与地图阴影对应的每个像素点到光源的距离,记作Dj。然后把Di与Dj进行比较,如果两者匹配,该像素不在阴影中。如果Di小于Dj,该像素在阴影中。

最后,根据比较结果,像素渲染器更新每一个像素的颜色。渲染地图阴影最适合的灯光是聚光灯(spotlight),因为地图阴影通过投影场景到其上面而被渲染。

程序初始化阶段,范例创建一张D3DFMT_R32F格式的纹理来承载shadowmap,选择这种格式的纹理是因为地图阴影只需要一个通道(记录深度),32位能够提供足够的精度。渲染由两个部分组成:

1、创建阴影地图。

2、通过阴影地图渲染场景。

2、创建地图阴影(ShadowMap)

地图阴影首先将用于承载地图阴影的纹理作为渲染目标。

LPDIRECT3DSURFACE9 pOldRT = NULL;

    V( pd3dDevice->GetRenderTarget( 0, &pOldRT ) );

    LPDIRECT3DSURFACE9 pShadowSurf;

    if( SUCCEEDED( g_pShadowMap->GetSurfaceLevel( 0, &pShadowSurf ) ) )

    {

        pd3dDevice->SetRenderTarget( 0, pShadowSurf );

        SAFE_RELEASE( pShadowSurf );

    }

创建地图阴影的两个渲染器分别是VertShader和PixShader。VertShader将输入的坐标转化到灯光的投影空间:

void VertShadow( float4 Pos : POSITION, float3 Normal : NORMAL,

                 out float4 oPos : POSITION, out float2 Depth : TEXCOORD0 )

{

    oPos = mul( Pos, g_mWorldView );

    oPos = mul( oPos, g_mProj );

}

g_mProj就是灯光投影空间,它的定义是:

D3DXMatrixPerspectiveFovLH( &g_mShadowProj, g_fLightFov, 1, 0.01f, 100.0f);

转化到这个投影空间就意味着摄像机从位于灯光的位置,且朝着灯光的方向望去。然后,VertexShader又传递纹理坐标投影的zw到像素渲染器:

Depth.xy = oPos.zw;

此时,像素渲染器每个像素都有了独一无二的z和w值。像素渲染器输出z/w值到渲染目标。

void PixShadow( float2 Depth : TEXCOORD0, out float4 Color : COLOR )

{

    Color = Depth.x / Depth.y;

}

这个值代表了场景中每个像素的深度,它的取值范围在0~1之间。在近剪裁面处,它的值是0,在远剪裁面处,他的值是1。渲染完成后,阴影地图就包含了每个像素的深度值。

3、渲染场景

带阴影的场景通过VertScene和PixScene两个渲染器渲染得到。VertScene将场景中的顶点转化到到投影坐标系,传递纹理坐标到像素渲染器。另外,它还输出:各顶点和法线在视(view space)内的坐标vPosvNormal。再另外,它还输出:各顶点在灯光投影空间内的坐标vPosLight

void VertScene( float4 iPos : POSITION,

float3 iNormal : NORMAL,

                float2 iTex : TEXCOORD0,

                out float4 oPos : POSITION,

                out float2 Tex : TEXCOORD0,

                out float4 vPos : TEXCOORD1,

                out float3 vNormal : TEXCOORD2,

                out float4 vPosLight : TEXCOORD3 )

{

  

    vPos = mul( iPos, g_mWorldView );

    oPos = mul( vPos, g_mProj );

    vNormal = mul( iNormal, (float3x3)g_mWorldView );

    Tex = iTex;

    vPosLight = mul( vPos, g_mViewToLightProj );

}

此时的g_mProj是g_VCamera.GetProjMatrix()。也就是说是观察者摄像机的投影方向(注意区别于1中g_mProj)。其中,vPosvNormal是在后续程序中用于光线计算。vPosLight是shadowmap的纹理坐标。vPosLight这个坐标是如何获得的呢?如果观察者相机位于灯光处且观察方向与灯光方向相同,通过将世界坐标变换到灯光视坐标,再进行灯光投影变换得到!

const D3DXMATRIX *pmView =

g_bCameraPerspective ? g_VCamera.GetViewMatrix() : &mLightView;

......

D3DXMATRIXA16 mViewToLightProj;

mViewToLightProj = *pmView;

D3DXMatrixInverse( &mViewToLightProj, NULL, &mViewToLightProj );

D3DXMatrixMultiply( &mViewToLightProj, &mViewToLightProj, &mLightView );

D3DXMatrixMultiply( &mViewToLightProj, &mViewToLightProj, &g_mShadowProj );

V( g_pEffect->SetMatrix( "g_mViewToLightProj", &mViewToLightProj ) );

像素着色器测试每个像素是否在阴影内。首先,像素着色其检测该点是否位于光圈中(聚光灯形成的那个亮的区域)。

if( dot( vLight, g_vLightDir ) > g_fCosTheta )

    其中,g_vLightDir是灯光的方向,vLight是某像素到灯光的方向。

float3 vLight = normalize( float3( vPos - g_vLightPos ) );

如果位于光圈内,检查它是否位于阴影内。这是通过把vPosLight(灯光位置在灯光投影空间内的坐标)转化到0到1的范围内,

float2 ShadowTexC = 0.5 * vPosLight.xy / vPosLight.w + float2( 0.5, 0.5 );

    目的是为了将它与shadowmap的贴图匹配。

由于在贴图空间内,坐标轴y是向下的,与投影坐标系内正好相反,所以要把投影坐标系里的y值反向。

ShadowTexC.y = 1.0f - ShadowTexC.y;

接下来,通过该坐标对shadowmap进行检查。

首先获得shadowmap上某点的颜色值,此时的颜色值代表的是深度(前面处理过的)。

tex2D(g_samShadow, ShadowTexC )

然后把它和当前渲染的图片对应shadowmap上的“某点”的像素值与灯泡的距离

vPosLight.z / vPosLight.w

进行比较,如果shadowmap的深度大,则该点没有在阴影中。

没在阴影中的话,它的‘源值’(sourcevals = source values),意思应该就是说,光就强。设其源值为1.0.否则为0,表示光线弱.

float sourcevals;

sourcevals = ((tex2D( g_samShadow, ShadowTexC ) + SHADOW_EPSILON) < (vPosLight.z / vPosLight.w))? 0.0f: 1.0f; 

最后,由这个‘源值’来控制该点的Diffuss。从而渲染出带阴影的场景。

Diffuse = (sourcevals * ( 1 - g_vLightAmbient ) + g_vLightAmbient ) * g_vMaterial;

其中SDK的例子中还使用了2×2邻近点过滤的办法什么意思呢?就是说shadowmap上的像素点和当前渲染的图片的像素点不是一一对应的。它是从shadowmap上取4个点

tex2D( g_samShadow, ShadowTexC )

tex2D( g_samShadow, ShadowTexC + float2(1.0/SMAP_SIZE, 0) )

tex2D( g_samShadow, ShadowTexC + float2(0, 1.0/SMAP_SIZE) )

tex2D( g_samShadow, ShadowTexC + float2(1.0/SMAP_SIZE, 1.0/SMAP_SIZE) )

仔细一看就知道,这四点分别是对应点,对应点左边,下边和左下边的点。比较完以后就存入

float sourcevals[4];

中。然后用‘邻近点过滤’的办法来综合处理这四个点来得到一个‘光总量’:

float LightAmount

然后再用‘光总量’来控制Diffuss,从而渲染出带阴影的场景。光总量的获得也很简单,lerp是个HLSL的函数,原型为ret lerp(x,y,s),含义是:ret = x + s(y - x)。leprs是是texelpos的小数部分, SMAP_SIZE * ShadowTexC的结果带有小数部分,小数部分表示shadowmap和场景渲染对应点之间错开的一小段距离。此时邻近点过滤的方法认为,该点的对错开的距离与权重成反比。

float2 texelpos = SMAP_SIZE * ShadowTexC;

float2 lerps = frac( texelpos );

......

float LightAmount = lerp( lerp( sourcevals[0], sourcevals[1], lerps.x ), lerp( sourcevals[2], sourcevals[3], lerps.x ), lerps.y );


这篇关于Direct9学习之--------------------------实时阴影的另一种实现ShadowMap的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1099300

相关文章

Redis客户端连接机制的实现方案

《Redis客户端连接机制的实现方案》本文主要介绍了Redis客户端连接机制的实现方案,包括事件驱动模型、非阻塞I/O处理、连接池应用及配置优化,具有一定的参考价值,感兴趣的可以了解一下... 目录1. Redis连接模型概述2. 连接建立过程详解2.1 连php接初始化流程2.2 关键配置参数3. 最大连

Python实现网格交易策略的过程

《Python实现网格交易策略的过程》本文讲解Python网格交易策略,利用ccxt获取加密货币数据及backtrader回测,通过设定网格节点,低买高卖获利,适合震荡行情,下面跟我一起看看我们的第一... 网格交易是一种经典的量化交易策略,其核心思想是在价格上下预设多个“网格”,当价格触发特定网格时执行买

深入浅出SpringBoot WebSocket构建实时应用全面指南

《深入浅出SpringBootWebSocket构建实时应用全面指南》WebSocket是一种在单个TCP连接上进行全双工通信的协议,这篇文章主要为大家详细介绍了SpringBoot如何集成WebS... 目录前言为什么需要 WebSocketWebSocket 是什么Spring Boot 如何简化 We

python设置环境变量路径实现过程

《python设置环境变量路径实现过程》本文介绍设置Python路径的多种方法:临时设置(Windows用`set`,Linux/macOS用`export`)、永久设置(系统属性或shell配置文件... 目录设置python路径的方法临时设置环境变量(适用于当前会话)永久设置环境变量(Windows系统

Python对接支付宝支付之使用AliPay实现的详细操作指南

《Python对接支付宝支付之使用AliPay实现的详细操作指南》支付宝没有提供PythonSDK,但是强大的github就有提供python-alipay-sdk,封装里很多复杂操作,使用这个我们就... 目录一、引言二、准备工作2.1 支付宝开放平台入驻与应用创建2.2 密钥生成与配置2.3 安装ali

Spring Security 单点登录与自动登录机制的实现原理

《SpringSecurity单点登录与自动登录机制的实现原理》本文探讨SpringSecurity实现单点登录(SSO)与自动登录机制,涵盖JWT跨系统认证、RememberMe持久化Token... 目录一、核心概念解析1.1 单点登录(SSO)1.2 自动登录(Remember Me)二、代码分析三、

PyCharm中配置PyQt的实现步骤

《PyCharm中配置PyQt的实现步骤》PyCharm是JetBrains推出的一款强大的PythonIDE,结合PyQt可以进行pythion高效开发桌面GUI应用程序,本文就来介绍一下PyCha... 目录1. 安装China编程PyQt1.PyQt 核心组件2. 基础 PyQt 应用程序结构3. 使用 Q

Python实现批量提取BLF文件时间戳

《Python实现批量提取BLF文件时间戳》BLF(BinaryLoggingFormat)作为Vector公司推出的CAN总线数据记录格式,被广泛用于存储车辆通信数据,本文将使用Python轻松提取... 目录一、为什么需要批量处理 BLF 文件二、核心代码解析:从文件遍历到数据导出1. 环境准备与依赖库

linux下shell脚本启动jar包实现过程

《linux下shell脚本启动jar包实现过程》确保APP_NAME和LOG_FILE位于目录内,首次启动前需手动创建log文件夹,否则报错,此为个人经验,供参考,欢迎支持脚本之家... 目录linux下shell脚本启动jar包样例1样例2总结linux下shell脚本启动jar包样例1#!/bin

go动态限制并发数量的实现示例

《go动态限制并发数量的实现示例》本文主要介绍了Go并发控制方法,通过带缓冲通道和第三方库实现并发数量限制,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面... 目录带有缓冲大小的通道使用第三方库其他控制并发的方法因为go从语言层面支持并发,所以面试百分百会问到