MME 编写入门(四)一个简单光照模型

本文由 简悦 SimpRead 转码, 原文地址 www.bilibili.com

作者:小林呓

纠结了半天,错误和各种不规范,最后看到一句话:计算机图形学的第一定律:如果他看起来是对的,那么他就是对的。

纠结了半天,错误和各种不规范,最后看到一句话:计算机图形学的第一定律:如果他看起来是对的,那么他就是对的。

《3D 数学基础:图形与游戏开发》的作者邓恩说的。

“他的话看起来是对的,所以是对的。” 我这么理解道。

float4x4 WorldViewMatrix        : WORLDVIEW;
float4x4 WorldViewProjMatrix    : WORLDVIEWPROJECTION;
float4x4 WorldMatrix            : WORLD;


float3   LightDirection    : DIRECTION < string Object = "Light"; >;
float3   CameraPosition    : POSITION  < string Object = "Camera"; >;



float4   MaterialDiffuse   : DIFFUSE  < string Object = "Geometry"; >;
float3   MaterialAmbient   : AMBIENT  < string Object = "Geometry"; >;
float3   MaterialEmmisive   : EMISSIVE  < string Object = "Geometry"; >;
float3   MaterialSpecular   : SPECULAR  < string Object = "Geometry"; >;

float    SpecularPower      : SPECULARPOWER < string Object = "Geometry"; >;
float3   MaterialToon      : TOONCOLOR;

float3   LightDiffuse      : DIFFUSE   < string Object = "Light"; >;
float3   LightAmbient      : AMBIENT   < string Object = "Light"; >;
float3   LightSpecular     : SPECULAR  < string Object = "Light"; >;

static float4 DiffuseColor  = MaterialDiffuse  * float4(LightDiffuse, 1.0f);
static float3 AmbientColor  = saturate(MaterialAmbient  * LightAmbient + MaterialEmmisive);
static float3 SpecularColor = MaterialSpecular * LightSpecular;

bool UseTexture;
bool UseToon ;

texture ObjectTexture: MATERIALTEXTURE;
sampler ObjTexSampler = sampler_state {
    texture = <ObjectTexture>;
    MINFILTER = LINEAR;
    MAGFILTER = LINEAR;
    ADDRESSU  = WRAP;
    ADDRESSV  = WRAP;
};

struct VS_OUTPUT
{
    float4 Pos        : POSITION;
    float2 Tex        : TEXCOORD1;
    float3 Normal      : TEXCOORD2;
    float3 Eye        : TEXCOORD3;
};

VS_OUTPUT Basic_VS( float4 Pos : POSITION, float3 Normal : NORMAL, float2 Tex : TEXCOORD0)
{

    float4 Pos0 = Pos;

    Pos = mul( Pos, WorldViewProjMatrix );

    float3 Eye = CameraPosition - mul( Pos0, WorldMatrix );

    Normal = normalize( mul( Normal, WorldMatrix ) );

    VS_OUTPUT Out = { Pos, Tex, Normal, Eye };
    return Out;
}


float4 Basic_PS( float2 Tex : TEXCOORD1, float3 Normal : TEXCOORD2, float3 Eye : TEXCOORD3 ) : COLOR0
{
    float4 Color = 0;

    Color.rgb = saturate( max(0,dot( Normal, -LightDirection )) * DiffuseColor.rgb + AmbientColor );
    Color.a = DiffuseColor.a;

    float3 HalfVector = normalize( normalize(Eye) + -LightDirection );
    float3 Specular = pow( max(0,dot( normalize(Normal),HalfVector )), SpecularPower ) * SpecularColor;

    Color *= tex2D( ObjTexSampler, Tex );

    if (UseTexture = true) {
        float LightNormal = dot(Normal,-LightDirection);
        Color.rgb *= lerp(MaterialToon, float3(1,1,1), saturate(LightNormal * 16 + 0.5));
    }

    Color.rgb += Specular;

    return Color;
}


technique BasicLight < string MMDPass = "object_ss"; > {
    pass DrawObject
    {
        VertexShader = compile vs_2_0 Basic_VS();
        PixelShader  = compile ps_2_0 Basic_PS();
    }
}

嗯…… 大概是对的吧…… 这个光照模型还缺少 Sph,不过问题倒也不大。

float4   SphereAddValue    : ADDINGSPHERETEXTURE;
float4   SphereMulValue    : MULTIPLYINGSPHERETEXTURE;

嗯…… 下次再考虑加上 spa 吧。看舞力介入的 full.fx 中是将影色单独拿出来写的……

首先是准备数据。这次需要的数据比上次多很多。

矩阵是必须的。接下来是这些。(唉我是真的麻了…… 我写的整整齐齐的代码扔进这个网页就不给我对齐,手动对齐好几遍还不齐)

float3   LightDirection    : DIRECTION < string Object = "Light"; >;
float3   CameraPosition    : POSITION  < string Object = "Camera"; >;
float4   MaterialDiffuse    : DIFFUSE  < string Object = "Geometry"; >;
float3   MaterialAmbient    : AMBIENT  < string Object = "Geometry"; >;
float3   MaterialEmmisive   : EMISSIVE < string Object = "Geometry"; >;
float3   MaterialSpecular    : SPECULAR < string Object = "Geometry"; >;
float    SpecularPower      : SPECULARPOWER < string Object = "Geometry"; >;
float3   MaterialToon        : TOONCOLOR;

float3   LightDiffuse      : DIFFUSE   < string Object = "Light"; >;
float3   LightAmbient      : AMBIENT   < string Object = "Light"; >;
float3   LightSpecular     : SPECULAR  < string Object = "Light"; >;

参考 MME 手册。即可明白书写的格式。把语义写对了就没太大问题。

颜色计算。static 的作用应该和 C 语言里的一样,用来说明静态变量,声明了 static 后变量就是内部变量。

static float4 DiffuseColor  = MaterialDiffuse  * float4(LightDiffuse, 1.0f);
static float3 AmbientColor  = saturate(MaterialAmbient  * LightAmbient + MaterialEmmisive);
static float3 SpecularColor  = MaterialSpecular * LightSpecular;

简单光照模型,假定物体为非透明物体,物体表面所呈现的颜色仅由反射光决定,不考虑透射光。反射光部分被细分为漫反射光 DiffuseColor 和 镜面反射光 SpecularColor 两种。

简单光照模型分为 环境光模型、漫反射光模型、镜面反射光模型,全部属于经验模型,表现为:

I 表示物体表面上一点反射到视点的光强,e、d、s 分别为环境光光强、漫反射光光强、镜面反射光光强。

材质部分,要考虑材质属性,材质属性是指物体表面对光的吸收、反射和透射的性能。由于是简单光照模型,所以只考虑材质的反射属性。

镜面反射光较为特殊,要考虑镜面反射率,所以引用了 float SPECULARPOWER

float    SpecularPower     : SPECULARPOWER < string Object = "Geometry"; >;

然后是

bool UseTexture;
bool UseToon ;

然后是采样器。

texture ObjectTexture: MATERIALTEXTURE;
sampler ObjTexSampler = sampler_state {
    texture = <ObjectTexture>;
    MINFILTER = LINEAR;
    MAGFILTER = LINEAR;
    ADDRESSU  = WRAP;
    ADDRESSV  = WRAP;
};

如果要调用纹理,那么就需要采样器。

然而纹理,是没法直接使用的。需要滤波。不理解也没关系,这东西绝大部分情况下都不用改,复制粘贴调用就行。

MinFilter = Linear; 
        MagFilter = Linear; 
        AddressU = Wrap; 
        AddressV = Wrap; 
struct VS_OUTPUT
{
    float4 Pos        : POSITION;
    float2 Tex        : TEXCOORD1;
    float3 Normal     : TEXCOORD2;
    float3 Eye        : TEXCOORD3;
};

结构体准备我们需要的用到的数据。然后是顶点着色,输入输出都很正常。这里我们呢需要准备 一个 Eye 的视点位置的数据。这个视点位置这样计算。

float3 Eye = CameraPosition - mul( Pos0, WorldMatrix );
Normal = normalize( mul( Normal, WorldMatrix ) );

Normal 顶点法线的转换,然后单位化。

这里用到了新的函数 normalize(x),意思是返回单位化向量,定义为 x / length(x)。

然后是像素着色器的部分。

首先得定义个 Color,后面才能用。

Color.rgb,意思是取得 Color 的前三维,我在上一篇有说但是没举例子。

举个例子,如果 float4 Color = float4(1,0.2,0.3,1),那么 Color.rgb 的意思就是调用 1,0.2,0.3,这三维。Color.r, 同理,调用第一维。

同理,xyzw,也可以。Color.xyzw 和 Color.rgba 等效,但是不能混着用。

float4 Color = 0;
Color.rgb = saturate( max(0,dot( Normal, -LightDirection )) * DiffuseColor.rgb + AmbientColor );
Color.a = DiffuseColor.a;

这里用到了新的函数 saturate(x),意思是把 x 截取到 [0, 1] 之间。

函数 max(x, y),意思是取 x,y 两者中较大的。

这里是计算物体的 rgb 值。就是物体的法向 * 漫反射颜色 + 环境色。

接下来计算反射光颜色。这里用到了一个半程向量,通过对观察向量和光向量取平均然后归一化得到一个新的向量,新向量称为半程向量 (bisector),使用半程向量与法线点乘来计算高光。

float3 HalfVector = normalize( normalize(Eye) + -LightDirection );
float3 Specular = pow( max(0,dot( normalize(Normal),HalfVector )), SpecularPower ) *SpecularColor;

这里有个新函数 pow,pow(x, y),意思是 x^y。

使用半程向量,是为了简化计算。

图片源于《三维计算机图形学》第 251 页,作者:孔令德 康凤娥

Phong 提出的一个计算镜面反射光的经验公式,成为 Phong 模型。模型上一点 P 的镜面反射光强

为镜面反射率

对于单位向量 R、V,有, 考虑有大于 90 的情况,R·V 的结果为负,应该取 0,所以改写为

不难看出镜面反射的光强不仅取决于物体表面的法线方向,而且依赖于光远与视线的相对位置。只有当视点位于比较合适的位置时,才可以观察到物体表面某些区域呈现的高光。所以在简单光照模型中,“视点固定,旋转物体”和 “视点旋转,物体固定” 所看到的物体是有差别的。

V 的计算很简单,R 的计算稍微复杂。


图片源于《三维计算机图形学》

总结下来,Phong 模型的计算非常复杂,再加上环境光和漫射光,会让式子变得更长。

Blinn 指出中分向量 H 的方向是最大反射光强方向,对模型进行改进,改进后的模型成为 Blinn-Phong 模型。Blinn 用 N·H 代替 R·V,其中 H 为中分向量,是单位光向量 L 和单位观察向量 V 的平分向量。

改进后的公式表述为


图片源于《三维计算机图形学》

Color *= tex2D( ObjTexSampler, Tex );

if (UseTexture = true) {
    float LightNormal = dot(Normal,-LightDirection);
    Color.rgb *= lerp(MaterialToon, float3(1,1,1), saturate(LightNormal * 16 + 0.5));
}

Color.rgb += Specular;

这里用到了新的函数 tex2D(s,t),意思是返回纹理 s 在 t 位置的颜色。

接下来做个判断,woc 我这里写错了,前边的插入的代码块不知道怎么变更了…… 麻烦把这个 UseTexture 改成 UseToon,这里是在判断是否使用 toon,做了一个插值计算。

if (UseToon = true)

虽然可能对结果影响不大,因为现在绝大部分模型都有 toon。

这里用了一个新函数 lerp,lerp(x, y, s),对 x、y 进行插值计算。Returns x + s(y - x)。

最后是个 pass

technique BasicLight < string MMDPass = "object_ss"; > {
    pass DrawObject
    {
        VertexShader = compile vs_2_0 Basic_VS();
        PixelShader  = compile ps_2_0 Basic_PS();
    }
}

注意

评论