【DirectX11】第九篇 光照模型——高光

本系列文章主要翻译和参考自《Real-Time 3D Rendering with DirectX and HLSL》一书(感谢原书作者),同时会加上一点个人理解和拓展,文章中如有错误,欢迎指正。

这里是书中的代码和资源。

本文所有的环境和工具使用都基于之前的文章,如有不明白的地方请先参考本系列之前的几篇文章。

本文索引:

  • 关于灯光
  • Specular Highlights 高光
    • 1 Phong 冯氏光照模型
    • 2 Phong Preamble 冯氏光照模型变量准备
    • 3 Phong Vertex Shader 冯氏模型顶点着色器
    • 4 Phong Pixel Shader 冯氏模型像素着色器
    • 5 Phong Output 冯氏光照模型效果输出
    • 6 Blinn-Phong 改进的冯氏光照模型
    • 7 Blinn-Phong Pixel Shader Blinn-Phong光照模型像素着色器
    • 8 Blinn-Phong with Intrinsics Blinn-Phong光照模型实现中使用内联函数
    • 9 Blinn-Phong vs Phong 两种高光模型计算的对比
  • 总结
  • 参考链接

关于灯光

在现实世界中,没有光我们将看不见任何东西,你所看见的物体或者是反射了光源的光或者是本身就能自发光。在计算机渲染的过程中,你将模拟灯光与物体的交互,并以此增加3D物体表面的细节。但是灯光的相互影响是一个非常复杂的过程,在目前的技术中并不能达到在一个可交互的帧率范围内进行这样大量的重复计算。因此,一般会采用一种近似算法,用一种描述灯光与3D模型如何交互的灯光模型来为你的感官增加更多可感受的细节。这篇文章中将介绍一些基础的光照模型。

Specular Highlights: 高光

当你模拟漫反射时,你只要能够提供出粗糙、亚光的表面感就可以了。这对大部分情况都是适用的,并且这提供了高光其他部分的基础照明。但有些时候你也需要制作一个闪光的表面模拟,例如,抛光金属和大理石地板。这部分要讲解的就是如何去实现这种高光效果。

(1) Phong 冯氏(光照模型)

有很多种方法可以用来模拟高光反射。我们第一个要研究的高光模型就是冯氏反射模型,这个模型以其发明者的名字名字,就是来自犹他大学(University of Utah)的Bui Tuong Phong。

跟漫反射不同,高光的计算需要根据观察者(相机)的位置和角度来计算。你可以在现实世界中观察到,当你改变观察高光物体的位置和角度时,其光斑位置也会发生变化。冯氏模型阐述了高光的计算依赖于观察者的观察方向和反射光向量之间的夹角。公式如下:

这里的R是反射光向量,V是观察方向,而指数S是高光的大小。越小的高光其指数值越大。反射光用下面的公式计算:

设顶点的单位法向量为N,有公式:

R + L = (2N ? L)N (这里再次提醒一下,L的方向是由顶点指向光源的)

由这个可以推出:

该公式中N代表表面法线,L代表光线向量。

下面的代码实现了一个冯氏模型:

代码段Listing 6.5 Phong.fx

#include "include\\Common.fxh"

/*************** Resources ***************/
cbuffer CBufferPerFrame
{
    float4 AmbientColor : AMBIENT
    <
        string UIName = "Ambient Light";
        string UIWidget = "Color";
    >  = {1.0f, 1.0f, 1.0f, 0.0f};

    float4 LightColor : COLOR
    <
        string Object = "LightColor0";
        string UIName = "Light Color";
        string UIWidget = "Color";
    > = {1.0f, 1.0f, 1.0f, 1.0f};

    float3 LightDirection : DIRECTION
    <
        string Object = "DirectionalLight0";
        string UIName = "Light Direction";
        string Space = "World";
    > = {0.0f, -1.0f, -1.0f};

    float3 CameraPosition : CAMERAPOSITION<string UIWidget="None";>;
}

cbuffer CBufferPerObject
{
    float4x4 WorldViewProjection : WORLDVIEWPROJECTION <string UIWidget="None";>;
    float4x4 World : WORLD <string UIWidget="None";>;

    float4 SpecularColor : SPECULAR
    <
        string UIName = "Specular Color";
        String UIWidget = "Color";
    > = {1.0f, 1.0f, 1.0f, 1.0f};

    float SpecularPower : SPECULARPOWER
    <
        string UIName = "Specular Power";
        string UIWidget = "Slider";
        float UIMin = 1.0;
        float UIMax = 255.0;
        float UIStep = 1.0;

    > = {25.0f};
}

Texture2D ColorTexture
<
    string ResourceName = "default_color.dds";
    string UIName = "Color Texture";
    string ResourceType = "2D";
>;

SamplerState ColorSampler
{
    Filter = MIN_MAG_MIP_LINEAR;
    AddressU = WRAP;
    AddressV = WRAP;
};

RasterizerState DisableCulling
{
    CullMode = NONE;
};

/*************** Data Structures ***************/
struct VS_INPUT
{
    float4 ObjectPosition : POSITION;
    float2 TextureCoordinate : TEXCOORD;
    float3 Normal : NORMAL;
};

struct VS_OUTPUT
{
    float4 Position : SV_Position;
    float3 Normal : NORMAL;
    float2 TextureCoordinate : TEXCOORD0;
    float3 LightDirection : TEXCOORD1;
    float3 ViewDirection : TEXCOORD2;
};

/*************** Vertex Shader ***************/
VS_OUTPUT vertex_shader(VS_INPUT IN)
{
    VS_OUTPUT OUT = (VS_OUTPUT)0;

    OUT.Position = mul(IN.ObjectPosition, WorldViewProjection);
    OUT.TextureCoordinate = get_corrected_texture_coordinate(IN.TextureCoordinate);
    OUT.Normal = normalize(mul(float4(IN.Normal, 0), World).xyz);
    OUT.LightDirection = normalize(-LightDirection);

    float3 worldPosition = mul(IN.ObjectPosition, World).xyz;
    OUT.ViewDirection = normalize(CameraPosition - worldPosition);

    return OUT;
}

/*************** Pixel Shader ***************/
float4 pixel_shader(VS_OUTPUT IN) : SV_Target
{
    float4 OUT = (float4)0;

    float3 normal = normalize(IN.Normal);
    float3 lightDirection = normalize(IN.LightDirection);
    float3 viewDirection = normalize(IN.ViewDirection);
    float n_dot_1 = dot(lightDirection, normal);

    float4 color = ColorTexture.Sample(ColorSampler, IN.TextureCoordinate);
    float3 ambient = AmbientColor.rgb * AmbientColor.a * color.rgb;

    float3 diffuse = (float3)0;
    float3 specular = (float3)0;

    if(n_dot_1 > 0)
    {
        diffuse = LightColor.rgb * LightColor.a * saturate(n_dot_1) * color.rgb;

        //R = 2 * (N.L) * N - L
        float3 reflectionVector = normalize(2 * n_dot_1 * normal - lightDirection);

        //specular = R.V^n with gloss map in color texture‘s alpha channel
        specular = SpecularColor.rgb * SpecularColor.a * min(pow(saturate(dot(reflectionVector, viewDirection)), SpecularPower), color.w);

        OUT.rgb = ambient + diffuse + specular;
        OUT.a = 1.0f;

        return OUT;
    }

    OUT.rgb = ambient + diffuse;
    OUT.a = color.a;

    return OUT;
}

/*************** Techniques ***************/
technique10 main10
{
    pass p0
    {
        SetVertexShader(CompileShader(vs_4_0, vertex_shader()));
        SetGeometryShader(NULL);
        SetPixelShader(CompileShader(ps_4_0, pixel_shader()));

        SetRasterizerState(DisableCulling);

    }
}

(2) Phong Preamble : 冯氏光照模型变量准备

对比上一篇文章中的漫反射效果,CBufferPerFrame缓冲区中只增加了一个成员:CameraPosition。这个常量储存了摄像机的位置也间接决定了视线方向。当你为这个常量关联了CAMERAPOSITION语义时,NVIDIA FX Composer会自动将Render面板中的摄像机位置绑定到这个常量上。

CBufferPerObject缓冲区中增加了两个成员SpecularColor和SpecularPower。SpecularColor和环境光、平行光有同样的函数体;他定义了高光的颜色和强度。这个多出来的部分可以让你独立调整高光而不用依赖于平行光。SpecularPower是指冯氏高光模型中的指数部分,用来调整高光的强度。

VS_OUTPUT结构体中也多出一个ViewDirection向量,这个向量用来将计算好的视线方向传递给光栅阶段进行处理。

(3) Phong Vertex Shader : 冯氏模型顶点着色器

在顶点着色器中,计算出了视线方向。但计算之前必须先统一坐标系,因此需要借助world矩阵将其转换到世界坐标系下。

(4) Phong Pixel Shader : 冯氏模型像素着色器

像素着色器中新加入了对高光部分的计算。这部分光只有在光线面向模型表面的时候才会有,所以必须先判断n_dot_1>0时才执行。

此外,像素着色器中比较复杂的是计算高光的那段代码,先将这段代码列在下面:

代码段Listing 6.6 Phong.fx中计算高光的代码

//specular = R.V^n with gloss map in color texture‘s alpha channel
specular = SpecularColor.rgb * SpecularColor.a * min(pow(saturate(dot(reflectionVector, viewDirection)), SpecularPower), color.w);

这段代码是根据冯氏光照模型的公式编写的。使用pow函数,其底数为反射光向量和视线向量的点乘,指数部分为SpeculiarPower常量。saturate()函数限制了计算结果在0.0到1.0之间,也就限制了角度必须在0~90度之间。

注释中也说明了高光的计算还依赖于光泽贴图(gloss map),这个贴图储存在纹理贴图的alpha通道中。光泽贴图,或者说高光贴图(specular map)要么存储在一张单独的纹理中(这张纹理只有一个通道),要么是像本文中的例子,存在材质贴图的某一部分,例如alpha通道中。高光贴图根据贴图制作者的意图改变了高光的计算结果。本文所使用的地球表面纹理中,海洋部分由于是水面应该是有高光反射的,而陆地部分应该是没有高光反射的。下面这张图展示了高光贴图的内容,可以看出陆地部分是黑色的,其值为0,这部分像素最后计算出来的高光部分的值应该也是0,因此只保留了漫反射部分的光照,而水面部分则会加入完全的高光效果。

(5) Phong Output : 冯氏光照模型效果输出

下面的图片中,左图展示了带有高光通道的贴图的渲染效果,右图的纹理中则不带有高光通道,注意观察他们在陆地部分的反射光效果区别:

(6) Blinn-Phong —— 改进的冯氏光照模型

1977年,Jim Blinn提出了简化版的冯氏模型,基于Half-Vector对光照模型的计算进行修改,Half-Vector是入射光与Eye的中间向量,计算公式如下:

从该公式可以看出,计算出的H(Half-Vector)是视线向量和入射光线中间向量的单位向量。Blinn-Phong光照模型由表面法线向量和Half-Vector计算出(与Phong模型的反射光和视线向量不同),这样增大了公式中的底数,计算公式如下:

(7) Blinn-Phong Pixel Shader : Blinn-Phong光照模型像素着色器

由于Blinn-Phong光照模型是在Phong光照模型的基础上修改的,他们大部分代码都是相似的,因此,只在此列出不同的部分,以下是像素着色器代码:

代码段 Listing 6.7 BlinnPhong.fx中的像素着色器

float4 pixel_shader(VS_OUTPUT IN) : SV_Target
{
    float4 OUT = (float4)0;

    float3 normal = normalize(IN.Normal);
    float3 lightDirection = normalize(IN.LightDirection);
    float3 viewDirection = normalize(IN.ViewDirection);
    float n_dot_1 = dot(lightDirection, normal);

    float4 color = ColorTexture.Sample(ColorSampler, IN.TextureCoordinate);
    float3 ambient = AmbientColor.rgb * AmbientColor.a * color.rgb;

    float3 diffuse = (float3)0;
    float3 specular = (float3)0;

    if(n_dot_1 > 0)
    {
        diffuse = LightColor.rgb * LightColor.a * saturate(n_dot_1) * color.rgb;

        float3 halfVector = normalize(lightDirection + viewDirection);

        //specular = N.H^s with gloss map in color texture‘s alpha channel
        specular = SpecularColor.rgb * SpecularColor.a * min(pow(saturate(dot(normal, halfVector)), SpecularPower), color.w);

        OUT.rgb = ambient + diffuse + specular;
        OUT.a = 1.0f;

        return OUT;
    }

    OUT.rgb = ambient + diffuse + specular;
    OUT.a = 1.0f;

    return OUT;
}

Blinn-Phong光照模型和Phong光照模型的显示效果几乎是一样的,但由于他的底数加大了,所以需要调整指数,也就是高光强度部分才能和Phong光照模型效果一样。如下图所示,左图为Blinn-Phong光照模型渲染效果,右图为Phong光照模型渲染效果,两个material的光照强度值设置的都为25:

可以看出Blinn-Phong光照模型的光斑在同样的光照强度参数下明显要比Phong光照模型的大些。

(8) Blinn-Phong with Intrinsics : Blinn-Phong光照模型实现中使用内联函数

关于内联函数(Intrinsics)的解释清查阅参考资料【1】中的instrinsics部分。

关于Intrinsics函数的原文引用说明
      内联函数有可能会被误会成我们通常想的那样,主要是这个单词我翻译不正确(intrinsics).这样的函数可以被C和C++的程序所调用.看上去和别的函数没有很多的区别,最多也就名字比较古怪.但是其实当这个代码在被编译器编译的时候,它会被转化为有序的低级指令.这些指令就是NEON的指令了.所以这样就办到了在高级语言层次使用低级语言了.主要是很简单的可以使用.最为主要的就是程序员不用去接触汇编了,可以减小优化的难度.当然我可以说这样的优化效率没有使用汇编的来的高.
      对于上面的这种技术其实就是ARM公司本身给你做好了一些函数,你就直接调用这些函数,这些函数在编译的时候就可以直接转化成NEON的汇编指令.为了支持这些内联的函数所以必须要包含头文件arm_neon.h.

文章中重要提到的是C里面的Intrinsics函数,但HLSL中的Intrinsics函数原理和他应该是类似的。

HLSL中提供了lit()函数,用来帮助计算Lambertian模型的漫反射部分和Blinn-Phong模型的高光部分。一个提升性能的很好的办法就是尽量在能使用Intrinsics函数或者说内置函数的时候使用它们,因为如上文所说这类型的函数在硬件中进行过优化。因此,我们将用lit()函数重写过的Blinn-Phong模型像素着色器列在下面:

代码段Listing 6.8 重写的Blinn-Phong光照模型像素着色器

float4 pixel_shader(VS_OUTPUT IN) : SV_Target
{
    float4 OUT = (float4)0;

    float3 normal = normalize(IN.Normal);
    float3 lightDirection = normalize(IN.LightDirection);
    float3 viewDirection = normalize(IN.ViewDirection);
    float n_dot_1 = dot(normal, lightDirection);
    float3 halfVector = normalize(lightDirection + viewDirection);
    float n_dot_h = dot(normal, halfVector);

    float4 color = ColorTexture.Sample(ColorSampler, IN.TextureCoordinate);
    float4 lightCoefficients = lit(n_dot_1, n_dot_h, SpecularPower);

    float3 ambient = get_vector_color_contribution(AmbientColor, color.rgb);
    float3 diffuse = get_vector_color_contribution(LightColor, lightCoefficients.y * color.rgb);
    float3 specular = get_scalar_color_contribution(SpecularColor, min(lightCoefficients.z, color.w));

    OUT.rgb = ambient + diffuse + specular;
    OUT.a = 1.0f;

    return OUT;
}

以上代码的改写和像素着色器有很多不同,为计算每种灯光单独封装了函数,所封装的get_vector_color_contribution和get_scalar_color_contribution函数在第七篇文章中提到的Common.fxh文件中已经写好了。并且在这个着色器中移除了条件判断句。新增加了lit()的调用,这个函数接受将n_dot_1和n_dot_h、高光强度作为输入参数,能实现和之前计算漫反射和blinn-phong高光模型计算出的结果一样的效果,返回值为float4,x和w的输出值总是1,y值代表的是这个像素上漫反射光照的计算结果,z值代表这个像素上blinn-phong光照计算结果。

(9) Blinn-Phong vs. Phong : 两种高光模型计算的对比

Blinn-Phong 和 Phong两种光照模型的实现效果相似,只是需要调节高光强度的大小不一样,还有应该就是渲染效率的问题。由于Blinn-Phong 中涉及到平方根的运算(这个运算来源于对half-vector向量的normalize计算)。但是,之后我们使用了lit()函数,这个内联函数使得Blinn-Phong 比Phong模型的计算又稍微提升了一些,并且减少了代码量。因此,在后面需要使用到带有漫反射和高光的模型时都将使用这个函数。

总结

这篇文章中主要讲解了两种高光模型的算法和shader实现。需要注意的是,本文中主要介绍了统一模型上混合了高光和漫反射的情况,但现实中也有很多模型是单一的,即比如玻璃杯和紫砂壶这类的物品他们的表面材质大部分是统一的。这种情况比较简单,只要注意对高光贴图的使用即可。

参考链接

【1】内联函数。(http://www.360doc.com/content/14/0428/15/9408846_372927783.shtml)

时间: 2024-10-13 05:24:36

【DirectX11】第九篇 光照模型——高光的相关文章

Python之路【第九篇】:Python操作 RabbitMQ、Redis、Memcache、SQLAlchemy

Python之路[第九篇]:Python操作 RabbitMQ.Redis.Memcache.SQLAlchemy Memcached Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载.它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高动态.数据库驱动网站的速度.Memcached基于一个存储键/值对的hashmap.其守护进程(daemon )是用C写的,但是客户端可以用任何语言来编写,并通过memcached协议与守护进程通信. Memc

第九篇 Integration Services:控制流任务错误

本篇文章是Integration Services系列的第九篇,详细内容请参考原文. 简介在前面三篇文章,我们创建了一个新的SSIS包,学习了脚本任务和优先约束,并检查包的MaxConcurrentExecutables属性.我们检查.演示并测试优先约束赋值为"成功"."完成"."失败"时对工作流的影响.我们学习了SSIS变量和表达式,并将它们应用到优先约束.这一篇,we introduce fault tolerance by examinin

第九篇 Replication:复制监视器

本篇文章是SQL Server Replication系列的第九篇,详细内容请参考原文. 复制监视器允许你查看复制配置组件的健康状况.这一篇假设你遵循前八篇,并且你已经有一个合并发布和事务发布.启动复制监控器复制监视器不是SSMS的一部分,它是一个独立的可执行文件(SqlMonitor.exe).在一个标准的SQL Server安装中开始菜单下找不到复制监视器.启动复制监视器的最简单方法是:SSMS对象资源管理器下连接到发布服务器,右击你的发布选择"启动复制监视器",如图9.1所示:图

用仿ActionScript的语法来编写html5——第九篇,仿URLLoader读取文件

第九篇,仿URLLoader读取文件 先看看最后的代码 function readFile(){ urlloader = new LURLLoader(); urlloader.addEventListener(LEvent.COMPLETE,readFileOk); urlloader.load("../file/test.txt","text"); } function readFileOk(){ mytxt.text = urlloader.data; } 基

Python之路【第九篇】:Python基础(26)——socket server

socketserver Python之路[第九篇]:Python基础(25)socket模块是单进程的,只能接受一个客户端的连接和请求,只有当该客户端断开的之后才能再接受来自其他客户端的连接和请求.当然我 们也可以通过python的多线程等模块自己写一个可以同时接收多个客户端连接和请求的socket.但是这完全没有必要,因为python标准库已经为 我们内置了一个多线程的socket模块socketserver,我们直接调用就可以了,完全没有必要重复造轮子. 我们只需简单改造一下之前的sock

第九篇 SQL Server代理了解作业和安全

本篇文章是SQL Server代理系列的第九篇,详细内容请参考原文 在这系列的前几篇,学习了如何在SQL Server代理作业步骤启动外部程序.你可以使用过时的ActiveX系统,运行批处理命令脚本,甚至自己的程序.你最好的选择是使用PowerShell的子系统运行PowerShell脚本.PowerShell脚本将允许你处理几乎所有方面的Windows和SQL Server问题.在这一篇,你会深入到SQL Server代理安全.安全是个令人困惑的话题,它值得一些明确的考虑.这系列有两个不同方面

学习java随笔第九篇:java异常处理

在java中的异常处理和c#中的异常处理是一样的都是用try-catch语句. 基本语法如下 try { //此处是可能出现异常的代码 } catch(Exception e) { //此处是如果发生异常的处理代码 } finally语句 try { //此处是可能出现异常的代码 } catch(Exception e) { //此处是如果发生异常的处理代码 } finally { //此处是肯定被执行的代码 } 抛出异常 使用thows和throw语句抛出异常 public static vo

SQL Server索引 - 索引(物化)视图 &lt;第九篇&gt;

一.索引视图基本概念 索引视图实际上是一种将一组唯一值“物化”为群集索引形式的视图,所为物化就是几乎和表一样,其数据也是会存储一份的(会占用硬盘空间,但是查询速度快,例如可以将count(),sum()等值设在索引视图中).其优点是它在提取视图背后的信息方面提供了一个非常快的查找方法.在第一个索引(必须是针对一组唯一值的聚集索引)之后,通过使用来自第一个索引的聚集键作为参考点,SQL Server还能在视图上建立额外的索引.其限制如下: 视图必须使用SCHEMABINDING选项: 如果视图引用

NHibernate Linq查询 扩展增强 (第九篇)

NHibernate Linq查询 扩展增强 (第九篇) 在上一篇的Linq to NHibernate的介绍当中,全部是namespace NHibernate命名空间中的IQueryOver<TRoot, TSubType>接口提供的.IQueryOver<TRoot, TSubType>这个借口实际上会被翻译成条件查询(Criteria Queries). 实际上Linq to NHibernate更加强大.我们先引入命名空间NHibernate.Linq,这里面有Linq