unity自定义着色器基础

这些内置渲染管线的着色器示例演示了编写自定义着色器的基础知识,并涵盖了常见的用例。

有关编写着色器的信息,请参阅编写着色器。

设置场景

第一步是创建一些用于测试着色器的对象。在主菜单中选择 Game Object > 3D Object > Capsule。然后,调整摄像机位置,使其显示胶囊体。在 Hierarchy 视图中双击胶囊体 (Capsule) 以将 Scene 视图聚焦在该胶囊体上,然后选择主摄像机 (Main Camera) 对象,并单击主菜单中的 Game object > Align with View

在 Project 视图中,从菜单中选择 Create > Material,创建新的材质。Project 视图中将显示一个名为 New Material 的新材质。

创建着色器

现在以类似方式创建一个新的着色器资源。在 Project 视图中,从菜单中选择 Create > Shader > Unlit Shader。 随后将创建一个基本着色器,该着色器仅显示一个纹理,无任何光照。

Create > Shader 菜单中的其他条目将创建基本要素着色器 或其他类型,例如,基本表面着色器。

链接网格、材质和着色器

通过材质的检视面板使材质使用着色器,或者在 Project 视图中将着色器资源拖动到材质资源上。材质检视面板在使用此着色器时将显示白色球体。

现在将材质拖动到 Scene 或 Hierarchy 视图中的网格对象上。或者,选择对象,并在检视面板中使其使用网格渲染器 (Mesh Renderer) 组件的 Materials 字段中的材质。

完整这些设置后,现在可以开始查看着色器代码,并会在 Scene 视图中看到对胶囊体上着色器所做的更改的结果。

着色器的主要部分

要开始检查着色器的代码,请在 Project 视图中双击着色器资源。着色器代码将在脚本编辑器(MonoDevelop 或 Visual Studio)中打开。

着色器最开始的代码如下:

Shader "Unlit/NewUnlitShader"
{Properties{_MainTex ("Texture", 2D) = "white" {}}SubShader{Tags { "RenderType"="Opaque" }LOD 100Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag// 使雾生效#pragma multi_compile_fog#include "UnityCG.cginc"struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f{float2 uv : TEXCOORD0;UNITY_FOG_COORDS(1)float4 vertex : SV_POSITION;};sampler2D _MainTex;float4 _MainTex_ST;v2f vert (appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = TRANSFORM_TEX(v.uv, _MainTex);UNITY_TRANSFER_FOG(o,o.vertex);return o;}fixed4 frag (v2f i) : SV_Target{// 对纹理进行采样fixed4 col = tex2D(_MainTex, i.uv);// 应用雾UNITY_APPLY_FOG(i.fogCoord, col);return col;}ENDCG}}
}

这个初始着色器看上去不是那么简单!但不要担心, 我们将逐步介绍每个部分。

让我们看看这个简单着色器的主要部分。

Shader

Shader 命令包含一个带有 着色器名称的字符串。在材质检视面板中选择着色器时,可以使用正斜杠字符“/”将着色器放置在子菜单中。

属性

Properties 代码块包含着色器变量 (纹理、颜色等),这些变量将保存为材质的一部分, 并显示在材质检视面板中。在我们的无光照着色器模板中, 声明了单个纹理属性。

SubShader

一个着色器 (Shader) 可以包含一个或多个子着色器 (SubShader),主要 用于实现不同 GPU 功能的着色器。 在本教程中,我们并不太关心这一点,所以我们 所有的着色器都只包含一个子着色器。

Pass

每个子着色器由多个通道组成,每个通道代表 为使用着色器材质渲染的同一对象执行 顶点和片元代码。 许多简单的着色器只使用一个通道,但与光照交互的 着色器可能需要更多通道(有关详细信息,请参阅 光照管线)。通道内部的 命令通常设置固定函数状态,例如 混合模式。

CGPROGRAM ..ENDCG

这些关键字包裹着顶点和片元着色器中的 HLSL 代码 部分。通常,大多数相关代码都包含在这里。有关详细信息,请参阅 顶点和片元着色器。

简单的无光照着色器

无光照着色器模板比显示带纹理的对象 绝对需要更多的代码。例如, 它支持雾效,以及材质中的纹理平铺/偏移字段。 让我们最大程度简化着色器,并添加更多注释:

Shader "Unlit/SimpleUnlitTexturedShader"
{Properties{// 我们已删除对纹理平铺/偏移的支持,// 因此请让它们不要显示在材质检视面板中[NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}}SubShader{Pass{CGPROGRAM// 使用 "vert" 函数作为顶点着色器#pragma vertex vert// 使用 "frag" 函数作为像素(片元)着色器#pragma fragment frag// 顶点着色器输入struct appdata{float4 vertex : POSITION; // 顶点位置float2 uv : TEXCOORD0; // 纹理坐标};// 顶点着色器输出("顶点到片元")struct v2f{float2 uv : TEXCOORD0; // 纹理坐标float4 vertex : SV_POSITION; // 裁剪空间位置};// 顶点着色器v2f vert (appdata v){v2f o;// 将位置转换为裁剪空间//(乘以模型*视图*投影矩阵)o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);// 仅传递纹理坐标o.uv = v.uv;return o;}// 我们将进行采样的纹理sampler2D _MainTex;// 像素着色器;返回低精度("fixed4" 类型)// 颜色("SV_Target" 语义)fixed4 frag (v2f i) : SV_Target{// 对纹理进行采样并将其返回fixed4 col = tex2D(_MainTex, i.uv);return col;}ENDCG}}
}

顶点着色器是对 3D 模型的每个顶点运行的程序。通常情况下,它并不做任何特别有趣的事情。这里我们只是将顶点位置从对象空间转换为所谓的“裁剪空间”(由 GPU 用于栅格化屏幕上的对象)。我们还传递未修改的输入纹理坐标;我们需要该坐标来对片元着色器中的纹理进行采样。

片元着色器是对屏幕上对象占据的每个像素运行的程序,通常用于计算和输出每个像素的颜色。通常,屏幕上有数百万个像素,并且片元着色器将 针对所有像素执行!优化片元着色器是整体游戏性能优化工作的重要组成部分。

一些变量或函数定义后跟一个语义指示符,例如 : POSITION 或 : SV_Target。这些语义指示符将这些变量的“含义”传达给 GPU。有关详细信息,请参阅着色器语义页面。

用于具有良好纹理的良好模型时,我们的简单着色器看起来非常好!

更简单的单色着色器

让我们进一步简化着色器:我们将制作一个用单一颜色绘制整个对象的 着色器。这并不是很有用,但别忘了,我们是在学习。

Shader "Unlit/SingleColor"
{Properties{// 材质检视面板的颜色属性,默认为白色_Color ("Main Color", Color) = (1,1,1,1)}SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag// 顶点着色器// 这次不使用 "appdata" 结构,仅手动拼写输入,// 并且不返回 v2f 结构,同样仅返回单个输出// float4 裁剪位置float4 vert (float4 vertex : POSITION) : SV_POSITION{return mul(UNITY_MATRIX_MVP, vertex);}// 来自材质的颜色fixed4 _Color;// 像素着色器,无需输入fixed4 frag () : SV_Target{return _Color; // 仅将其返回}ENDCG}}
}

这次,着色器函数只是手动拼出输入,而不是使用输入结构 (appdata) 和输出结构 (v2f)。两种方式都有效,选择使用哪种方式取决于您编写代码的风格和偏好。

使用网格法线,轻松获利

让我们继续了解在世界空间中显示网格法线的着色器。言归正传:

Shader "Unlit/WorldSpaceNormals"
{// 这次没有 Properties 代码块!SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag// 包含 UnityObjectToWorldNormal helper 函数的 include 文件#include "UnityCG.cginc"struct v2f {// 我们将输出世界空间法线作为常规 ("texcoord") 插值器之一half3 worldNormal : TEXCOORD0;float4 pos : SV_POSITION;};// 顶点着色器:将对象空间法线也作为输入v2f vert (float4 vertex : POSITION, float3 normal : NORMAL){v2f o;o.pos = UnityObjectToClipPos(vertex);// UnityCG.cginc 文件包含将法线从对象变换到// 世界空间的函数,请使用该函数o.worldNormal = UnityObjectToWorldNormal(normal);return o;}fixed4 frag (v2f i) : SV_Target{fixed4 c = 0;// 法线是具有 xyz 分量的 3D 矢量;处于 -1..1// 范围。要将其显示为颜色,请将此范围设置为 0..1// 并放入红色、绿色、蓝色分量c.rgb = i.worldNormal*0.5+0.5;return c;}ENDCG}}
}

除了产生漂亮的颜色外,法线还用于各种图形效果:光照、反射、轮廓等。

在上面的着色器中,我们开始使用 Unity 的内置着色器 include 文件之一。 这里使用的 UnityCG.cginc 包含一个方便的函数 UnityObjectToWorldNormal。我们还使用了实用函数 UnityObjectToClipPos,该函数将顶点从对象空间转换为屏幕。这使得代码更易于阅读,并且在某些情况下更有效。

在所谓的“插值器”(有时称为“变化”)中,我们已经看到数据可从顶点传入片元着色器。在 HLSL 着色语言中,它们通常用 TEXCOORDn 语义进行标记,其中每一个最多可以是一个 4 分量矢量(有关详细信息,请参阅语义页面)。

此外,我们学习了如何使用一种简单的技术将标准化矢量(在 –1.0 到 +1.0 范围内)可视化为颜色:只需将它们乘以二分之一并加二分之一。有关更多顶点数据可视化示例,请参阅可视化顶点数据。

使用世界空间法线的环境反射

如果在场景中使用天空盒作为反射源(请参阅 Lighting 窗口), 基本上会创建一个包含天空盒数据的“默认”反射探针。 反射探针内部是立方体贴图纹理;我们将扩展上面的世界空间法线着色器来对其进行深入了解。

代码现在开始变得有点复杂。当然,如果您希望着色器能够自动处理光源、阴影、反射以及光照系统的其余部分,使用表面着色器会容易得多。此示例旨在向您展示如何以“手动”方式使用光照系统的各个部分。

Shader "Unlit/SkyReflection"
{SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f {half3 worldRefl : TEXCOORD0;float4 pos : SV_POSITION;};v2f vert (float4 vertex : POSITION, float3 normal : NORMAL){v2f o;o.pos = UnityObjectToClipPos(vertex);// 计算顶点的世界空间位置float3 worldPos = mul(_Object2World, vertex).xyz;// 计算世界空间视图方向float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));// 世界空间法线float3 worldNormal = UnityObjectToWorldNormal(normal);// 世界空间反射矢量o.worldRefl = reflect(-worldViewDir, worldNormal);return o;}fixed4 frag (v2f i) : SV_Target{// 使用反射矢量对默认反射立方体贴图进行采样half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.worldRefl);// 将立方体贴图数据解码为实际颜色half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);// 将其输出!fixed4 c = 0;c.rgb = skyColor;return c;}ENDCG}}
}

以上示例使用了内置着色器 include 文件中的部分内容:

  • 来自内置着色器变量的 unity_SpecCube0unity_SpecCube0_HDRObject2World 和 UNITY_MATRIX_MVPunity_SpecCube0 包含激活的反射探针的数据。
  • UNITY_SAMPLE_TEXCUBE 是一个用于采样立方体贴图的内置宏。通常使用标准 HLSL 语法声明和 使用大多数常规立方体贴图(samplerCUBE 和 texCUBE),但 Unity 中的反射探针立方体贴图以特殊方式声明以节省采样器字段。如果您不知道这是什么,请不要担心,只需要知道:要使用 unity_SpecCube0 立方体贴图,必须使用 UNITY_SAMPLE_TEXCUBE 宏。
  • UnityCG.cginc 中的 UnityWorldSpaceViewDir 函数以及来自同一文件的 DecodeHDR 函数。后者用于从反射探针数据中获取实际颜色(因为 Unity 以特殊编码方式存储反射探针立方体贴图)。
  • reflect 只是一个内置的 HLSL 函数,用于计算给定法线周围的矢量反射。
使用法线贴图的环境反射

通常,法线贴图用于在对象上创建其他细节,而无需创建其他几何体。让我们看看如何使用法线贴图纹理制作发射环境的着色器。

现在开始真正涉及到了数学知识,所以我们将分几步完成。在上面的着色器中,反射 方向是按每个顶点计算的(在顶点着色器中),片元着色器仅执行反射探针 立方体贴图查找。然而,一旦我们开始使用法线贴图,表面法线本身需要按每个像素计算,这意味着我们还必须按每个像素计算环境的反射方式!

首先,让我们重写上面的着色器来实现相同效果,只是我们将一些计算移到片元着色器,从而按每个像素进行计算:

Shader "Unlit/SkyReflection Per Pixel"
{SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f {float3 worldPos : TEXCOORD0;half3 worldNormal : TEXCOORD1;float4 pos : SV_POSITION;};v2f vert (float4 vertex : POSITION, float3 normal : NORMAL){v2f o;o.pos = UnityObjectToClipPos(vertex);o.worldPos = mul(_Object2World, vertex).xyz;o.worldNormal = UnityObjectToWorldNormal(normal);return o;}fixed4 frag (v2f i) : SV_Target{// 计算视图方向和反射矢量// 此处为每像素计算half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));half3 worldRefl = reflect(-worldViewDir, i.worldNormal);// 与在先前的着色器中相同half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);fixed4 c = 0;c.rgb = skyColor;return c;}ENDCG}}
}

这里并没有带给我们太多不同:着色器看起来完全相同,只是它现在运行得更慢,因为它对屏幕上的每个像素进行更多的计算,而不是仅对每个模型的顶点进行计算。但是,我们很快就会真正需要这些计算。更高的图形保真度通常需要更复杂的着色器。

我们现在也要学习新东西,即所谓的“切线空间”。法线贴图纹理通常在坐标空间中表示,可将其视为“跟随模型表面”。在我们的着色器中,我们需要知道切线空间基矢量,从纹理中读取法线矢量,将其转换为世界空间,然后通过上面的着色器 进行所有数学运算。我们行动吧!

Shader "Unlit/SkyReflection Per Pixel"
{Properties {// 材质上的法线贴图纹理,// 默认为虚拟的 "平面表面" 法线贴图_BumpMap("Normal Map", 2D) = "bump" {}}SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f {float3 worldPos : TEXCOORD0;// 这三个矢量将保持一个 3x3 旋转矩阵,// 此矩阵进行从切线到世界空间的转换half3 tspace0 : TEXCOORD1; // tangent.x, bitangent.x, normal.xhalf3 tspace1 : TEXCOORD2; // tangent.y, bitangent.y, normal.yhalf3 tspace2 : TEXCOORD3; // tangent.z, bitangent.z, normal.z// 法线贴图的纹理坐标float2 uv : TEXCOORD4;float4 pos : SV_POSITION;};// 顶点着色器现在还需要每顶点切线矢量。// 在 Unity 中,切线为 4D 矢量,其中使用 .w 分量// 指示双切线矢量的方向。// 我们还需要纹理坐标。v2f vert (float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0){v2f o;o.pos = UnityObjectToClipPos(vertex);o.worldPos = mul(_Object2World, vertex).xyz;half3 wNormal = UnityObjectToWorldNormal(normal);half3 wTangent = UnityObjectToWorldDir(tangent.xyz);// 从法线和切线的交叉积计算双切线half tangentSign = tangent.w * unity_WorldTransformParams.w;half3 wBitangent = cross(wNormal, wTangent) * tangentSign;// 输出切线空间矩阵o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);o.uv = uv;return o;}// 来自着色器属性的法线贴图纹理sampler2D _BumpMap;fixed4 frag (v2f i) : SV_Target{// 对法线贴图进行采样,并根据 Unity 编码进行解码half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));// 将法线从切线变换到世界空间half3 worldNormal;worldNormal.x = dot(i.tspace0, tnormal);worldNormal.y = dot(i.tspace1, tnormal);worldNormal.z = dot(i.tspace2, tnormal);// 与在先前的着色器中处于相同的位置half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));half3 worldRefl = reflect(-worldViewDir, worldNormal);half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);fixed4 c = 0;c.rgb = skyColor;return c;}ENDCG}}
}

哎呦,这个非常复杂。但是,看看法线贴图反射!

添加更多纹理

让我们为上面的法线贴图、天空反射着色器添加更多纹理。我们将添加基础颜色纹理(如第一个无光照的示例中所示)和遮挡贴图来使洞穴变暗。

Shader "Unlit/More Textures"
{Properties {// 我们将在材质中使用三种纹理_MainTex("Base texture", 2D) = "white" {}_OcclusionMap("Occlusion", 2D) = "white" {}_BumpMap("Normal Map", 2D) = "bump" {}}SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"// 与在先前的着色器中完全相同struct v2f {float3 worldPos : TEXCOORD0;half3 tspace0 : TEXCOORD1;half3 tspace1 : TEXCOORD2;half3 tspace2 : TEXCOORD3;float2 uv : TEXCOORD4;float4 pos : SV_POSITION;};v2f vert (float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0){v2f o;o.pos = UnityObjectToClipPos(vertex);o.worldPos = mul(_Object2World, vertex).xyz;half3 wNormal = UnityObjectToWorldNormal(normal);half3 wTangent = UnityObjectToWorldDir(tangent.xyz);half tangentSign = tangent.w * unity_WorldTransformParams.w;half3 wBitangent = cross(wNormal, wTangent) * tangentSign;o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);o.uv = uv;return o;}// 来自着色器属性的纹理sampler2D _MainTex;sampler2D _OcclusionMap;sampler2D _BumpMap;fixed4 frag (v2f i) : SV_Target{// 与在先前的着色器中相同...half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));half3 worldNormal;worldNormal.x = dot(i.tspace0, tnormal);worldNormal.y = dot(i.tspace1, tnormal);worldNormal.z = dot(i.tspace2, tnormal);half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));half3 worldRefl = reflect(-worldViewDir, worldNormal);half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);                fixed4 c = 0;c.rgb = skyColor;// 使用基本纹理和遮挡贴图调制天空颜色fixed3 baseColor = tex2D(_MainTex, i.uv).rgb;fixed occlusion = tex2D(_OcclusionMap, i.uv).r;c.rgb *= baseColor;c.rgb *= occlusion;return c;}ENDCG}}
}

气球猫看起来很不错!

纹理化着色器示例

程序化棋盘图案

下面的着色器根据网格的纹理坐标输出棋盘图案:

Shader "Unlit/Checkerboard"
{Properties{_Density ("Density", Range(2,50)) = 30}SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f{float2 uv : TEXCOORD0;float4 vertex : SV_POSITION;};float _Density;v2f vert (float4 pos : POSITION, float2 uv : TEXCOORD0){v2f o;o.vertex = UnityObjectToClipPos(pos);o.uv = uv * _Density;return o;}fixed4 frag (v2f i) : SV_Target{float2 c = i.uv;c = floor(c) / 2;float checker = frac(c.x + c.y) * 2;return checker;}ENDCG}}
}

Properties 代码块中的密度滑动条控制棋盘的密集程度。在顶点着色器中,网格 UV 与密度值相乘,使它们从 0 到 1 的范围变为 0 到密度的范围。假设密度设置为 30,这将使片元着色器中的 i.uv 输入包含 0 到 30 的浮点值,对应于正在渲染的网格的各个位置。

然后,片元着色器代码仅使用 HLSL 的内置 floor 函数获取输入坐标的整数部分,并将其除以 2。回想一下,输入坐标是从 0 到 30 的数字;这使得它们都被“量化”为 0、0.5、1、1.5、2、2.5 等等的值。输入坐标的 x 和 y 分量都完成了此操作。

接下来,我们将这些 x 和 y 坐标相加(每个坐标的可能值只有 0、0.5、1、1.5 等等),并且只使用另一个内置的 HLSL 函数 frac 来获取小数部分。结果只能是 0.0 或 0.5。然后,我们将它乘以 2 使其为 0.0 或 1.0,并输出为颜色(这分别产生黑色或白色)。

三面纹理

对于复杂网格或程序化网格,不使用常规 UV 坐标对它们进行纹理化,有时只需在三个主方向将纹理“投影”到对象上即可。这称为“三面”纹理。这个想法是使用表面法线来加权三个纹理方向。下面是着色器:

Shader "Unlit/Triplanar"
{Properties{_MainTex ("Texture", 2D) = "white" {}_Tiling ("Tiling", Float) = 1.0_OcclusionMap("Occlusion", 2D) = "white" {}}SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f{half3 objNormal : TEXCOORD0;float3 coords : TEXCOORD1;float2 uv : TEXCOORD2;float4 pos : SV_POSITION;};float _Tiling;v2f vert (float4 pos : POSITION, float3 normal : NORMAL, float2 uv : TEXCOORD0){v2f o;o.pos = UnityObjectToClipPos(pos);o.coords = pos.xyz * _Tiling;o.objNormal = normal;o.uv = uv;return o;}sampler2D _MainTex;sampler2D _OcclusionMap;fixed4 frag (v2f i) : SV_Target{// 使用法线的绝对值作为纹理权重half3 blend = abs(i.objNormal);// 确保权重之和为 1(除以 x+y+z 之和)blend /= dot(blend,1.0);// 针对 x、y、z 轴读取三个纹理投影fixed4 cx = tex2D(_MainTex, i.coords.yz);fixed4 cy = tex2D(_MainTex, i.coords.xz);fixed4 cz = tex2D(_MainTex, i.coords.xy);// 根据权重混合纹理fixed4 c = cx * blend.x + cy * blend.y + cz * blend.z;// 根据常规遮挡贴图进行调制c *= tex2D(_OcclusionMap, i.uv);return c;}ENDCG}}
}

光照计算

通常,当您需要适用于 Unity 光照管线的着色器时, 可以编写表面着色器。这样会为您完成大部分“繁重的工作”, 您的着色器代码只需要定义表面属性。

但在某些情况下,您希望绕过标准表面着色器路径; 或者是出于性能原因,您只想支持整个光照管线的某个有限子集, 或者您想要进行“标准光照”以外的自定义。以下示例 将说明如何从手动编写的顶点和片元着色器获取光照数据。 查看表面着色器生成的代码(通过着色器检视面板)也是一种 很好的学习资源。

简单的漫射光照

我们需要做的第一件事就是指出我们的着色器确实需要传递给它的光照信息。Unity 的渲染管线支持各种渲染方式;这里我们将使用默认的前向渲染。

我们首先只支持一个方向光。Unity 中的前向渲染的工作方式是在一个名为 ForwardBase 的单个通道中渲染主方向光、环境光、光照贴图和反射。在着色器中,通过添加以下通道标签来指示这一情况:Tags {“LightMode”=“ForwardBase”}。这将使方向光数据通过一些内置变量传入着色器。

以下着色器为每个顶点计算简单漫射光照,并使用单个主纹理:

Shader "Lit/Simple Diffuse"
{Properties{[NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}}SubShader{Pass{// 指示我们的通道是前向渲染管线中的// "基础" 通道。它可设置环境光和主// 方向光数据;光线方向为 _WorldSpaceLightPos0// 而颜色为 _LightColor0Tags {"LightMode"="ForwardBase"}CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc" // 对于 UnityObjectToWorldNormal#include "UnityLightingCommon.cginc" // 对于 _LightColor0struct v2f{float2 uv : TEXCOORD0;fixed4 diff : COLOR0; // 漫射光照颜色float4 vertex : SV_POSITION;};v2f vert (appdata_base v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = v.texcoord;// 在世界空间中获取顶点法线half3 worldNormal = UnityObjectToWorldNormal(v.normal);// 标准漫射(兰伯特)光照的法线和// 光线方向之间的点积half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));// 考虑浅色因素o.diff = nl * _LightColor0;return o;}sampler2D _MainTex;fixed4 frag (v2f i) : SV_Target{// 样本纹理fixed4 col = tex2D(_MainTex, i.uv);// 乘以光照col *= i.diff;return col;}ENDCG}}
}

这使得对象对光线方向作出反应:面向光源的部分获得光照,而背向的部分完全没有光照。

带环境光的漫射光照

上面的示例不考虑任何环境光照或光照探针。我们来解决这个问题! 事实证明,我们可以通过添加一行代码来实现这一目标。环境光和光照探针数据都以球谐函数形式传递给着色器,UnityCG.cginc include 文件 中的 ShadeSH9 函数在给定世界空间法线的情况下完成所有估算工作。

Shader "Lit/Diffuse With Ambient"
{Properties{[NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}}SubShader{Pass{Tags {"LightMode"="ForwardBase"}CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"#include "UnityLightingCommon.cginc"struct v2f{float2 uv : TEXCOORD0;fixed4 diff : COLOR0;float4 vertex : SV_POSITION;};v2f vert (appdata_base v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = v.texcoord;half3 worldNormal = UnityObjectToWorldNormal(v.normal);half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));o.diff = nl * _LightColor0;// 与先前着色器的唯一区别:// 除了来自主光源的漫射光照,// 还可添加来自环境或光照探针的光照// 来自 UnityCG.cginc 的 ShadeSH9 函数使用世界空间法线// 对其进行估算o.diff.rgb += ShadeSH9(half4(worldNormal,1));return o;}sampler2D _MainTex;fixed4 frag (v2f i) : SV_Target{fixed4 col = tex2D(_MainTex, i.uv);col *= i.diff;return col;}ENDCG}}
}

事实上,这个着色器开始看起来非常类似于内置的旧版漫射着色器!

实现阴影投射

我们的着色器目前既不能接受也不能投射阴影。让我们先实现阴影投射。

为了投射阴影,着色器必须在其任何子着色器或任何回退中具有 ShadowCaster 通道类型。ShadowCaster 通道用于将对象渲染到阴影贴图中,通常它非常简单:顶点着色器只需要估算顶点位置,片元着色器几乎不执行任何操作。阴影贴图只是深度缓冲区,因此即使是片元着色器输出的颜色也无关紧要。

这意味着对于许多着色器,阴影投射物通道几乎完全相同(除非对象具有基于自定义顶点着色器的变形,或者具有 Alpha 镂空/半透明部分)。最简单的捕捉方法是使用着色器命令 UsePass:

Pass
{// 常规光照通道
}
// 通过 VertexLit 内置着色器捕捉阴影投射物
UsePass "Legacy Shaders/VertexLit/SHADOWCASTER"

但我们在这里是为了学习,所以让我们通过“手动”方式实现相同效果。为了缩短代码长度, 我们已经将光照通道 (“ForwardBase”) 替换为仅执行无纹理环境光的代码。在它下面,有一个“ShadowCaster”通道,让对象能够支持阴影投射。

Shader "Lit/Shadow Casting"
{SubShader{// 非常简单的光照通道,仅处理非纹理环境Pass{Tags {"LightMode"="ForwardBase"}CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f{fixed4 diff : COLOR0;float4 vertex : SV_POSITION;};v2f vert (appdata_base v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);half3 worldNormal = UnityObjectToWorldNormal(v.normal);// 仅估算环境光o.diff.rgb = ShadeSH9(half4(worldNormal,1));o.diff.a = 1;return o;}fixed4 frag (v2f i) : SV_Target{return i.diff;}ENDCG}// 阴影投射物渲染通道,// 使用 UnityCG.cginc 中的宏手动实现Pass{Tags {"LightMode"="ShadowCaster"}CGPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile_shadowcaster#include "UnityCG.cginc"struct v2f { V2F_SHADOW_CASTER;};v2f vert(appdata_base v){v2f o;TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)return o;}float4 frag(v2f i) : SV_Target{SHADOW_CASTER_FRAGMENT(i)}ENDCG}}
}

现在下方有一个平面,使用常规的内置漫射着色器,因此我们可以看到 我们的阴影生效(请记住,我们当前的着色器还不支持_接受_阴影!)。

我们使用了 #pragma multi_compile_shadowcaster 指令。这会导致着色器被编译为多个变体,并为每个变体定义了不同的预处理器宏(有关详细信息,请参阅 多个着色器变体页面)。渲染到阴影贴图时,点光源与其他光源类型需要着色器代码略有不同,这就是需要此指令的原因。

接受阴影

Implementing support for receiving shadows will require compiling the base lighting pass into several variants, to handle cases of “directional light without shadows” and “directional light with shadows” properly. #pragma multi_compile_fwdbase directive does this (see multiple shader variants for details). In fact it does a lot more: it also compiles variants for the different lightmap types, Enlighten Realtime Global Illumination being on or off etc. Currently we don’t need all that, so we’ll explicitly skip these variants.

然后,为了获得实际的阴影计算,我们将 #include “AutoLight.cginc” 着色器的 include 文件 并使用该文件中的 SHADOW_COORDS、TRANSFER_SHADOW 和 SHADOW_ATTENUATION 宏。

下面是着色器:

Shader "Lit/Diffuse With Shadows"
{Properties{[NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}}SubShader{Pass{Tags {"LightMode"="ForwardBase"}CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"#include "Lighting.cginc"// 将着色器编译成多个有阴影和没有阴影的变体//(我们还不关心任何光照贴图,所以跳过这些变体)#pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlight// 阴影 helper 函数和宏#include "AutoLight.cginc"struct v2f{float2 uv : TEXCOORD0;SHADOW_COORDS(1) // 将阴影数据放入 TEXCOORD1fixed3 diff : COLOR0;fixed3 ambient : COLOR1;float4 pos : SV_POSITION;};v2f vert (appdata_base v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.uv = v.texcoord;half3 worldNormal = UnityObjectToWorldNormal(v.normal);half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));o.diff = nl * _LightColor0.rgb;o.ambient = ShadeSH9(half4(worldNormal,1));// 计算阴影数据TRANSFER_SHADOW(o)return o;}sampler2D _MainTex;fixed4 frag (v2f i) : SV_Target{fixed4 col = tex2D(_MainTex, i.uv);// 计算阴影衰减(1.0 = 完全照亮,0.0 = 完全阴影)fixed shadow = SHADOW_ATTENUATION(i);// 用阴影使光照变暗,保持环境不变fixed3 lighting = i.diff * shadow + i.ambient;col.rgb *= lighting;return col;}ENDCG}// 阴影投射支持UsePass "Legacy Shaders/VertexLit/SHADOWCASTER"}
}

看看,现在我们实现了阴影!

其他着色器示例

Fog

Shader "Custom/TextureCoordinates/Fog" {SubShader {Pass {CGPROGRAM#pragma vertex vert#pragma fragment frag//Needed for fog variation to be compiled.#pragma multi_compile_fog#include "UnityCG.cginc"struct vertexInput {float4 vertex : POSITION;float4 texcoord0 : TEXCOORD0;};struct fragmentInput{float4 position : SV_POSITION;float4 texcoord0 : TEXCOORD0;//Creates a variable that contains fog coordinates. The parameter must be a free TEXCOORD, for example 1 if TEXCOORD1 is free.UNITY_FOG_COORDS(1)};fragmentInput vert(vertexInput i){fragmentInput o;o.position = UnityObjectToClipPos(i.vertex);o.texcoord0 = i.texcoord0;//Compute fog amount from clip space position.UNITY_TRANSFER_FOG(o,o.position);return o;}fixed4 frag(fragmentInput i) : SV_Target {fixed4 color = fixed4(i.texcoord0.xy,0,0);//Apply fog (additive pass are automatically handled)UNITY_APPLY_FOG(i.fogCoord, color); //to handle custom fog color another option would have been //#ifdef UNITY_PASS_FORWARDADD//  UNITY_APPLY_FOG_COLOR(i.fogCoord, color, float4(0,0,0,0));//#else//  fixed4 myCustomColor = fixed4(0,0,1,0);//  UNITY_APPLY_FOG_COLOR(i.fogCoord, color, myCustomColor);//#endifreturn color;}ENDCG}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/711172.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

高光谱遥感学习入门丨高光谱数据处理基础、Python和Matlab高光谱遥感数据处理

目录 ①Python高光谱遥感数据处理与高光谱遥感机器学习方法深度应用 ②Matlab高光谱遥感、数据处理与混合像元分解实践技术应用 ③高光谱遥感数值建模技术及在植被、水体、土壤信息提取领域应用 更多应用 高光谱遥感信息对于我们认识世界具有重要意义。尽管大部分物质在人眼…

记录 | docker权限原因导致service ssh start失败

【报错】 容器内启 ssh server 报错 有两个错: (1)/etc/ssh/sshd_host_rsa_key 权限太高; (2)/run/sshd用户组不为 root 解决方法: 方法一: 各自容器内对/etc/ssh/sshd_host_r…

【前端素材】推荐优质后台管理系统 Adminity平台模板(附源码)

一、需求分析 1、系统定义 后台管理系统是一种用于管理网站、应用程序或系统的管理界面,通常由管理员和工作人员使用。它提供了访问和控制网站或应用程序后台功能的工具和界面,使其能够管理用户、内容、数据和其他各种功能。 2、功能需求 后台管理系…

2024年四川媒体新闻发布渠道,媒体邀约资源表

传媒如春雨,润物细无声,大家好,我是51媒体网胡老师。 四川有哪些媒体新闻发布渠道,媒体邀约资源表? 2024年四川媒体新闻发布渠道,媒体邀约资源表 四川本地媒体:如四川日报、华西都市报、成都商…

【论文阅读-PRIVGUARD】Day4:3节

3 PRIVANALYZER:强制执行隐私政策的静态分析 本节介绍PRIVANALYZER,这是一个用于强制执行由PRIVGUARD追踪的隐私政策的静态分析器**。我们首先回顾LEGALEASE政策语言,我们使用它来正式编码政策,然后描述如何静态地强制执行它们**…

<专利>机器人3D视觉快速定位抓取方法及系统

摘要,此专利无可用的关键技术信息,基本都是下面几句话反复说。。。 本发明提供了一种机器人3D快速定位抓取方法及系统, 包括: 通过高速的3D结构光成像对目标物体的表面轮廓进行扫描, 形成点云数据;对所述点…

Day07:基础入门-抓包技术全局协议封包监听网卡模式APP小程序PC应用

目录 非HTTP/HTTPS协议抓包工具 WireShark 科来网络分析系统 WPE封包 思维导图 章节知识点: 应用架构:Web/APP/云应用/三方服务/负载均衡等 安全产品:CDN/WAF/IDS/IPS/蜜罐/防火墙/杀毒等 渗透命令:文件上传下载/端口服务/Sh…

未来已来:智慧餐饮点餐系统引领餐饮业的数字化转型

时下,智慧餐饮点餐系统正在引领着餐饮业迈向更高的位置。今天,小编将与大家共同探讨智慧餐饮点餐系统的发展趋势、优势以及对餐饮业的影响。 一、智慧餐饮点餐系统的发展趋势 智慧餐饮点餐系统的出现填补了这一空白,它通过引入数字化技术&a…

如何学习、上手点云算法(一):点云基础

写在前面 本文内容 点云算法的学习基础,入门方法,相关领域,资源,开源库,算法等的介绍; 以Open3D和PCL等为基础工具的点云处理代码讲解、实现; 文中涉及的参考以链接形式给出,涉及文…

【JavaEE】_第一个SpringBoot项目

目录 1. 第一个SpringBoot项目 1.1 创建项目 1.2 加载依赖 1.3 运行启动类 1.4 创建一个简单类试运行 2. 关于SpringBoot项目的目录结构 3. 关于修改文件名 4. 关于启动日志 5. 关于访问出错 5.1 404 5.1.2 URL输入错误 5.1.2 注解错误 5.2 500 5.3 无法访问此网…

JDK动态代理如何获取动态生成的代理类的class文件

在使用JDK动态代理,即reflect包下的Proxy类的newProxyInstance方法时,会在运行时,根据传进来的接口类型动态生成class字节码文件。这个字节码文件是在内存中动态获取的,程序结束就没有了,如何动态获取呢。 System.set…

LeetCode 2265.统计值等于子树平均值的节点数

给你一棵二叉树的根节点 root ,找出并返回满足要求的节点数,要求节点的值等于其 子树 中值的 平均值 。 注意: n 个元素的平均值可以由 n 个元素 求和 然后再除以 n ,并 向下舍入 到最近的整数。 root 的 子树 由 root 和它的所…

海外代理IP干货:应该选择SOCKS55代理还是Http代理?

在使用IPFoxy全球代理时,选择 SOCKS55代理还是HTTP代理?IPFoxy代理可以SOCKS55、Http协议自主切换,但要怎么选择?为解决这个问题,得充分了解两种代理的工作原理和配置情况。 在这篇文章中,我们会简要介绍 …

Android PDFView 提示401 pom

背景 在开发安卓app,使用PDF组件来解析URL地址 ,从github找到一个开源组件 AndroidPdfViewer 遇到一个大坑,一直提示下载依赖401 pom 打开控制台链接弹出需要登录jitpack 原因分析: 这个组件项目依赖库链接到了需要鉴权的…

能力认证!时代新威荣获《数据安全服务能力评定资格证书》

2024年1月17日,时代新威受邀出席中国计算机行业协会数据安全专业委员会(以下简称“数专委”)年度工作总结会。此次会议旨在聚集行业企业,搭建数据安全产业企业交流平台,共建产业发展“朋友圈”。 会上,数专…

初阶数据结构:链表相关题目练习(补充)

目录 1. 单链表相关练习题1.1 移除链表元素1.2 反转链表1.3 链表的中间结点1.4 链表的倒数第k个结点1.5 合并两个有序链表1.6 链表分割1.7 链表的回文结构1.8 相交链表1.9 判断一个链表中是否有环1.10 寻找环状链表相遇点1.11 链表的深度拷贝 1. 单链表相关练习题 注&#xff1…

IEEE Transactions on Industrial Electronics工业电子TIE修改稿注意事项及提交须知

一、背景 兔年末投了一篇TIE,手稿初次提交的注意事项也整理成了博客IEEE Transactions on Industrial Electronics工业电子TIE论文投稿须知,获得了许多点赞和收藏。最近也收到了审稿结果,给的意见是大修major revision,总之只要不…

基于springboot+vue的线上辅导班系统

博主主页:猫头鹰源码 博主简介:Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战,欢迎高校老师\讲师\同行交流合作 ​主要内容:毕业设计(Javaweb项目|小程序|Pyt…

吸猫毛空气净化器哪个好?推荐除猫毛好的宠物空气净化器品牌

如今,越来越多的家庭选择养宠物!虽然家里变得更加温馨,但养宠可能会带来异味和空气中的毛发增多可能会引发健康问题,这也是一个大问题。 但我不想家里到处都是异味,尤其是便便的味道,所以很需要一款能够处…