unity 角度限制_Unity自定义可编程渲染管线(SRP)(九)——灯光照明
本文是自定義可渲染管線系列比較重要的章節(jié),我們將實現(xiàn)自定義可編程渲染管線對燈光照明的支持。
如果我們想創(chuàng)建一個更加逼真的場景,那么我們必須要模擬物體表面的光照現(xiàn)象。這需要提供更加復(fù)雜的shader才能實現(xiàn)。
LitShader
復(fù)制UnlitPass.hlsl文件并將其重命名為LitPass,然后修改引用保護以及頂點、片元函數(shù)的名字。我們將在后面添加燈光計算。
#ifndef CUSTOM_LIT_PASS_INCLUDED #define CUSTOM_LIT_PASS_INCLUDED…Varyings LitPassVertex (Attributes input) { … }float4 LitPassFragment (Varyings input) : SV_TARGET { … }#endif同時復(fù)制Unlit shader文件,并重命名為Lit。修改它的菜單名,引用文件和所使用的函數(shù)。讓我們也將默認顏色更改為灰色,因為在光線充足的場景中全白的表面可能顯得非常明亮。 默認情況下,unity的通用管道也使用灰色。
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}} }我們將使用一種自定義的照明方法,通過將著色器的照明模式設(shè)置為CustomLit。在Pass模塊中添加一個Tag標簽,其中包括”LightMode”=”CustomLit”。
Pass {Tags {"LightMode" = "CustomLit"}… }要渲染使用此通道的對象,我們必須將其包含在CameraRenderer中。 首先為其添加一個著色器標簽標識符。
static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit"), litShaderTagId = new ShaderTagId("CustomLit");然后將其添加到要在DrawVisibleGeometry中渲染的過程中,就像在DrawUnsupportedShaders中所做的那樣。
var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings) {enableDynamicBatching = useDynamicBatching,enableInstancing = useGPUInstancing};drawingSettings.SetShaderPassName(1, litShaderTagId);現(xiàn)在我們可以創(chuàng)建一個新的不透明材質(zhì)了,雖然現(xiàn)在它的結(jié)果跟無光照材質(zhì)一樣。
法向量
一個物體的光照結(jié)果取決于很多因素,比如物體表面與光線的相對角度。要想知道物體表面的方向,我們必須要知道物體表面的法向,它是一個單位長度的指向表面正向的向量。這個向量是頂點數(shù)據(jù)的一部分,在物體局部空間中定義,就像位置坐標一樣。所以將它添加到LitPass中的屬性中。
struct Attributes {float3 positionOS : POSITION;float3 normalOS : NORMAL;float2 baseUV : TEXCOORD0;UNITY_VERTEX_INPUT_INSTANCE_ID };光照是根據(jù)每個片段計算的,所以我們必須將法向量也添加到Varyings中。我們將在世界空間中執(zhí)行計算,因此將其命名為normalWS。
struct Varyings {float4 positionCS : SV_POSITION;float3 normalWS : VAR_NORMAL;float2 baseUV : VAR_BASE_UV;UNITY_VERTEX_INPUT_INSTANCE_ID };我們可以使用SpaceTransforms庫中的TransformObjectToWorldNormal把法向量從局部坐標轉(zhuǎn)換到世界空間。
output.positionWS = TransformObjectToWorld(input.positionOS);output.positionCS = TransformWorldToHClip(positionWS);output.normalWS = TransformObjectToWorldNormal(input.normalOS);為了驗證我們是否在LitPassFragment中得到了一個正確的法向量,我們可以使用它作為一種顏色。
base.rgb = input.normalWS;return base;負值是無法顯示的,所以它們被限制為0。
法線插值
雖然法向量在頂點程序中是單位長度的,但三角形間的線性插值會影響它們的長度。我們可以通過渲染向量長度與1之間的差值來可視化誤差,并將其放大10倍以使其更加明顯。
base.rgb = abs(length(input.normalWS) - 1.0) * 10.0;我們可以通過對LitPassFragment中的法向量進行歸一化來平滑插值失真。當(dāng)只觀察法向量時,這種差異并不是很明顯,但當(dāng)用于照明時,這種差異就更明顯了。
base.rgb = normalize(input.normalWS);表面參數(shù)
著色器中的照明是實質(zhì)模擬光線到物體表面的相互作用,這意味著我們必須跟蹤表面的屬性。現(xiàn)在我們有一個法向量和一個底色。我們可以將后者分為兩部分:RGB顏色和alpha值。我們將在幾個地方使用這些數(shù)據(jù),所以讓我們定義一個方便的表面結(jié)構(gòu)來包含所有相關(guān)數(shù)據(jù)。把它放在一個單獨的surface.hlsl文件中,并保存在ShaderLibrary文件夾下。
#ifndef CUSTOM_SURFACE_INCLUDED #define CUSTOM_SURFACE_INCLUDEDstruct Surface {float3 normal;float3 color;float alpha; };#endif然后在LitPass中,我們在Common之后將Surface.hlsl文件引入進來。這樣我們可以讓LitPass保持簡短。
#include "../ShaderLibrary/Common.hlsl" #include "../ShaderLibrary/Surface.hlsl"在LitPassFragment中定義一個Surface變量并填充它。最后的結(jié)果就是表面的顏色和alpha值。
Surface surface;surface.normal = normalize(input.normalWS);surface.color = base.rgb;surface.alpha = base.a;return float4(surface.color, surface.alpha);光照計算
為了計算實際的照明,我們將創(chuàng)建一個帶有表面屬性參數(shù)的GetLighting函數(shù)。首先讓它返回曲面法線的Y分量。由于這是照明功能,我們將把它放在一個單獨的Lighting.HLSL文件中。
#ifndef CUSTOM_LIGHTING_INCLUDED #define CUSTOM_LIGHTING_INCLUDEDfloat3 GetLighting (Surface surface) {return surface.normal.y; }#endif在LitPass中,我們在引用 surface后引用它,因為光照依賴于它。
#include "../ShaderLibrary/Surface.hlsl" #include "../ShaderLibrary/Lighting.hlsl"現(xiàn)在我們可以在LitPassFragment中獲取照明,并將其用于fragment的RGB部分。
float3 color = GetLighting(surface);return float4(color, surface.alpha);光源
要進行光照計算,我們還需要知道光的特性。在本教程中,我們將只討論平行光。平行光表示光源離得很遠,所以它的位置無關(guān)緊要,只與它的方向有關(guān)。它可以模擬地球上的太陽光和其它單向入射光。
這里我們將使用一個結(jié)構(gòu)體來存儲光源數(shù)據(jù),現(xiàn)在我們只需要一個顏色和一個方向就足夠了。把它放在一個單獨的light.hlsl文件。還定義一個GetDirectionalLight函數(shù),該函數(shù)返回配置的方向燈。光源默認顏色是白色和方向是向上,與我們當(dāng)前使用的光線數(shù)據(jù)相匹配。請注意,光的方向這里被定義為光射入的方向,而不是光射出的方向。
#ifndef CUSTOM_LIGHT_INCLUDED #define CUSTOM_LIGHT_INCLUDEDstruct 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之前引用這個文件
#include "../ShaderLibrary/Light.hlsl" #include "../ShaderLibrary/Lighting.hlsl"光照函數(shù)
為lighting添加一個GetIncomingLight函數(shù),計算給定表面和入射光線的反射光。對于任意方向光的反射結(jié)果,我們需要取表面法線和方向的點積再乘以光的顏色。
float3 GetIncomingLight (Surface surface, Light light) {return dot(surface.normal, light.direction) * light.color; }但這個結(jié)果只在表面朝向光源的時候是正確的。當(dāng)點積是負數(shù)時,我們必須使它等于零,這可以通過saturate函數(shù)來實現(xiàn)。
float3 IncomingLight (Surface surface, Light light) {return saturate(dot(surface.normal, light.direction)) * light.color; }向GPU發(fā)送光源數(shù)據(jù)
我們應(yīng)該使用當(dāng)前場景的燈光,而不是總是使用來自我們設(shè)置的默認光。默認場景有一個方向燈,代表太陽,顏色稍微偏黃——fff4d6十六進制——繞X軸旋轉(zhuǎn)50°,繞Y軸旋轉(zhuǎn)30°。如果這樣的光源不存在則創(chuàng)造一個。
為了使光源數(shù)據(jù)在著色器中可訪問,我們必須為它創(chuàng)建統(tǒng)一值,就像著色器屬性一樣。在本例中,我們將定義兩個float3類型的向量:_DirectionalLightColor和_DirectionalLightDirection。將它們放到定義在Light頂部的_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)在,我們的RP必須將光源數(shù)據(jù)發(fā)送到GPU。 我們將為此創(chuàng)建一個新的Lighting類。 它的工作方式與CameraRenderer相似,但適用于燈光照明。給它提供一個帶有context參數(shù)的公共Setup方法,在該方法中它調(diào)用一個單獨的SetupDirectionalLight方法。
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 () {} }追蹤兩個著色器屬性的標識符。
static int dirLightColorId = Shader.PropertyToID("_DirectionalLightColor"),dirLightDirectionId = Shader.PropertyToID("_DirectionalLightDirection");我們可以通過RenderSettings.sun訪問場景的主光源。 默認情況下,這使我們得到主光源,還可以通過Window /Rendering /Lighting Settings顯式配置它。 使用CommandBuffer.SetGlobalVector將光源數(shù)據(jù)發(fā)送到GPU。 顏色是光源在線性空間中的顏色,而方向是光源的正向向量取反。
void SetupDirectionalLight () {Light light = RenderSettings.sun;buffer.SetGlobalVector(dirLightColorId, light.color.linear);buffer.SetGlobalVector(dirLightDirectionId, -light.transform.forward); }光源的顏色屬性是其配置的顏色,但是光源也具有單獨的強度因子。 最終的顏色是兩者的乘積。
buffer.SetGlobalVector(dirLightColorId, light.color.linear * light.intensity );為CameraRenderer提供一個Lighting實例,并在繪制幾何圖形之前使用它來設(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();}在進行剔除時,Unity還會找出哪些光源會影響相機可見的空間。 我們可以依靠這些信息而不是全局的sun光源。 為此,Lighting需要訪問剔除結(jié)果,我們?yōu)镾etup添加一個剔除結(jié)果的輸入?yún)?shù),并將其存儲在字段中以方便使用。然后,我們可以支持多個光源,讓我們使用新的SetupLights方法來替換SetupDirectionalLight。
CullingResults cullingResults;public void Setup (ScriptableRenderContext context, CullingResults cullingResults) {this.cullingResults = cullingResults;buffer.BeginSample(bufferName);//SetupDirectionalLight();SetupLights();…}void SetupLights () {}在CameraRenderer.Render中調(diào)用Setup時,將剔除結(jié)果添加為參數(shù)。
lighting.Setup(context, cullingResults);現(xiàn)在Lighting.SetupLights可以通過剔除結(jié)果的visibleLights屬性檢索所需的數(shù)據(jù)。 它以的Unity.Collections.NativeArray的形式存在。
using Unity.Collections; using UnityEngine; using UnityEngine.Rendering;public class Lighting {…void SetupLights () {NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;}… }多光源照射
使用可見光數(shù)據(jù)可以支持多個定向光,但是我們必須將所有這些光的數(shù)據(jù)發(fā)送到GPU。 因此,我們將使用兩個Vector4數(shù)組,并用這兩個數(shù)組來存儲光源信息。讓我們將最大值設(shè)置為四個,這對于大多數(shù)場景來說應(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];將索引和VisibleLight參數(shù)傳遞給SetupDirectionalLight。 用提供的索引設(shè)置顏色和光照方向。在這種情況下,最終顏色是通過VisibleLight.finalColor屬性提供的。 可以通過VisibleLight.localToWorldMatrix屬性找到前向矢量。 它是矩陣的第三列,必須再次取反。
void SetupDirectionalLight (int index, VisibleLight visibleLight) {dirLightColors[index] = visibleLight.finalColor;dirLightDirections[index] = -visibleLight.localToWorldMatrix.GetColumn(2);}最終顏色已經(jīng)應(yīng)用了光源的強度,但是默認情況下Unity不會將其轉(zhuǎn)換為線性空間。 我們必須將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中的所有可見光,并為每個元素調(diào)用SetupDirectionalLight。 然后在緩沖區(qū)上調(diào)用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);但是我們最多只支持四個定向燈,因此當(dāng)達到最大值時,我們應(yīng)該中止循環(huán)。 讓我們添加一個與循環(huán)的迭代器分開的索引。
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īng)該忽略其他光源類型。 我們可以通過檢查可見光的lightType屬性是否等于LightType.Directional來做到這一點。
VisibleLight visibleLight = visibleLights[i]; if (visibleLight.lightType == LightType.Directional) {SetupDirectionalLight(dirLightCount++, visibleLight);if (dirLightCount >= maxDirLightCount) {break;} }這樣雖然可行,但是VisibleLight結(jié)構(gòu)相當(dāng)大。 理想情況下,我們只從本地數(shù)組中檢索一次,并且也不要將其作為常規(guī)參數(shù)傳遞給SetupDirectionalLight,因為那樣會對其進行復(fù)制。 我們可以通過引用傳遞參數(shù)。
SetupDirectionalLight(dirLightCount++, ref visibleLight);這要求我們也將參數(shù)定義為引用。
void SetupDirectionalLight (int index, ref VisibleLight visibleLight) { … }Shader循環(huán)
在Light中調(diào)整_CustomLight緩沖區(qū),使其與我們的新數(shù)據(jù)格式匹配。 在這種情況下,我們將顯式使用float4作為數(shù)組類型。 著色器中的數(shù)組大小固定,無法調(diào)整大小。 確保使用與Lighting中定義的最大值相同。
#define MAX_DIRECTIONAL_LIGHT_COUNT 4CBUFFER_START(_CustomLight)//float4 _DirectionalLightColor;//float4 _DirectionalLightDirection;int _DirectionalLightCount;float4 _DirectionalLightColors[MAX_DIRECTIONAL_LIGHT_COUNT];float4 _DirectionalLightDirections[MAX_DIRECTIONAL_LIGHT_COUNT]; CBUFFER_END添加一個函數(shù)以獲取定向光照計數(shù)并調(diào)整GetDirectionalLight,以便它檢索特定光照索引的數(shù)據(jù)。
int GetDirectionalLightCount () {return _DirectionalLightCount; }Light GetDirectionalLight (int index) {Light light;light.color = _DirectionalLightColors[index].rgb;light.direction = _DirectionalLightDirections[index].xyz;return light; }然后調(diào)整曲面的GetLight,使其使用for循環(huá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)在,我們的著色器最多支持四個平行光。 通常只需要一個平行光來表示太陽或月球,但是也可能存在行星上有多個太陽的場景。 定向燈也可以用于近似多個大型照明設(shè)備,例如大型體育場的照明設(shè)備。
如果游戲始終只有一個平行光,那么你可以擺脫循環(huán),或者制作多個著色器變體。 但是對于本教程,我們?yōu)榱撕唵螆猿忠粋€通用循環(huán)。 最好的性能總是通過剔除不需要的內(nèi)容來實現(xiàn)的。
Shader的目標等級
著色器曾經(jīng)遇到過長度可變的循環(huán)問題,但是現(xiàn)在的GPU可以毫無問題地處理它們,尤其是當(dāng)繪制的所有片段以相同的方式遍歷同一數(shù)據(jù)時。但是,默認情況下,OpenGL ES 2.0和WebGL 1.0圖形API不能處理此類循環(huán)。我們可以通過合并硬編碼的最大值來使其工作,例如,使GetDirectionalLight返回min(_DirectionalLightCount,MAX_DIRECTIONAL_LIGHT_COUNT)。這樣就可以展開循環(huán),將其變成一系列條件代碼塊。不幸的是,生成的著色器代碼一團糟,性能下降得很快。在非常老式的硬件上,所有代碼塊都將始終執(zhí)行,它們的貢獻可通過條件分配來控制。盡管我們可以進行這項工作,但它會使代碼更加復(fù)雜,因為我們還必須進行其他調(diào)整。因此,為了簡化起見,我選擇忽略這些限制并在構(gòu)建中關(guān)閉WebGL 1.0和OpenGL ES 2.0支持。他們不支持線性照明。我們還可以通過#pragma target 3.5指令將著色器傳遞的目標級別提高到3.5,從而避免為它們編譯OpenGL ES 2.0著色器變體。讓我們保持一致,并為兩個著色器執(zhí)行此操作。
HLSLPROGRAM#pragma target 3.5…ENDHLSL總結(jié)
以上是生活随笔為你收集整理的unity 角度限制_Unity自定义可编程渲染管线(SRP)(九)——灯光照明的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: inovance变频器说明书参数设置_变
- 下一篇: 扑克牌排序_巧用扑克牌搞定孩子的数学思维