Unity3D CustomSRP[譯].3.平行光[Directional Lights]

Directional Lights(平行光、方向光)

——直接照明


本節(jié)內(nèi)容

  • 使用法向量來計(jì)算光照
  • 支持多達(dá)四個(gè)平行光
  • 應(yīng)用雙向反射分布函數(shù)(BRDF)
  • 制作有照明的透明材質(zhì)
  • 創(chuàng)建一個(gè)自定義的著色器圖形用戶界面(Shader GUI)躏嚎。



這是一個(gè)關(guān)于如何創(chuàng)建一個(gè)Custom SRP的系列教程的第三個(gè)部分冤狡,它添加了對(duì)多個(gè)平行光的支持完箩。

這個(gè)教程使用的是Unity版本是2019.2.6f1.

由四個(gè)光照亮的各種球體

(ps:文章總被吞…最后偶然看到可能會(huì)被吞的一些詞兒…嘗試改了點(diǎn)但有些意思感覺不到位~)


1. 照明(Lighting)

如果我們想要?jiǎng)?chuàng)建一個(gè)更真實(shí)的場(chǎng)景鹿霸,那么我們必須模擬光如何與物體的表面相互作用。這需要一個(gè)比我們目前的無光照的shader更復(fù)雜的shader泪电。


1.1 照明著色器(Lit Shader)

復(fù)制UnlitPass.hlsl文件并將其重命名為LitPass.hlsl般妙。調(diào)整引用保護(hù)定義以及頂點(diǎn)和片元函數(shù)名。稍后我們將添加光照計(jì)算相速。

#ifndef CUSTOM_LIT_PASS_INCLUDED
#define CUSTOM_LIT_PASS_INCLUDED

…

Varyings LitPassVertex (Attributes input) { … }

float4 LitPassFragment (Varyings input) : SV_TARGET { … }

#endif

也復(fù)制Unlit著色器碟渺,并將其重命名為Lit。更改其菜單名稱和蚪、引用的文件止状、以及使用的函數(shù)。讓我們同樣也改變默認(rèn)顏色為灰色攒霹,因?yàn)橐粋€(gè)完全白色的表面在一個(gè)明亮的場(chǎng)景中會(huì)顯得非常明亮怯疤。URP默認(rèn)也使用灰色。

Shader "Custom RP/Lit" {
    Properties {
        _BaseMap("Texture", 2D) = "white" {}
        _BaseColor("Color", Color) = (0.5, 0.5, 0.5, 1.0)
        …
    }

    SubShader {
        Pass {
            …
            #pragma vertex LitPassVertex
            #pragma fragment LitPassFragment
            #include "LitPass.hlsl"
            ENDHLSL
        }
    }
}

我們將使用一個(gè)自定義照明方法催束,通過設(shè)置shader的照明模式為CustomLit集峦。在Pass中添加一個(gè)Tags塊,包含"LightMode" = "CustomLit"

Pass {
    Tags {
        "LightMode" = "CustomLit"
    }

    …
}

要渲染使用這個(gè)pass的對(duì)象塔淤,我們必須在CameraRenderer中包含它摘昌。首先為它添加一個(gè)shader標(biāo)簽標(biāo)識(shí)符。

static ShaderTagId
    unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit"),
    litShaderTagId = new ShaderTagId("CustomLit");

然后將它添加到要在DrawVisibleGeometry中渲染的pass中高蜂,就像我們?cè)?code>DrawUnsupportedShaders中做的那樣聪黎。

var drawingSettings = new DrawingSettings(
            unlitShaderTagId, sortingSettings
) {
    enableDynamicBatching = useDynamicBatching,
    enableInstancing = useGPUInstancing
};
drawingSettings.SetShaderPassName(1, litShaderTagId);

現(xiàn)在我們可以創(chuàng)建一個(gè)新的非透明的材質(zhì),盡管目前它產(chǎn)生的結(jié)果與無光照的材質(zhì)相同备恤。

默認(rèn)的非透明材質(zhì)


1.2 法向量(Normal Vectors)

一個(gè)物體被照亮的程度取決于多種因素稿饰,包括光與物體表面之間的相對(duì)角度。為了知道表面的方向露泊,我們需要訪問表面的法線喉镰,它是一個(gè)垂直于表面的單位長(zhǎng)度的向量。這個(gè)向量是頂點(diǎn)數(shù)據(jù)的一部分惭笑,在對(duì)象空間中定義侣姆,就像位置一樣。 所以把它添加到LitPassAttributes中沉噩。

struct Attributes {
    float3 positionOS : POSITION;
    float3 normalOS : NORMAL;
    float2 baseUV : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

光照是根據(jù)每個(gè)片元計(jì)算的捺宗,所以我們必須將法向量添加到Varyings中川蒙。我們將在世界空間中執(zhí)行計(jì)算偿凭,因此將其命名為normalWS

struct Varyings {
    float4 positionCS : SV_POSITION;
    float3 normalWS : VAR_NORMAL;
    float2 baseUV : VAR_BASE_UV;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

我們可以使用來自SpaceTransforms.hlslTransformObjectToWorldNormal方法在LitPassVertex中將法線變換到世界空間中派歌。

output.positionWS = TransformObjectToWorld(input.positionOS);
output.positionCS = TransformWorldToHClip(positionWS);
output.normalWS = TransformObjectToWorldNormal(input.normalOS);


TransformObjectToWorldNormal 是如何工作的?
.
檢查代碼時(shí)痰哨,您將看到它使用了兩種方法之一胶果,這取決于是否定義了 UNITY_ASSUME_UNIFORM_SCALING
.
當(dāng) UNITY_ASSUME_UNIFORM_SCALING被定義時(shí)斤斧,它調(diào)用 TransformObjectToWorldDir早抠,這和 TransformObjectToWorld做的是一樣的,除了它忽略了平移部分撬讽,因?yàn)槲覀兲幚淼氖欠较蛳蛄慷皇俏恢萌锪5沁@個(gè)向量也會(huì)被均勻縮放,所以之后應(yīng)該被歸一化游昼。
·
在另一種情況下鳄虱,不假設(shè)是均勻縮放挨厚。這是更復(fù)雜的,因?yàn)楫?dāng)一個(gè)物體因非均勻縮放而變形時(shí),法向量必須反向縮放以匹配新的表面方向扩借。這需要與轉(zhuǎn)置的 UNITY_MATRIX_I_M矩陣相乘哆窿,并進(jìn)行歸一化。
·

不正確和正確的法線變換

使用 UNITY_ASSUME_UNIFORM_SCALING是一個(gè)輕微的優(yōu)化,你可以通過自己定義它來啟用靖榕。然而,當(dāng)使用 GPU-Instancing時(shí)顽铸,這將更有意義茁计。因?yàn)?UNITY_MATRIX_I_M矩陣數(shù)組必須發(fā)送給GPU,在不需要的時(shí)候避免這樣做是值得的谓松。你可以通過在著色器中添加 #pragma instancing_options assumeuniformscaling指令來啟用它星压,但只有在你用統(tǒng)一縮放渲染對(duì)象時(shí)才這么做。


為了驗(yàn)證我們是否在LitPassFragment中得到了正確的法向量毒返,我們可以使用它作為顏色輸出租幕。

base.rgb = input.normalWS;
return base;

世界空間的法向量

負(fù)值無法顯示,所以它們被固定為零拧簸。


1.3 差值法線(Interpolated Normals)

雖然在頂點(diǎn)程序中劲绪,法向量是單位長(zhǎng)度的,但三角形之間的線性插值會(huì)影響它們的長(zhǎng)度盆赤。我們可以通過渲染1和向量的長(zhǎng)度之間的差值來可視化誤差贾富,并將結(jié)果放大10倍,使其更明顯牺六。

base.rgb = abs(length(input.normalWS) - 1.0) * 10.0;

放大的法線差值的誤差

我們可以通過對(duì)LitPassFragment中的法向量進(jìn)行歸一化來平滑插值失真颤枪。當(dāng)只看法向量時(shí),這種差異并不明顯淑际,但當(dāng)用于照明時(shí)畏纲,這種差異就更明顯了。

base.rgb = normalize(input.normalWS);

差值后的歸一化


1.4 表面屬性(Surface Properties)

在一個(gè)shader中產(chǎn)生照明需要模擬光照之間的交互作用春缕,這意味著我們必須跟蹤表面的屬性〉琳停現(xiàn)在我們有一個(gè)法向量和一個(gè)基色。我們可以將后者分成兩部分:RGB顏色和Alpha值锄贼。我們將在一些不同的地方使用這些數(shù)據(jù)票灰,所以讓我們定義一個(gè)方便的Surface結(jié)構(gòu)體來包含所有相關(guān)數(shù)據(jù)。把這個(gè)結(jié)構(gòu)體放在ShaderLibrary文件夾中的一個(gè)單獨(dú)的Surface.hlsl文件中宅荤。

#ifndef CUSTOM_SURFACE_INCLUDED
#define CUSTOM_SURFACE_INCLUDED

struct Surface {
    float3 normal;
    float3 color;
    float alpha;
};

#endif


我們不應(yīng)該把法線定義為 normalWS 嗎屑迂?
.
可以,但是表面不關(guān)心法線是在什么空間定義的冯键。光照計(jì)算可以在任何合適的3D空間中進(jìn)行惹盼,所以我們不為法線定義這個(gè)空間限制。當(dāng)填充數(shù)據(jù)時(shí)琼了,我們只需要在所有地方使用相同的空間逻锐。我們將使用世界空間夫晌,但我們之后有可能會(huì)切換到另一個(gè)空間,一切仍將保持不變昧诱。


LitPassCommon之后引用它晓淀,這樣我們就可以保持LitPass的簡(jiǎn)潔。從現(xiàn)在起盏档,我們將把專用的代碼放在它們自己的HLSL文件中凶掰,以便更容易地定位相關(guān)的功能。

#include "../ShaderLibrary/Common.hlsl"
#include "../ShaderLibrary/Surface.hlsl"

LitPassFragment中定義一個(gè)surface變量并填充它蜈亩,結(jié)果變成表面的顏色和透明度懦窘。

Surface surface;
surface.normal = normalize(input.normalWS);
surface.color = base.rgb;
surface.alpha = base.a;

return float4(surface.color, surface.alpha);


這不是低效的代碼嗎?
.
這沒有區(qū)別稚配,因?yàn)橹骶幾g器將生成高度優(yōu)化的程序畅涂,完全重寫了我們的代碼。結(jié)構(gòu)體純粹是為了方便我們使用道川。你可以通過在著色器面板的 Compile and show code按鈕檢查編譯器的編譯結(jié)果午衰。


1.5 光照計(jì)算(Calculating Lighting)

為了計(jì)算實(shí)際的光照,我們將創(chuàng)建一個(gè)具有Surface參數(shù)的GetLighting函數(shù)冒萄。最初讓它返回表面法線的Y分量臊岸。因?yàn)檫@是照明功能,我們將把它放在一個(gè)單獨(dú)的Lighting.hlsl文件中尊流。

#ifndef CUSTOM_LIGHTING_INCLUDED
#define CUSTOM_LIGHTING_INCLUDED

float3 GetLighting (Surface surface) {
    return surface.normal.y;
}

#endif

LitPass中引用Surface之后引用它帅戒,因?yàn)檎彰饕蕾囉?code>Surface.hlsl。

#include "../ShaderLibrary/Surface.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"


為什么不在 Lighting.hlsl 中 引用 Surface.hlsl崖技?
.
我們可以這樣做逻住,但最終的結(jié)果可能是多個(gè)文件依賴于多個(gè)其他文件,依賴關(guān)系會(huì)十分雜亂迎献。相反鄙信,我選擇將所有include語句放在一個(gè)地方,這樣可以明確依賴關(guān)系忿晕。這也使得用一個(gè)文件替換另一個(gè)文件從而改變著色器的工作方式變得更加容易,只要新文件定義了其他文件依賴的相同功能银受。


現(xiàn)在我們可以在LitPassFragment中獲得照明践盼,并將其用于片元函數(shù)返回顏色的RGB部分。

    float3 color = GetLighting(surface);
    return float4(color, surface.alpha);

漫反射光照

現(xiàn)在宾巍,輸出的是表面法線的Y分量咕幻,所以它在球體的頂部是1,在它的兩側(cè)是0顶霞。再往下結(jié)果變?yōu)樨?fù)值肄程,并在底部達(dá)到- 1锣吼。但我們觀察不到負(fù)值,它等于法向量和上(up)向量夾角的余弦值蓝厌。忽略負(fù)的部分玄叠,這在視覺上就好像一個(gè)漫反射的平行光從上垂直的向下照明。最后一步是在GetLighting中把表面顏色合并到結(jié)果中拓提,將其詮釋為表面反照率(Albedo)读恃。

float3 GetLighting (Surface surface) {
    return surface.normal.y * surface.color;
}
應(yīng)用反照率


反照率(Albedo)是什么意思?
.
反照率在拉丁語中是白色程度的意思代态。它代表著光被一個(gè)表面漫反射的程度寺惫。如果反照率不是全白,那么意味著部分光能被吸收而不是被反射蹦疑。


2. 燈光(Lights)

為了表現(xiàn)合適的照明西雀,我們還需要知道光源的屬性。在本章節(jié)中歉摧,我們將只使用平行光艇肴。平行光代表著一個(gè)距離很遠(yuǎn)很遠(yuǎn)的光源,它的位置并不重要判莉,重要的是它的方向豆挽。這是一種簡(jiǎn)化,但它足以模擬地球上的太陽(yáng)光和其他單向光線的情況券盅。


2.1 光照結(jié)構(gòu)(Light Structure)

我們將使用一個(gè)結(jié)構(gòu)體來存儲(chǔ)光的數(shù)據(jù)“锕現(xiàn)在我們只需要一個(gè)顏色和一個(gè)方向就夠了。將其放在單獨(dú)的Light.hlsl文件中锰镀。同時(shí)定義一個(gè)GetDirectionalLight函數(shù)來返回一個(gè)配置好的平行光娘侍。使用白色和向上矢量初始化它,匹配我們目前使用的光照數(shù)據(jù)泳炉。請(qǐng)注意憾筏,光的方向的定義是,光線從哪里來花鹅,而不是它要到哪里去氧腰。

#ifndef CUSTOM_LIGHT_INCLUDED
#define CUSTOM_LIGHT_INCLUDED

struct Light {
    float3 color;
    float3 direction;
};

Light GetDirectionalLight () {
    Light light;
    light.color = 1.0;
    light.direction = float3(0.0, 1.0, 0.0);
    return light;
}

#endif

LitPass中引用Lighting.hlsl之前引用它。

#include "../ShaderLibrary/Light.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"


2.2 照明函數(shù)(Lighting Functions)

Lighting中添加一個(gè)IncomingLight函數(shù)刨肃,計(jì)算在給定一個(gè)表面和光源后有多少入射光古拴。對(duì)于任意方向的光,我們必須取表面法線和光方向的點(diǎn)積真友。我們可以用點(diǎn)積函數(shù)黄痪。結(jié)果應(yīng)該被光的顏色調(diào)節(jié)。

float3 IncomingLight (Surface surface, Light light) {
    return dot(surface.normal, light.direction) * light.color;
}


什么是點(diǎn)積盔然?
.
兩個(gè)向量的點(diǎn)積在幾何學(xué)上的定義如下圖:


這意味著它是兩個(gè)向量夾角的余弦值桅打,乘以它們的長(zhǎng)度是嗜。 對(duì)于兩個(gè)單位長(zhǎng)度的向量 A·B=cos θ
在代數(shù)中,他被如下圖這樣定義:
·

可以通過將所有的分量相乘并相加來計(jì)算他們的值:
·

將點(diǎn)積具象化的話挺尾,這個(gè)操作是直接將一個(gè)矢量投射到另一個(gè)矢量上鹅搪,就像在它投下陰影一樣。通過這樣做潦嘶,你最終得到一個(gè)直角三角形涩嚣,其底邊的長(zhǎng)度正是點(diǎn)積的結(jié)果。如果兩個(gè)向量都是單位長(zhǎng)度掂僵,那點(diǎn)積的結(jié)果就是它們夾角的余弦值航厚。
·
點(diǎn)積


但這只在表面朝向光線時(shí)才正確。 當(dāng)點(diǎn)積為負(fù)時(shí)锰蓬,我們需要將它限制到零幔睬,這可以通過saturate函數(shù)來實(shí)現(xiàn)。

float3 IncomingLight (Surface surface, Light light) {
    return saturate(dot(surface.normal, light.direction)) * light.color;
}


saturate 做了什么芹扭?fffffffff
·
它將一個(gè)值限制于0和1之間麻顶。我們只需要指定一個(gè)最小值,因?yàn)辄c(diǎn)積永遠(yuǎn)不應(yīng)該大于1舱卡。 saturate是一個(gè)非常常見的著色器操作辅肾。


添加另一個(gè)GetLighting函數(shù),它返回表面和光源的最終照明÷肿叮現(xiàn)在它返回的是入射光結(jié)果乘以表面顏色矫钓。在其他函數(shù)上面定義這個(gè)函數(shù)。

float3 GetLighting (Surface surface, Light light) {
    return IncomingLight(surface, light) * surface.color;
}

最后舍杜,調(diào)整GetLighting函數(shù)新娜,它只有一個(gè)表面參數(shù),所以它需要調(diào)用另一個(gè)同名函數(shù)來獲取一個(gè)光源信息既绩,使用GetDirectionalLight來提供平行光數(shù)據(jù)概龄。

float3 GetLighting (Surface surface) {
    return GetLighting(surface, GetDirectionalLight());
}


2.3 向GPU發(fā)送光源數(shù)據(jù)(Sending Light Data to the GPU)

我們應(yīng)該使用當(dāng)前場(chǎng)景的光,而不是總是使用之前定義的白色的光饲握。默認(rèn)場(chǎng)景有一個(gè)平行光私杜,代表太陽(yáng),它是淡黃色的——使用了十六進(jìn)制的值fff4d6救欧,并圍繞X軸旋轉(zhuǎn)50°歪今,圍繞Y軸旋轉(zhuǎn)30°。如果這樣的光不存在颜矿,那就創(chuàng)建一個(gè)。

為了在shader中訪問光的數(shù)據(jù)嫉晶,我們必須為它定義一個(gè)值骑疆,就像著色器屬性一樣田篇。在這個(gè)例子中,我們將定義兩個(gè)float3類型的向量:_DirectionalLightColor_DirectionalLightDirection箍铭。把它們放在一個(gè)定義在頂部的_CustomLight緩沖區(qū)中泊柬。

CBUFFER_START(_CustomLight)
    float3 _DirectionalLightColor;
    float3 _DirectionalLightDirection;
CBUFFER_END

GetDirectionalLight中使用這些值而不是常量。

Light GetDirectionalLight () {
    Light light;
    light.color = _DirectionalLightColor;
    light.direction = _DirectionalLightDirection;
    return light;
}

現(xiàn)在我們的渲染管線必須把光數(shù)據(jù)發(fā)送到GPU诈火。我們將為此創(chuàng)建一個(gè)新的Lighting.cs類兽赁。它的工作原理類似于CameraRenderer,但是用于照明冷守。給它一個(gè)帶有context參數(shù)的公共方法Setup刀崖,在這個(gè)方法中它調(diào)用一個(gè)單獨(dú)的SetupDirectionalLight方法。盡管不是嚴(yán)格必要的拍摇,但讓我們也為它提供一個(gè)專用的命令緩沖區(qū)亮钦,以便在完成時(shí)執(zhí)行,這樣也便于調(diào)試充活。另一種方法是可以添加一個(gè)緩沖區(qū)參數(shù)蜂莉。

using UnityEngine;
using UnityEngine.Rendering;

public class Lighting {

    const string bufferName = "Lighting";

    CommandBuffer buffer = new CommandBuffer {
        name = bufferName
    };
    
    public void Setup (ScriptableRenderContext context) {
        buffer.BeginSample(bufferName);
        SetupDirectionalLight();
        buffer.EndSample(bufferName);
        context.ExecuteCommandBuffer(buffer);
        buffer.Clear();
    }
    
    void SetupDirectionalLight () {}
}

跟蹤著色器兩個(gè)屬性的標(biāo)識(shí)符。

static int
        dirLightColorId = Shader.PropertyToID("_DirectionalLightColor"),
        dirLightDirectionId = Shader.PropertyToID("_DirectionalLightDirection");

我們可以通過RenderSettings.sun訪問場(chǎng)景的主光源混卵。這讓我們默認(rèn)情況下得到最重要的平行光映穗,它也可以通過Window/Rendering/Lighting Settings設(shè)置。使用CommandBuffer.SetGlobalVector將光源數(shù)據(jù)發(fā)送到GPU幕随。顏色是光在線性空間中的顏色蚁滋,而方向是光源變換信息的正向量取反后的值。

void SetupDirectionalLight () {
    Light light = RenderSettings.sun;
    buffer.SetGlobalVector(dirLightColorId, light.color.linear);
    buffer.SetGlobalVector(dirLightDirectionId, -light.transform.forward);
}


SetGlobalVector 不是需要 Vector4 類型的參數(shù)嗎合陵?
·
是的枢赔,發(fā)送給GPU的向量總是有四個(gè)分量,即使我們以較少的分量定義它們拥知,其余的分量會(huì)在shader中被隱式掩蓋踏拜。同樣,也有一個(gè)從Vector3到Vector4的隱式轉(zhuǎn)換低剔。



光的color屬性是它的配置顏色速梗,但光也有一個(gè)單獨(dú)的強(qiáng)度因子。最后的顏色需要兩者相乘襟齿。

buffer.SetGlobalVector(
    dirLightColorId, light.color.linear * light.intensity
);

CameraRenderer一個(gè)Lighting實(shí)例姻锁,并在繪制可見幾何圖形之前使用它來設(shè)置照明。

Lighting lighting = new Lighting();

public void Render (
    ScriptableRenderContext context, Camera camera,
    bool useDynamicBatching, bool useGPUInstancing
) {
        …

    Setup();
    lighting.Setup(context);
    DrawVisibleGeometry(useDynamicBatching, useGPUInstancing);
    DrawUnsupportedShaders();
    DrawGizmos();
    Submit();
}
被太陽(yáng)光照亮


2.4 可見光(Visible Lights)

在進(jìn)行裁剪時(shí)猜欺,Unity還會(huì)計(jì)算出哪些燈光會(huì)影響到對(duì)相機(jī)可見的空間位隶。我們可以依靠這些信息,而不是單一的全局太陽(yáng)光开皿。為此涧黄,Lighting需要訪問裁剪結(jié)果篮昧,所以為Setup添加一個(gè)參數(shù),并將其存儲(chǔ)在字段中笋妥。然后我們可以支持多個(gè)光源懊昨,所以用一個(gè)新的SetupLights方法替換掉SetupDirectionalLight的調(diào)用。

CullingResults cullingResults;

public void Setup (ScriptableRenderContext context, CullingResults cullingResults) {
    this.cullingResults = cullingResults;
    buffer.BeginSample(bufferName);
    //SetupDirectionalLight();
    SetupLights();
    …
}
    
void SetupLights () {}

CameraRenderer.Render中調(diào)用Setup時(shí)春宣,添加裁剪結(jié)果作為參數(shù)酵颁。

    lighting.Setup(context, cullingResults);

現(xiàn)在Lighting.SetupLights可以通過裁剪結(jié)果的visibleLights屬性獲取所需的數(shù)據(jù)。它是一個(gè)Unity.Collections.NativeArray模板類結(jié)構(gòu)月帝,元素類型為VisibleLight躏惋。

using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;

public class Lighting {
    …

    void SetupLights () {
        NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;
    }

    …
}


什么是 NativeArray?
·
它是一個(gè)作用類似于數(shù)組的結(jié)構(gòu)嫁赏,但提供了到本機(jī)內(nèi)存緩沖區(qū)的鏈接其掂。這使得在托管的c#代碼和原生的Unity引擎代碼之間高效地共享數(shù)據(jù)成為可能。


2.5 多個(gè)平行光(Multiple Directional Lights)

使用可見光數(shù)據(jù)可以支持多個(gè)平行光潦蝇,但我們必須將所有這些光的數(shù)據(jù)發(fā)送到GPU款熬。所以我們不再使用僅僅一對(duì)向量,而是使用兩個(gè)Vector4數(shù)組存儲(chǔ)光的方向和顏色攘乒,另外加上一個(gè)整數(shù)作為光的計(jì)數(shù)贤牛。我們還將定義最大數(shù)量的平行光,我們可以使用它初始化兩個(gè)數(shù)組字段來緩沖數(shù)據(jù)则酝。讓我們將最大值設(shè)置為4殉簸,這對(duì)大多數(shù)場(chǎng)景來說應(yīng)該足夠了。

const int maxDirLightCount = 4;

    static int
        //dirLightColorId = Shader.PropertyToID("_DirectionalLightColor"),
        //dirLightDirectionId = Shader.PropertyToID("_DirectionalLightDirection");
        dirLightCountId = Shader.PropertyToID("_DirectionalLightCount"),
        dirLightColorsId = Shader.PropertyToID("_DirectionalLightColors"),
        dirLightDirectionsId = Shader.PropertyToID("_DirectionalLightDirections");

    static Vector4[]
        dirLightColors = new Vector4[maxDirLightCount],
        dirLightDirections = new Vector4[maxDirLightCount];


為什么不使用結(jié)構(gòu)化的緩沖區(qū)沽讹?
·
也許可能吧般卑,但我不準(zhǔn)備用,因?yàn)橹鲗?duì)結(jié)構(gòu)化的緩沖區(qū)的支持還不夠好爽雄。它們要么根本不被支持或只在片元函數(shù)中存在蝠检,要么比常規(guī)數(shù)組性能更差。好消息是挚瘟,數(shù)據(jù)在CPU和GPU之間如何傳遞的細(xì)節(jié)只影響少數(shù)地方叹谁,所以很容易改變。這是使用Light結(jié)構(gòu)體的另一個(gè)好處乘盖。


SetupDirectionalLight添加一個(gè)索引和一個(gè)VisibleLight參數(shù)焰檩。讓它根據(jù)提供的索引設(shè)置顏色和方向數(shù)據(jù)。在這種情況下订框,最終的顏色是通過調(diào)用VisibleLight.finalColor獲得的
析苫。可以通過VisibleLight.localToWorldMatrix獲得前向量,它是矩陣的第三列衩侥,同樣我們需要對(duì)它取反浪腐。

void SetupDirectionalLight (int index, VisibleLight visibleLight) {
    dirLightColors[index] = visibleLight.finalColor;
    dirLightDirections[index] = -visibleLight.localToWorldMatrix.GetColumn(2);
}

最終的顏色已經(jīng)應(yīng)用了光的強(qiáng)度,但默認(rèn)情況下Unity不會(huì)將其轉(zhuǎn)換到線性空間顿乒。我們必須設(shè)置GraphicsSettings. lightsUseLinearIntensity設(shè)置為true,我們可以在CustomRenderPipeline的構(gòu)造函數(shù)中執(zhí)行一次泽谨。

public CustomRenderPipeline (
        bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher
) {
    this.useDynamicBatching = useDynamicBatching;
    this.useGPUInstancing = useGPUInstancing;
    GraphicsSettings.useScriptableRenderPipelineBatching = useSRPBatcher;
    GraphicsSettings.lightsUseLinearIntensity = true;
}

接下來璧榄,在Lighting.SetupLights中循環(huán)所有可見光,并為每個(gè)元素調(diào)用SetupDirectionalLight吧雹。然后調(diào)用bufferSetGlobalIntSetGlobalVectorArray將數(shù)據(jù)發(fā)送給GPU骨杂。

NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;
for (int i = 0; i < visibleLights.Length; i++) {
    VisibleLight visibleLight = visibleLights[i];
    SetupDirectionalLight(i, visibleLight);
}

buffer.SetGlobalInt(dirLightCountId, visibleLights.Length);
buffer.SetGlobalVectorArray(dirLightColorsId, dirLightColors);
buffer.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);

但是我們最多只支持四個(gè)方向燈,所以當(dāng)我們達(dá)到這個(gè)最大值時(shí)雄卷,我們應(yīng)該中止循環(huán)搓蚪。讓我們從循環(huán)的迭代器中單獨(dú)跟蹤平行光的索引。

int dirLightCount = 0;
for (int i = 0; i < visibleLights.Length; i++) {
    VisibleLight visibleLight = visibleLights[i];
    SetupDirectionalLight(dirLightCount++, visibleLight);
    if (dirLightCount >= maxDirLightCount) {
        break;
    }
}

buffer.SetGlobalInt(dirLightCountId, dirLightCount);

因?yàn)槲覀兡壳爸恢С制叫泄舛○模晕覀儜?yīng)該忽略其他類型的燈妒潭。我們可以通過檢查可見光的lightType屬性是否等于LightType.directional來做到這一點(diǎn)。

VisibleLight visibleLight = visibleLights[i];
if (visibleLight.lightType == LightType.Directional) {
    SetupDirectionalLight(dirLightCount++, visibleLight);
    if (dirLightCount >= maxDirLightCount) {
        break;
    }
}

這是可行的揣钦,但VisibleLight的結(jié)構(gòu)相當(dāng)大雳灾。理想情況下,我們應(yīng)該只從數(shù)組中檢索它一次冯凹,而不是將它作為常規(guī)參數(shù)傳遞給SetupDirectionalLight谎亩,因?yàn)槟菢訒?huì)復(fù)制它划乖。我們可以使用Unity用于ScriptableRenderContext.DrawRenderers方法的技巧有决,通過ref傳遞參數(shù)。

SetupDirectionalLight(dirLightCount++, ref visibleLight);

這也要求我們將參數(shù)定義為引用甜攀。

void SetupDirectionalLight (int index, ref VisibleLight visibleLight) { … }


2.6 著色器循環(huán)(Shader Loop)

Light.hlsl中調(diào)整_CustomLight緩沖區(qū)浑劳,使其匹配我們的新數(shù)據(jù)阱持。在本例中,我們將顯式地使用float4作為數(shù)組類型呀洲。數(shù)組在著色器中需要有固定的大小紊选,它們不能被調(diào)整大小。確保使用與我們?cè)?code>Lighting.cs中定義的相同的最大值道逗。

#define MAX_DIRECTIONAL_LIGHT_COUNT 4

CBUFFER_START(_CustomLight)
    //float4 _DirectionalLightColor;
    //float4 _DirectionalLightDirection;
    int _DirectionalLightCount;
    float4 _DirectionalLightColors[MAX_DIRECTIONAL_LIGHT_COUNT];
    float4 _DirectionalLightDirections[MAX_DIRECTIONAL_LIGHT_COUNT];
CBUFFER_END

添加一個(gè)函數(shù)來獲取平行光計(jì)數(shù)以及調(diào)整GetDirectionalLight函數(shù)兵罢,以便它可以獲取特定的光索引的數(shù)據(jù)。

int GetDirectionalLightCount () {
    return _DirectionalLightCount;
}

Light GetDirectionalLight (int index) {
    Light light;
    light.color = _DirectionalLightColors[index].rgb;
    light.direction = _DirectionalLightDirections[index].xyz;
    return light;
}


rgb 和 xyz 之間有什么區(qū)別嗎滓窍?
·
他們都是語義的別名卖词,使用 rgbaxyzw是等價(jià)的。(譯注:但通常來講,我們使用 rgba 獲取顏色此蜈,用 xyzw 獲取向量和矢量等即横,便于功能上的區(qū)分和理解)


然后調(diào)整GetLighting函數(shù),使用for循環(huán)來分別計(jì)算并累計(jì)每個(gè)平行光對(duì)于表面光照的貢獻(xiàn)裆赵。

float3 GetLighting (Surface surface) {
    float3 color = 0.0;
    for (int i = 0; i < GetDirectionalLightCount(); i++) {
        color += GetLighting(surface, GetDirectionalLight(i));
    }
    return color;
}
四個(gè)平行光

現(xiàn)在我們的shader最多支持四個(gè)平行光东囚。 通常只需要一個(gè)平行光來代表太陽(yáng)或月亮,但也許在一個(gè)行星上有多個(gè)太陽(yáng)的場(chǎng)景战授。平行光也可以用于模擬多個(gè)大型燈光平臺(tái)页藻,例如大型體育場(chǎng)的燈光。

如果你的游戲總是有一個(gè)單一的方向光植兰,那么你可以擺脫循環(huán)份帐,或制作多個(gè)shader變體。但在本教程中楣导,我們將保持簡(jiǎn)單废境,堅(jiān)持使用一個(gè)通用目的的循環(huán)。最好的性能總是需要通過刪除不需要的內(nèi)容來實(shí)現(xiàn)筒繁,盡管這樣做并不總是會(huì)產(chǎn)生顯著的差異噩凹。


2.7 著色器目標(biāo)等級(jí)(Shader Target Level)

具有可變長(zhǎng)度的循環(huán)曾經(jīng)是shader的一個(gè)問題,但現(xiàn)代GPU可以毫無壓力的處理它們膝晾,特別是當(dāng)draw call的所有片元以相同的方式迭代相同的數(shù)據(jù)栓始。然而,OpenGL ES 2.0和WebGL 1.0的圖形api在默認(rèn)情況下不能處理這樣的循環(huán)血当。我們可以通過結(jié)合一個(gè)硬編碼的最大值來讓它工作幻赚,例如通過GetDirectionalLight返回min(_DirectionalLightCount, MAX_DIRECTIONAL_LIGHT_COUNT),這使得循環(huán)成為可能臊旭,將其轉(zhuǎn)換為條件代碼塊序列落恼。

不幸的是,這產(chǎn)生的shader代碼是亂糟糟的离熏,性能也下降很多佳谦。在非常老式的硬件上,所有的代碼塊都會(huì)被執(zhí)行滋戳,它們是通過條件賦值來控制的钻蔑。雖然我們可以使它工作,但它使代碼更復(fù)雜奸鸯,因?yàn)槲覀冞€必須進(jìn)行其他更多的調(diào)整咪笑。

因此,為了簡(jiǎn)單起見娄涩,我選擇忽略這些限制窗怒,在項(xiàng)目中取消對(duì)WebGL 1.0和OpenGL ES 2.0的支持,它們不支持線性照明。 我們也可以通過#pragma target 3.5指令扬虚,將我們的著色器的目標(biāo)級(jí)別提升到3.5努隙,從而避免為它們編譯OpenGL ES 2.0的著色器變體。讓我們對(duì)兩個(gè)著色器都這樣做辜昵。

HLSLPROGRAM
#pragma target 3.5
…
ENDHLSL


3. 雙向反射分布函數(shù)(BRDF)

我們目前使用的是一個(gè)非常簡(jiǎn)單的光照模型荸镊,只適合完美的漫反射表面。我們可以通過應(yīng)用雙向反射分布函數(shù)(Bidirectional Reflectance Distribution Function)堪置,簡(jiǎn)稱BRDF贷洲,來實(shí)現(xiàn)更豐富和真實(shí)的照明。有許多類型的BRDF函數(shù)晋柱。我們將使用URP所使用的相同的方法,這為了性能犧牲了一些更真實(shí)的表現(xiàn)诵叁。


3.1 入射光(Incoming Light)

當(dāng)一束光迎面擊中表面的片段時(shí)雁竞,它所有的能量都會(huì)影響到片段。為了簡(jiǎn)單起見拧额,我們假定光束的寬度與表面片段的寬度相匹配碑诉。這就是光的方向L和表面法線N平行對(duì)齊的情況,即 N·L = 1侥锦。當(dāng)他們沒有平行對(duì)齊而是至少有一部分的光會(huì)偏離表面进栽,所以影響表面的能量也就少了。影響表面的能量是 N·L恭垦。結(jié)果為負(fù)的話就表明表面遠(yuǎn)離了光入射的方向快毛,所以它不會(huì)被光影響到。

入射光和表面


3.2 出射光(Outgoing Light)

我們看不到直接到達(dá)表面的光番挺。我們只能看到從表面反射到相機(jī)或我們眼睛的那部分唠帝。如果表面是一個(gè)完美的平面鏡,那么光線就會(huì)被反射出去玄柏,出射角與入射角相等襟衰。只有當(dāng)相機(jī)與出射光一致時(shí),我們才能看到這道光粪摘。這就是所謂的鏡面反射瀑晒。這是光與表面相互作用的簡(jiǎn)化,但對(duì)于我們當(dāng)前來說已經(jīng)足夠了徘意。

完美的鏡面反射

但如果表面不是完全平坦苔悦,光線就會(huì)被散射,因?yàn)楸砻娴钠螌?shí)際上是由許多方向不同的小碎片組成的映砖。這將光線分成不同方向的更細(xì)小的光束间坐,這有效地模糊了鏡面反射。我們最終可能會(huì)看到一些散射的光,即使沒有與完美的反射方向?qū)R竹宋。

分散的鏡面反射

除此之外劳澄,光也會(huì)穿透表面,在表面細(xì)碎的部位反彈蜈七,以不同的角度離開秒拔,以及還有一些其他我們目前不需要考慮的事情。在極端情況下飒硅,我們最終會(huì)得到一個(gè)完全漫反射的表面砂缩,將光線均勻地散射到所有可能的方向。這是我們當(dāng)前在shader中計(jì)算的光照三娩。

完全的漫反射

無論相機(jī)在哪里庵芭,表面接收到的漫射光的量是一樣的,但這也意味著我們觀測(cè)到的光能遠(yuǎn)遠(yuǎn)小于到達(dá)表面碎片的光能雀监,這表明我們應(yīng)該用另外一些因素來衡量入射光双吆。然而,因?yàn)檫@個(gè)因素總是一樣的会前,我們可以把它放入光的顏色和強(qiáng)度中好乐。因此,我們使用的最終光顏色代表了從一個(gè)完美的白色漫反射表面片段瓦宜,接受正面照明時(shí)觀察到的數(shù)量蔚万,這只是實(shí)際發(fā)出的光總量的一小部分。還有其他配置光源的方法临庇,例如指定光通量或輻照度反璃,這樣更容易配置真實(shí)的光源,但我們目前將堅(jiān)持現(xiàn)在這種的方法假夺。


3.3 表面屬性(Surface Properties)

表面可以是完美的漫反射版扩,或完美的高光反射(鏡面反射),或者介于兩者之間的任何東西侄泽。我們有很多方法可以控制它礁芦,目前我們將使用金屬工作流,需要添加兩個(gè)表面屬性到Lit著色器中悼尾。

第一個(gè)屬性用于表明一個(gè)表面是金屬的還是非金屬的柿扣,也被稱為電介質(zhì)。因?yàn)橐粋€(gè)表面可以包含兩者的混合闺魏,我們將為它添加一個(gè)范圍0-1的滑動(dòng)條未状,1表示它是完全金屬的,默認(rèn)為0析桥。

第二個(gè)屬性控制表面的光滑程度司草。我們還將為此使用范圍0-1滑動(dòng)條艰垂,0表示完全粗糙,1表示完全光滑埋虹。我們將使用0.5作為默認(rèn)值猜憎。

_Metallic ("Metallic", Range(0, 1)) = 0
_Smoothness ("Smoothness", Range(0, 1)) = 0.5

具有金屬度和光滑度屬性滑動(dòng)條的材質(zhì)

將屬性添加到UnityPerMaterial緩存區(qū)中。

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
    UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
    UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
    UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)
    UNITY_DEFINE_INSTANCED_PROP(float, _Metallic)
    UNITY_DEFINE_INSTANCED_PROP(float, _Smoothness)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

還有Surface結(jié)構(gòu)體搔课。

struct Surface {
    float3 normal;
    float3 color;
    float alpha;
    float metallic;
    float smoothness;
};

LitPassFragment中將它們復(fù)制到surface中胰柑。

    Surface surface;
    surface.normal = normalize(input.normalWS);
    surface.color = base.rgb;
    surface.alpha = base.a;
    surface.metallic = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Metallic);
    surface.smoothness =
        UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Smoothness);

并在PerObjectMaterialProperties.cs中添加對(duì)它們的支持。

    static int
        baseColorId = Shader.PropertyToID("_BaseColor"),
        cutoffId = Shader.PropertyToID("_Cutoff"),
        metallicId = Shader.PropertyToID("_Metallic"),
        smoothnessId = Shader.PropertyToID("_Smoothness");

    …

    [SerializeField, Range(0f, 1f)]
    float alphaCutoff = 0.5f, metallic = 0f, smoothness = 0.5f;

    …

    void OnValidate () {
        …
        block.SetFloat(metallicId, metallic);
        block.SetFloat(smoothnessId, smoothness);
        GetComponent<Renderer>().SetPropertyBlock(block);
    }


3.4 BRDF 屬性(BRDF Properties)

我們將利用表面屬性來計(jì)算BRDF方程爬泥。它告訴我們有多少光從一個(gè)表面反射回來柬讨,這是漫反射和高光反射兩者的結(jié)合。我們需要在漫反射和高光反射部分分割表面顏色袍啡,我們還需要知道表面有多粗糙踩官。讓我們?cè)谝粋€(gè)BRDF結(jié)構(gòu)體中存放和跟蹤這三個(gè)值,放在一個(gè)單獨(dú)的BRDF.hlsl文件中境输。

#ifndef CUSTOM_BRDF_INCLUDED
#define CUSTOM_BRDF_INCLUDED

struct BRDF {
    float3 diffuse;
    float3 specular;
    float roughness;
};

#endif

添加一個(gè)函數(shù)來獲取給定表面的BRDF數(shù)據(jù)卖鲤。讓我們從一個(gè)完全漫反射的表面開始,因此漫反射部分應(yīng)該相當(dāng)于表面顏色畴嘶,而高光部分則是黑色,粗糙度為1集晚。

BRDF GetBRDF (Surface surface) {
    BRDF brdf;
    brdf.diffuse = surface.color;
    brdf.specular = 0.0;
    brdf.roughness = 1.0;
    return brdf;
}

在引用Light.hlslLighting.hlsl之間引用BRDF.HLSL

#include "../ShaderLibrary/Common.hlsl"
#include "../ShaderLibrary/Surface.hlsl"
#include "../ShaderLibrary/Light.hlsl"
#include "../ShaderLibrary/BRDF.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"

為兩個(gè)GetLighting函數(shù)添加一個(gè)BRDF參數(shù)窗悯,然后將入射光與漫反射部分相乘,而不是整個(gè)表面顏色偷拔。

float3 GetLighting (Surface surface, BRDF brdf, Light light) {
    return IncomingLight(surface, light) * brdf.diffuse;
}

float3 GetLighting (Surface surface, BRDF brdf) {
    float3 color = 0.0;
    for (int i = 0; i < GetDirectionalLightCount(); i++) {
        color += GetLighting(surface, brdf, GetDirectionalLight(i));
    }
    return color;
}

最后蒋院,在LitPassFragment中獲取BRDF數(shù)據(jù),并將其傳遞給GetLighting莲绰。

BRDF brdf = GetBRDF(surface);
float3 color = GetLighting(surface, brdf);


3.5 反射率(Reflectivity)

一個(gè)表面如何反射是可變的欺旧,但一般來說,金屬表面通過鏡面反射反射所有的光蛤签,而其漫反射為零辞友。所以我們聲明反射率等于金屬表面的屬性。被反射的光不會(huì)被漫反射震肮,所以我們應(yīng)該在GetBRDF中將漫反射的比例調(diào)整為1-反射率称龙。

float oneMinusReflectivity = 1.0 - surface.metallic;

brdf.diffuse = surface.color * oneMinusReflectivity;
金屬度依次為 0,0.25戳晌,0.5鲫尊,0.75,1

在現(xiàn)實(shí)中沦偎,一些光也會(huì)被非金屬表面反射疫向,這就給了它們高光咳蔚。非金屬的反射率各不相同,但平均約為0.04搔驼。讓我們將其定義為最小反射率谈火,并添加一個(gè)OneMinusReflectivity函數(shù),將0-1范圍調(diào)整到0-0.96匙奴。此范圍調(diào)整與URP使用的方法相同堆巧。

#define MIN_REFLECTIVITY 0.04

float OneMinusReflectivity (float metallic) {
    float range = 1.0 - MIN_REFLECTIVITY;
    return range - metallic * range;
}

GetBRDF中使用該函數(shù)限制一個(gè)最小值。當(dāng)只渲染漫反射時(shí)泼菌,這種差異很難被注意到谍肤,但當(dāng)我們添加高光反射后,這種差異會(huì)變得很明顯哗伯。沒有它荒揣,非金屬就不會(huì)有高光。

float oneMinusReflectivity = OneMinusReflectivity(surface.metallic);


3.6 高光反射的顏色(Specular Color)

以一種方式反射的光不能以另一種方式反射焊刹,這就是所謂的能量守恒系任,這意味著出射光的能量不能超過入射光的能量,這也表明高光部分的顏色應(yīng)該等于表面顏色減去漫反射顏色虐块。

brdf.diffuse = surface.color * oneMinusReflectivity;
brdf.specular = surface.color - brdf.diffuse;

然而俩滥,這忽略了一個(gè)事實(shí),金屬會(huì)影響高光反射的顏色贺奠,而非金屬則不會(huì)霜旧。非金屬表面的高光部分的顏色應(yīng)該是白色的,我們可以通過使用金屬度在最小反射率和表面顏色之間進(jìn)行插值來實(shí)現(xiàn)儡率。

brdf.specular = lerp(MIN_REFLECTIVITY, surface.color, surface.metallic);


3.7 粗糙度(Roughness)

粗糙度是光滑度的反義詞挂据,所以我們可以簡(jiǎn)單地用1減去平滑度。Core RP庫(kù)中有一個(gè)名為PerceptualSmoothnessToPerceptualRoughness的函數(shù)儿普,我們將使用這個(gè)函數(shù)崎逃,來定義光滑度和粗糙度是“感知上的”。我們可以通過PerceptualRoughnessToRoughness函數(shù)轉(zhuǎn)換為實(shí)際的粗糙度值眉孩,該函數(shù)對(duì)“感知的值”進(jìn)行平方个绍,這種方法與迪士尼的光照模型相匹配。之所以這樣做浪汪,是因?yàn)樵诰庉嫴馁|(zhì)時(shí)調(diào)整“感知的值”更為直觀障贸。

    float perceptualRoughness =
        PerceptualSmoothnessToPerceptualRoughness(surface.smoothness);
    brdf.roughness = PerceptualRoughnessToRoughness(perceptualRoughness);

這些函數(shù)在Core RP庫(kù)的CommonMaterial.hlsl文件中定義。在我們自己的Common.hlsl文件中引用Common.hlsl之后已用它吟宦。

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
#include "UnityInput.hlsl"


3.8 視圖方向(View Direction)

為了確定相機(jī)與完美反射方向的對(duì)齊情況篮洁,我們需要知道相機(jī)的位置。Unity通過float3 _WorldSpaceCameraPos使這些數(shù)據(jù)可訪問殃姓,所以將其添加到UnityInput.hlsl中袁波。

float3 _WorldSpaceCameraPos;

為了在LitPassFragment中獲得視圖方向瓦阐,即從表面到相機(jī)的方向,我們需要在Varyings中添加表面在世界空間的位置篷牌。

struct Varyings {
    float4 positionCS : SV_POSITION;
    float3 positionWS : VAR_POSITION;
    …
};

Varyings LitPassVertex (Attributes input) {
    …
    output.positionWS = TransformObjectToWorld(input.positionOS);
    output.positionCS = TransformWorldToHClip(output.positionWS);
    …
}

我們將視圖方向視為表面數(shù)據(jù)的一部分睡蟋,因此將其添加到Surface中。

struct Surface {
    float3 normal;
    float3 viewDirection;
    float3 color;
    float alpha;
    float metallic;
    float smoothness;
};

LitPassFragment中對(duì)它賦值枷颊,它等于相機(jī)坐標(biāo)減去片元坐標(biāo)戳杀,并進(jìn)行歸一化。

    surface.normal = normalize(input.normalWS);
    surface.viewDirection = normalize(_WorldSpaceCameraPos - input.positionWS);


3.9 高光強(qiáng)度(Specular Strength)

我們觀察到的高光反射的強(qiáng)度取決于我們的視角方向與完美反射方向的匹配程度夭苗。我們將使用URP中使用的相同公式信卡,它是極簡(jiǎn)化的Cook-Torrance BRDF的變體。 這個(gè)公式包含一些平方操作题造,所以讓我們先在Common.hlsl中添加一個(gè)方便的Square函數(shù)傍菇。

float Square (float v) {
    return v * v;
}

然后在BRDF中添加一個(gè)帶有SurfaceBRDF數(shù)據(jù)和Light參數(shù)的SpecularStrength函數(shù)界赔,這個(gè)函數(shù)應(yīng)該計(jì)算如下圖的公式:

其中d代表飽和度丢习,N表示表面法線,L是光的方向淮悼,H表示歸一化后的L + V咐低,是一個(gè)光的方向和視圖方向中間的向量。使用SafeNormalize方法讓向量歸一化袜腥,來避免向量相反的情況下除以0,见擦。最后,n = 4r + 2 是一個(gè)標(biāo)準(zhǔn)化的項(xiàng)瞧挤。

float SpecularStrength (Surface surface, BRDF brdf, Light light) {
    float3 h = SafeNormalize(light.direction + surface.viewDirection);
    float nh2 = Square(saturate(dot(surface.normal, h)));
    float lh2 = Square(saturate(dot(light.direction, h)));
    float r2 = Square(brdf.roughness);
    float d2 = Square(nh2 * (r2 - 1.0) + 1.00001);
    float normalization = brdf.roughness * 4.0 + 2.0;
    return r2 / (d2 * max(0.1, lh2) * normalization);
}


這個(gè)方法是如何工作的?
·
BRDF 理論太復(fù)雜了儡湾,不能簡(jiǎn)單地解釋清楚特恬,也不是本教程的重點(diǎn)。 您可以查看 URPLighting.hlsl文件獲得一些代碼文檔和參考資料徐钠。


接下來癌刽,添加一個(gè)DirectBRDF方法,它將返回通過直接照明獲得的顏色尝丐。給定一個(gè)表面显拜、BRDF數(shù)據(jù)和光數(shù)據(jù)。結(jié)果是高光顏色由高光強(qiáng)度控制爹袁,再加上漫反射顏色远荠。

float3 DirectBRDF (Surface surface, BRDF brdf, Light light) {
    return SpecularStrength(surface, brdf, light) * brdf.specular + brdf.diffuse;
}

GetLighting則必須將入射光乘以該函數(shù)的輸出。

float3 GetLighting (Surface surface, BRDF brdf, Light light) {
    return IncomingLight(surface, light) * DirectBRDF(surface, brdf, light);
}

光滑度從上到下為0失息、0.25譬淳、0.5档址、0.75、0.95

我們現(xiàn)在實(shí)現(xiàn)了高光反射反射邻梆,它為我們的表面添加了高光部分守伸。對(duì)于完全粗糙的表面,高光模擬漫反射浦妄。光滑的表面得到更集中的亮光尼摹。一個(gè)完全光滑的表面會(huì)有一個(gè)我們看不到的無限小的高光。需要一些散射才能使它可見剂娄。

由于能量守恒蠢涝,對(duì)于光滑的表面,因?yàn)榇蟛糠值竭_(dá)表面的光都聚焦了宜咒,高光可以變得非常明亮惠赫,因此,我們才會(huì)看到更多的光故黑,而不是由于高光部分可見的漫反射儿咱。你可以通過大幅縮小最終輸出的顏色來驗(yàn)證這一點(diǎn)。

最終顏色除以100

你也可以通過使用白色以外的基色场晶,來驗(yàn)證金屬材質(zhì)會(huì)影響高光部分的顏色混埠,而非金屬材質(zhì)不會(huì)。

藍(lán)色基色

我們現(xiàn)在有了可靠的直接照明的功能诗轻,盡管目前的效果還太暗钳宪,尤其是對(duì)金屬材質(zhì),因?yàn)槲覀冞€不支持環(huán)境反射等扳炬。在這一點(diǎn)上吏颖,一個(gè)標(biāo)準(zhǔn)的黑色環(huán)境會(huì)比默認(rèn)的天空框更真實(shí),但這會(huì)讓我們的對(duì)象更難以觀察到恨樟。添加更多的燈光也可以做到半醉。

四個(gè)光


3.10 網(wǎng)格球體(Mesh Ball)

讓我們也為MeshBall.cs添加對(duì)不同金屬度和平滑度屬性的支持。這需要添加兩個(gè)浮點(diǎn)數(shù)組劝术。

static int
        baseColorId = Shader.PropertyToID("_BaseColor"),
        metallicId = Shader.PropertyToID("_Metallic"),
        smoothnessId = Shader.PropertyToID("_Smoothness");

    …
    float[]
        metallic = new float[1023],
        smoothness = new float[1023];

    …

    void Update () {
        if (block == null) {
            block = new MaterialPropertyBlock();
            block.SetVectorArray(baseColorId, baseColors);
            block.SetFloatArray(metallicId, metallic);
            block.SetFloatArray(smoothnessId, smoothness);
        }
        Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 1023, block);
    }

讓我們?cè)?code>Awake中缩多,把25%的實(shí)例設(shè)置為金屬質(zhì)感,光滑度在0.05到0.95之間隨機(jī)养晋。

baseColors[i] =
                new Vector4(
                    Random.value, Random.value, Random.value,
                    Random.Range(0.5f, 1f)
                );
            metallic[i] = Random.value < 0.25f ? 1f : 0f;
            smoothness[i] = Random.Range(0.05f, 0.95f);

然后讓網(wǎng)格球體使用一個(gè)照明材質(zhì)衬吆。

照亮網(wǎng)格球


4. 透明(Transparency)

讓我們?cè)俅慰紤]透明度。物體仍然會(huì)根據(jù)它們的alpha值變淡绳泉,但現(xiàn)在是反射光也變淡了逊抡。這對(duì)于漫反射是有意義的,因?yàn)橹挥幸徊糠止獗环瓷淞憷遥溆嗟墓獯┻^表面秦忿。

變淡的球體

然而麦射,高光反射也會(huì)減弱。在全透的玻璃情況下灯谣,光要么通過潜秋,要么被反射。高光反射部分不會(huì)減弱胎许。我們目前的方法不能表現(xiàn)出這一點(diǎn)峻呛。


4.1 預(yù)乘透明度(Premultiplied Alpha)

解決的辦法是只淡化漫反射光,同時(shí)保持高光反射的全部強(qiáng)度辜窑。Src Blend模式目前不適用于我們的需求钩述,讓我們將其設(shè)置為One,而目標(biāo)混合模式仍然使用OneMinusSrcAlpha穆碎。

更改混合模式

這恢復(fù)了高光反射的強(qiáng)度牙勘,但漫反射沒有減弱。我們通過將使用表面透明度來減弱漫反射顏色來解決這個(gè)問題所禀。因此我們用透明度對(duì)漫反射進(jìn)行預(yù)乘方面,而不是稍后依靠GPU去混合。這種方法被稱為預(yù)乘透明度混合色徘。在GetBRDF里這樣做恭金。

brdf.diffuse = surface.color * oneMinusReflectivity;
brdf.diffuse *= surface.alpha;

預(yù)乘后的漫反射


4.2 預(yù)乘開關(guān)(Premultiplication Toggle)

將透明度和漫反射預(yù)乘有效地把對(duì)象變成像玻璃的材質(zhì),而一成不變的透明度混合模式使對(duì)象總是只存在部分褂策。讓我們同時(shí)支持兩種方法横腿,通過在GetBRDF中添加一個(gè)布爾參數(shù)來控制是否對(duì)透明度進(jìn)行預(yù)乘,默認(rèn)設(shè)置為false斤寂。

BRDF GetBRDF (inout Surface surface, bool applyAlphaToDiffuse = false) {
    …
    if (applyAlphaToDiffuse) {
        brdf.diffuse *= surface.alpha;
    }

    …
}

我們可以使用一個(gè)_PREMULTIPLY_ALPHA關(guān)鍵字來決定在LitPassFragment中使用哪種方法耿焊,類似于我們之前如何控制透明度裁剪邻奠。

#if defined(_PREMULTIPLY_ALPHA)
    BRDF brdf = GetBRDF(surface, true);
#else
    BRDF brdf = GetBRDF(surface);
#endif
float3 color = GetLighting(surface, brdf);
return float4(color, surface.alpha);

Lit的Pass添加一個(gè)著色器特性(shader feature)粮宛。

#pragma shader_feature _CLIPPING
#pragma shader_feature _PREMULTIPLY_ALPHA

并添加一個(gè)開關(guān)屬性环壤。

[Toggle(_PREMULTIPLY_ALPHA)] _PremulAlpha ("Premultiply Alpha", Float) = 0

預(yù)乘透明度開關(guān)


5. 著色器的圖形用戶界面(Shader GUI)

我們現(xiàn)在支持多種渲染模式娩缰,每種模式都需要特定的設(shè)置。為了使模式之間的切換更容易芭商,讓我們?cè)诓馁|(zhì)面板中添加一些按鈕來應(yīng)用預(yù)設(shè)配置。


5.1 自定義著色器GUI(Custom Shader GUI)

添加一個(gè)CustomEditor "CustomShaderGUI"語句到燈光著色器的主塊。

Shader "Custom RP/Lit" {
    …

    CustomEditor "CustomShaderGUI"
}

這指示Unity編輯器使用CustomShaderGUI類的一個(gè)實(shí)例來繪制使用Lit著色器的材質(zhì)的面板再愈。為該類創(chuàng)建一個(gè)腳本資源,并將其放入一個(gè)新的Custom RP/Editor文件夾中护戳。

我們需要使用UnityEditor翎冲、 UnityEngineUnityEngine.Rendering名稱空間。這個(gè)類必須擴(kuò)展ShaderGUI并重寫公共的OnGUI方法媳荒,它有一個(gè)MaterialEditor和一個(gè)MaterialProperty數(shù)組參數(shù)抗悍。讓它調(diào)用基類方法驹饺,這樣我們就得到了默認(rèn)的顯示面板。

using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;

public class CustomShaderGUI : ShaderGUI {

    public override void OnGUI (
        MaterialEditor materialEditor, MaterialProperty[] properties
    ) {
        base.OnGUI(materialEditor, properties);
    }
}


5.2 設(shè)置屬性和關(guān)鍵字(Setting Properties and Keywords)

為了完成我們的工作缴渊,我們需要訪問三個(gè)東西赏壹,我們將它們存儲(chǔ)在字段中。首先是材質(zhì)編輯器衔沼,它是底層的編輯器對(duì)象蝌借,負(fù)責(zé)顯示和編輯材質(zhì)。第二個(gè)是對(duì)正在編輯的材質(zhì)的引用指蚁,我們可以通過編輯器的targets屬性來訪問它菩佑,它被定義為一個(gè)Object數(shù)組,因?yàn)閠argets是通用Editor類的一個(gè)屬性凝化。第三個(gè)是可以編輯的屬性數(shù)組稍坯。

    MaterialEditor editor;
    Object[] materials;
    MaterialProperty[] properties;

    public override void OnGUI (
        MaterialEditor materialEditor, MaterialProperty[] properties
    ) {
        base.OnGUI(materialEditor, properties);
        editor = materialEditor;
        materials = materialEditor.targets;
        this.properties = properties;
    }


為什么會(huì)有多種材質(zhì)?
·
使用相同著色器的多個(gè)材質(zhì)可以同時(shí)被編輯搓劫,就像你可以選擇和編輯多個(gè)游戲?qū)ο笠粯印?/em>


為了設(shè)置一個(gè)屬性瞧哟,我們首先必須在數(shù)組中找到它,為此我們可以使用ShaderGUI.FindProperty方法糟把,傳遞給它一個(gè)名稱和屬性數(shù)組绢涡。然后,我們可以通過給它的floatValue屬性賦值來調(diào)整它的值遣疯。 用一個(gè)字符串名稱和一個(gè)float值參數(shù)將其封裝在一個(gè)方便的SetProperty方法中雄可。

void SetProperty (string name, float value) {
    FindProperty(name, properties).floatValue = value;
}

設(shè)置關(guān)鍵字稍微有點(diǎn)復(fù)雜。我們將為此創(chuàng)建一個(gè)SetKeyword方法缠犀,帶有一個(gè)字符串和一個(gè)布爾參數(shù)数苫,以指示該關(guān)鍵字是應(yīng)啟用還是禁用。我們必須在所有材質(zhì)上調(diào)用EnableKeywordDisableKeyword辨液,給它們傳遞關(guān)鍵字名稱虐急。

    void SetKeyword (string keyword, bool enabled) {
        if (enabled) {
            foreach (Material m in materials) {
                m.EnableKeyword(keyword);
            }
        }
        else {
            foreach (Material m in materials) {
                m.DisableKeyword(keyword);
            }
        }
    }

讓我們?cè)賱?chuàng)建一個(gè)SetProperty變體,用于同時(shí)開關(guān)"屬性-關(guān)鍵字"滔迈。

void SetProperty (string name, string keyword, bool value) {
    SetProperty(name, value ? 1f : 0f);
    SetKeyword(keyword, value);
}

然后定義一些屬性的訪問器止吁。

bool Clipping {
    set => SetProperty("_Clipping", "_CLIPPING", value);
}

bool PremultiplyAlpha {
    set => SetProperty("_PremulAlpha", "_PREMULTIPLY_ALPHA", value);
}

BlendMode SrcBlend {
    set => SetProperty("_SrcBlend", (float)value);
}

BlendMode DstBlend {
    set => SetProperty("_DstBlend", (float)value);
}

bool ZWrite {
    set => SetProperty("_ZWrite", value ? 1f : 0f);
}

最后,渲染隊(duì)列通過分配所有材質(zhì)的RenderQueue屬性來設(shè)置燎悍。

RenderQueue RenderQueue {
    set {
        foreach (Material m in materials) {
            m.renderQueue = (int)value;
        }
    }
}


5.3 預(yù)設(shè)按鈕(Preset Buttons)

可以通過GUILayout.Button方法創(chuàng)建按鈕敬惦。傳遞給它一個(gè)標(biāo)簽,這將是一個(gè)預(yù)設(shè)的名稱谈山。 如果該方法返回true俄删,則代表它被按下。在應(yīng)用預(yù)設(shè)之前,我們應(yīng)該向編輯器注冊(cè)一個(gè)撤銷步驟畴椰,這可以通過調(diào)用RegisterPropertyChangeUndo來完成臊诊。 因?yàn)檫@段代碼對(duì)所有預(yù)設(shè)都是相同的,所以把它放在PresetButton方法中斜脂。

    bool PresetButton (string name) {
        if (GUILayout.Button(name)) {
            editor.RegisterPropertyChangeUndo(name);
            return true;
        }
        return false;
    }

我們將為每個(gè)預(yù)設(shè)創(chuàng)建一個(gè)單獨(dú)的方法抓艳,從默認(rèn)的Opaque模式開始,在激活時(shí)恰當(dāng)?shù)卦O(shè)置屬性帚戳。

void OpaquePreset () {
        if (PresetButton("Opaque")) {
            Clipping = false;
            PremultiplyAlpha = false;
            SrcBlend = BlendMode.One;
            DstBlend = BlendMode.Zero;
            ZWrite = true;
            RenderQueue = RenderQueue.Geometry;
        }
    }

第二個(gè)預(yù)設(shè)是Clipping壶硅。

void ClipPreset () {
        if (PresetButton("Clip")) {
            Clipping = true;
            PremultiplyAlpha = false;
            SrcBlend = BlendMode.One;
            DstBlend = BlendMode.Zero;
            ZWrite = true;
            RenderQueue = RenderQueue.AlphaTest;
        }
    }

第三個(gè)預(yù)設(shè)是標(biāo)準(zhǔn)的透明,它會(huì)淡出對(duì)象销斟,所以我們將其命名為Fade庐椒。

    void FadePreset () {
        if (PresetButton("Fade")) {
            Clipping = false;
            PremultiplyAlpha = false;
            SrcBlend = BlendMode.SrcAlpha;
            DstBlend = BlendMode.OneMinusSrcAlpha;
            ZWrite = false;
            RenderQueue = RenderQueue.Transparent;
        }
    }

第四種預(yù)設(shè)是Fade的變體,應(yīng)用了預(yù)乘的透明度混合蚂踊。 我們將它命名為Transparent约谈。

void TransparentPreset () {
        if (PresetButton("Transparent")) {
            Clipping = false;
            PremultiplyAlpha = true;
            SrcBlend = BlendMode.One;
            DstBlend = BlendMode.OneMinusSrcAlpha;
            ZWrite = false;
            RenderQueue = RenderQueue.Transparent;
        }
    }

OnGUI結(jié)束時(shí)調(diào)用預(yù)設(shè)方法,這樣它們就會(huì)顯示在默認(rèn)材質(zhì)面板下面犁钟。

public override void OnGUI (
        MaterialEditor materialEditor, MaterialProperty[] properties
    ) {
        …

        OpaquePreset();
        ClipPreset();
        FadePreset();
        TransparentPreset();
    }

預(yù)設(shè)按鈕

預(yù)設(shè)按鈕不會(huì)經(jīng)常使用棱诱,所以讓我們把它們放在一個(gè)折疊標(biāo)簽中。這是通過調(diào)用[EditorGUILayout](http://docs.unity3d.com/Documentation/ScriptReference/EditorGUILayout.html).Foldout來完成的涝动。它返回新的折疊狀態(tài)迈勋,我們應(yīng)該將其存儲(chǔ)在一個(gè)字段中,只有當(dāng)折疊打開時(shí)才繪制按鈕醋粟。

bool showPresets;

    …

    public override void OnGUI (
        MaterialEditor materialEditor, MaterialProperty[] properties
    ) {
        …

        EditorGUILayout.Space();
        showPresets = EditorGUILayout.Foldout(showPresets, "Presets", true);
        if (showPresets) {
            OpaquePreset();
            ClipPreset();
            FadePreset();
            TransparentPreset();
        }
    }

可以折疊的預(yù)設(shè)按鈕


5.4 無光照著色器的預(yù)設(shè)(Presets for Unlit)

我們也可以為我們的無光照著色器使用自定義著色器GUI靡菇。

Shader "Custom RP/Unlit" {
    …

    CustomEditor "CustomShaderGUI"
}

然而,激活預(yù)設(shè)將導(dǎo)致一個(gè)錯(cuò)誤米愿,因?yàn)槲覀冊(cè)噲D設(shè)置一個(gè)著色器沒有的屬性厦凤。我們可以通過調(diào)整SetProperty來防止這種情況。 讓它調(diào)用FindProperty育苟,并將false作為附加參數(shù)较鼓,表示如果沒有找到該屬性,它不應(yīng)該報(bào)告錯(cuò)誤违柏。結(jié)果將為null博烂,所以只有在不為null的情況下才設(shè)置值。

bool SetProperty (string name, float value) {
    MaterialProperty property = FindProperty(name, properties, false);
    if (property != null) {
        property.floatValue = value;
        return true;
    }
    return false;
}

然后調(diào)整SetProperty漱竖,使它只在相關(guān)屬性存在的情況下設(shè)置關(guān)鍵字禽篱。

void SetProperty (string name, string keyword, bool value) {
    if (SetProperty(name, value ? 1f : 0f)) {
        SetKeyword(keyword, value);
    }
}


5.5 非透明(No Transparency)

現(xiàn)在預(yù)設(shè)也適用于使用無光照著色器的材質(zhì),盡管透明模式在這種情況下沒有多大意義闲孤,因?yàn)橄嚓P(guān)屬性不存在谆级。 讓我們?cè)诓幌嚓P(guān)的時(shí)候隱藏這個(gè)預(yù)設(shè)。

首先讼积,添加一個(gè)返回屬性是否存在的HasProperty方法们颜。

bool HasProperty (string name) =>
        FindProperty(name, properties, false) != null;

其次,創(chuàng)建一個(gè)方便的屬性來檢查_PremultiplyAlpha是否存在刨秆。

    bool HasPremultiplyAlpha => HasProperty("_PremulAlpha");

最后剔交,通過在TransparentPreset中檢查偎快,使所有的Transparent相關(guān)預(yù)設(shè)都以該屬性為條件。

    if (HasPremultiplyAlpha && PresetButton("Transparent")) { … }

沒有透明預(yù)設(shè)的無光照材質(zhì)


下一個(gè)章節(jié)是 平行光陰影(Directional Shadows)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市瘦锹,隨后出現(xiàn)的幾起案子辫红,更是在濱河造成了極大的恐慌,老刑警劉巖贴妻,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件切油,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡名惩,警方通過查閱死者的電腦和手機(jī)澎胡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來娩鹉,“玉大人攻谁,你說我怎么就攤上這事⊥溆瑁” “怎么了戚宦?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)锈嫩。 經(jīng)常有香客問我受楼,道長(zhǎng)垦搬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任艳汽,我火速辦了婚禮猴贰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘骚灸。我一直安慰自己,他們只是感情好慌植,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布甚牲。 她就那樣靜靜地躺著,像睡著了一般蝶柿。 火紅的嫁衣襯著肌膚如雪丈钙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天交汤,我揣著相機(jī)與錄音雏赦,去河邊找鬼。 笑死芙扎,一個(gè)胖子當(dāng)著我的面吹牛星岗,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播戒洼,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼俏橘,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了圈浇?” 一聲冷哼從身側(cè)響起寥掐,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎磷蜀,沒想到半個(gè)月后召耘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡褐隆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年污它,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片庶弃。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡轨蛤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出虫埂,到底是詐尸還是另有隱情祥山,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布掉伏,位于F島的核電站缝呕,受9級(jí)特大地震影響澳窑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜供常,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一摊聋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧栈暇,春花似錦麻裁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至香缺,卻和暖如春手销,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背图张。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工锋拖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人祸轮。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓兽埃,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親适袜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子讲仰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容