着色器不是魔术。 在Unity中编写着色器。 顶点着色器

大家好! 我叫Grigory Dyadichenko,是Foxsys Studios的创始人兼CTO。 今天我们将讨论顶点着色器。 本文将从Unity的角度检查实践,并提供非常简单的示例以及用于研究有关Unity中着色器信息的许多链接。 如果您擅长编写着色器,那么您将自己找不到任何新东西。 任何想开始在Unity中编写着色器的人,欢迎加入。



一点理论



为了更好地了解着色器过程,让我们看一下一些理论。 顶点着色器或顶点着色器是可与各个顶点配合使用的着色器的可编程阶段。 顶点又存储了着色器的此部分处理的各种属性,以便在输出中获取转换后的属性。

使用顶点着色器的示例





对象的变形 -逼真的波浪,雨水涟漪的影响,子弹撞击时的变形,所有这些都可以通过顶点着色器完成,并且比通过着色器片段部分中的“凹凸贴图”进行的同一件事看起来更加真实。 由于这是几何形状的变化。 有关此主题的3.0级着色器具有一种称为“间距映射”的技术,因为它们现在可以访问着色器顶点部分中的纹理。



对象的动画。 当植物对角色或风中摇曳的树木做出反应时,游戏看起来更加生动有趣。 为此,还使用了顶点着色器。



卡通照明或程式化。 从样式的角度来看,在许多游戏中,看起来有趣的不是pbr照明,而是样式。 计算片段部分中的任何内容都没有意义。



剥皮。 在游戏引擎中的给定时刻,此问题已解决,但是了解顶点着色器以了解其工作原理还是很有用的。

使用顶点的简单示例




我不希望发生这种情况,就像在上一堂有关如何画猫头鹰的老课程中一样,让我们​​分阶段进行。 创建一个标准的表面着色器。 这可以通过在项目视图中或“资产”选项卡上面板中的鼠标右键来完成。 创建->着色器->标准曲面着色器。

我们得到了这样的标准空白。

表面着色器
Shader "Custom/SimpleVertexExtrusionShader"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200

CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows

// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0

sampler2D _MainTex;

struct Input
{
float2 uv_MainTex;
};

half _Glossiness;
half _Metallic;
fixed4 _Color;

// Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
// See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
// #pragma instancing_options assumeuniformscaling
UNITY_INSTANCING_BUFFER_START(Props)
// put more per-instance properties here
UNITY_INSTANCING_BUFFER_END(Props)

void surf (Input IN, inout SurfaceOutputStandard o)
{
// Albedo comes from a texture tinted by color
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
// Metallic and smoothness come from slider variables
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = ca;
}
ENDCG
}
FallBack "Diffuse"
}

它是如何工作的,一般而言,我们将在基本实践之后在文章中对其进行详细分析,此外,在实现着色器期间我们将部分地理解它。 现在,让某些事情保持原样。 简而言之,没有魔术(就如何选择参数等而言)仅针对某些关键字,该单元会为您生成代码,以免从头开始编写代码。 因此,此过程不够明显。 您可以在Unity中阅读有关表面着色器及其属性的更多信息。 docs.unity3d.com/Manual/SL-SurfaceShaders.html

我们将删除其中多余的所有内容,以免分散注意力,因为在给定的时刻不需要它。 并获得如此短的着色器。

简化的着色器
Shader "Custom/SimpleVertexExtrusionShader"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200

CGPROGRAM

#pragma surface surf Standard fullforwardshadows

#pragma target 3.0

struct Input
{
float4 color : COLOR;
};

fixed4 _Color;

void surf (Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = _Color;
o.Albedo = c.rgb;
}
ENDCG
}
FallBack "Diffuse"
}




只是模型上带有照明的颜色。 在这种情况下,Unity负责计算照明。

首先,添加Unity示例中最简单的效果。 挤出是正常的,在此示例中,我们将分析其工作原理。

为此,将vertex:vert修改器添加到#pragma曲面冲浪“标准fullforwards”阴影线。 如果我们将inout appdata_full v作为参数传递给函数,则实质上该函数是顶点修饰符。 它的核心是顶点着色器的一部分,该着色器由代码生成单元创建,该代码生成单元执行顶点的初步处理。 同样在“ 属性”块中,添加接受值从0到1的_Amount字段。要在着色器中使用_Amount字段,我们还需要在其中定义它。 在函数中,我们将简单地根据_Amount移至法线 ,其中0是标准顶点位置(零移),而1是正法线

SimpleVertexExtrusionShader
Shader "Custom/SimpleVertexExtrusionShader"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_Amount ("Extrusion Amount", Range(0,1)) = 0.5
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200

CGPROGRAM

#pragma surface surf Standard fullforwardshadows vertex:vert

#pragma target 3.0

struct Input
{
float4 color : COLOR;
};

fixed4 _Color;
float _Amount;

void vert (inout appdata_full v)
{
v.vertex.xyz += v.normal * _Amount;
}
void surf (Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = _Color;
o.Albedo = c.rgb;
}
ENDCG
}
FallBack "Diffuse"
}


您会注意到着色器的重要功能。 尽管着色器每帧执行一次,但在着色器操作期间获得的结果不会保存在网格中,而仅用于渲染。 因此,不可能与着色器的功能以及脚本中的更新相关 。 在不更改网格数据的情况下,每帧都应用它们,而只是修改网格以进一步渲染。

例如,制作动画最简单的方法之一就是使用时间更改振幅。 该单元具有内置变量,可在此处找到其完整列表docs.unity3d.com/Manual/SL-UnityShaderVariables.html在这种情况下,我们将基于过去的着色器编写一个新的着色器。 代替_Amount,让我们将float值设为_Amplitude,并使用内置的Unity变量_SinTime_SinTime是时间的正弦,因此它的取值范围是-1至1。但是,请不要忘记单位着色器中所有内置的时间变量都是float4向量。 例如, _SinTime定义为(sin(t / 8),sin(t / 4),sin(t / 2),sin(t)) ,其中t是时间。 因此,我们采用z分量,以便动画更快。 我们得到:

SimpleVertexExtrusionWithTime
Shader "Custom/SimpleVertexExtrusionWithTime"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_Amplitude ("Extrusion Amplitude", float) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200

CGPROGRAM

#pragma surface surf Standard fullforwardshadows vertex:vert

#pragma target 3.0

struct Input
{
float4 color : COLOR;
};

fixed4 _Color;
float _Amplitude;

void vert (inout appdata_full v)
{
v.vertex.xyz += v.normal * _Amplitude * (1 - _SinTime.z);
}
void surf (Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = _Color;
o.Albedo = c.rgb;
}
ENDCG
}
FallBack "Diffuse"
}




因此,这些只是简单的示例。 现在该画猫头鹰了!

物体变形





我已经写了整篇文章,涉及一种变形效应,并详细分析了过程数学以及开发这种效应时的思维逻辑habr.com/en/post/435828这将是我们的猫头鹰。

本文中的所有着色器均以hlsl编写。 这种语言实际上有其自己的大量文档,尽管它们在HLSL docs.microsoft.com/zh-cn/windows/win32/direct3dhlsl/dx-graphics-hlsl-中定义,但许多人忘记并想知道有线功能的一半来自何处。内在功能

但是实际上,一个单元中的表面着色器本身就是一个庞大而庞大的话题。 另外,您并不总是想与Unity照明混为一谈。 有时,您需要作弊并编写仅具有正确的预定义效果集的最快的着色器。 可以统一编写较低级别的着色器。

低级着色器





根据使用着色器的良好传统,以下我们将折磨斯坦福兔子。

通常,所谓的Unity ShaderLab本质上是对检查员的可视化,其中包含材料中的字段并简化了编写着色器。

采用Shaderlab着色器的一般结构:

通用着色器结构
Shader "MyShaderName"
{
Properties
{
//
}
SubShader // ( )
{
Pass
{
//
}
//
}
//
FallBack "VertexLit" // ,
}


编译指令,例如
#pragma顶点vert
#pragma片段片段
确定分别将哪些着色器功能编译为顶点着色器和片段着色器。

假设我们采用了最常见的示例之一-用于显示法线颜色的着色器:

简单法线可视化
Shader "Custom/SimpleNormalVisualization"
{
Properties
{
}
SubShader
{
Pass
{
CGPROGRAM

#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
};

v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.normal * 0.5 + 0.5;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
return fixed4 (i.color, 1);
}
ENDCG
}
}
FallBack "VertexLit"
}




在这种情况下,在顶点部分,我们将转换后的法线值写入顶点颜色,在像素部分,我们将该颜色用作模型的颜色。

UnityObjectToClipPos函数是Unity辅助函数(来自UnityCG.cginc文件),该函数将对象的顶点转换为与相机关联的位置。 如果没有它,则无论变换的位置如何,当对象进入相机的可见性(视锥)时,都将在屏幕的坐标中绘制该对象。 由于最初,顶点的位置在对象的坐标中表示。 只是相对于他的枢轴而言的价值观。

这个块。
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
};

这是将在顶点部分中处理并转移到片段一的结构的定义。 在这种情况下,确定从网格获取两个参数-顶点的位置和顶点的颜色。 您可以在此链接docs.unity3d.com/Manual/SL-VertexProgramInputs.html上了解有关可以在一个单元中抛出哪些数据的更多信息。

重要说明。 网格属性的名称无关紧要。 也就是说,假设您在color属性中可以写出与原始位置的偏差矢量(这样,当角色移动时它们有时会起作用,以便草从中“退”)。 如何处理此属性完全取决于您的着色器。

结论



感谢您的关注! 写一些没有片段的复杂效果是有问题的,因此,我们将在单独的文章中讨论类似的问题。 我希望在这篇文章中,总体上如何编写顶点着色器的代码以及在何处可以找到要研究的信息变得更加清楚,因为着色器是一个非常深入的主题。

在以后的文章中,我们将分析其他类型的着色器,单个效果,并在创建新的或复杂的效果时尝试描述我的思维逻辑。

还创建了一个存储库,将添加该系列文章github.com/Nox7atra/ShaderExamples的所有结果,我希望这些信息对刚开始研究此主题的初学者有用。

一些有用的链接(包括来源):


www.khronos.org/opengl/wiki/Vertex_Shader
docs.microsoft.com/zh-CN/windows/win32/direct3dhlsl/dx-graphics-hlsl-reference
docs.unity3d.com/en/current/Manual/SL-Reference.html
docs.unity3d.com/Manual/GraphicsTutorials.html
www.malbred.com/3d-grafika-3d-redaktory/sovremennaya-terminologiya-3d-grafiki/vertex-shader-vershinnyy-sheyder.html
3dpapa.ru/准确的位移工作流程

Source: https://habr.com/ru/post/zh-CN474812/


All Articles