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.
(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ì)相同备恤。
1.2 法向量(Normal Vectors)
一個(gè)物體被照亮的程度取決于多種因素稿饰,包括光與物體表面之間的相對(duì)角度。為了知道表面的方向露泊,我們需要訪問表面的法線喉镰,它是一個(gè)垂直于表面的單位長(zhǎng)度的向量。這個(gè)向量是頂點(diǎn)數(shù)據(jù)的一部分惭笑,在對(duì)象空間中定義侣姆,就像位置一樣。 所以把它添加到LitPass
的Attributes
中沉噩。
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.hlsl
的TransformObjectToWorldNormal
方法在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è)空間,一切仍將保持不變昧诱。
在LitPass
中Common
之后引用它晓淀,這樣我們就可以保持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;
}
反照率(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é)果就是它們夾角的余弦值航厚。
·
但這只在表面朝向光線時(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();
}
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)用buffer
的SetGlobalInt
和SetGlobalVectorArray
將數(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ū)別嗎滓窍?
·
他們都是語義的別名卖词,使用 rgba和 xyzw是等價(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;
}
現(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
將屬性添加到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.hlsl
和Lighting.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;
在現(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è)帶有Surface
、BRDF
數(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)。 您可以查看 URP的 Lighting.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);
}
我們現(xiàn)在實(shí)現(xiàn)了高光反射反射邻梆,它為我們的表面添加了高光部分守伸。對(duì)于完全粗糙的表面,高光模擬漫反射浦妄。光滑的表面得到更集中的亮光尼摹。一個(gè)完全光滑的表面會(huì)有一個(gè)我們看不到的無限小的高光。需要一些散射才能使它可見剂娄。
由于能量守恒蠢涝,對(duì)于光滑的表面,因?yàn)榇蟛糠值竭_(dá)表面的光都聚焦了宜咒,高光可以變得非常明亮惠赫,因此,我們才會(huì)看到更多的光故黑,而不是由于高光部分可見的漫反射儿咱。你可以通過大幅縮小最終輸出的顏色來驗(yàn)證這一點(diǎn)。
你也可以通過使用白色以外的基色场晶,來驗(yàn)證金屬材質(zhì)會(huì)影響高光部分的顏色混埠,而非金屬材質(zhì)不會(huì)。
我們現(xiàn)在有了可靠的直接照明的功能诗轻,盡管目前的效果還太暗钳宪,尤其是對(duì)金屬材質(zhì),因?yàn)槲覀冞€不支持環(huán)境反射等扳炬。在這一點(diǎn)上吏颖,一個(gè)標(biāo)準(zhǔn)的黑色環(huán)境會(huì)比默認(rèn)的天空框更真實(shí),但這會(huì)讓我們的對(duì)象更難以觀察到恨樟。添加更多的燈光也可以做到半醉。
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ì)衬吆。
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;
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
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
翎冲、 UnityEngine
和UnityEngine.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)用EnableKeyword
或DisableKeyword
辨液,給它們傳遞關(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è)按鈕不會(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();
}
}
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")) { … }