当前位置: 首页 > news >正文

Unity-Shader详解-其四

今天我们来聊Unity特有的表面着色器以及很少提到的几何着色器。

表面着色器

在前文关于光照的计算中,我们学会了很多:我们学习了一系列光照模型,比如专门针对漫反射的兰伯特模型和改进的半兰伯特模型,又比如由高光、漫反射和环境光组成的冯模型以及改进了反射方向计算的布林-冯模型,我们学会了针对多光源场景的前向渲染和延迟渲染方式,我们还学会了针对透明物体和不透明物体的渲染队列设置。

但是在实际的开发过程中,场景可能比我们想象的还要复杂,如果我们依然手动的写好每一个顶点着色器、片元着色器,那么工作效率是非常低下的,是否有办法来改进呢?

答案是有的,兄弟,有的。

在使用之前有一些不得不提的是:

具体来说我们如何使用呢?

我们直接用一个代码示例:
 

Shader "Chapter5/chapter5_1_sample"
{// 定义Shader的属性(Properties),这些属性会显示在材质面板中,允许用户进行调整。Properties{// 定义一个颜色属性,默认值为白色 (1,1,1,1)。_Color ("Color", Color) = (1,1,1,1)// 定义一个2D纹理属性,表示材质的基础颜色贴图(Albedo),默认值为白色纹理。_MainTex ("Albedo (RGB)", 2D) = "white" {}// 定义光滑度(Smoothness)的滑块,范围是0到1,默认值为0.5。_Glossiness ("Smoothness", Range(0,1)) = 0.5// 定义金属度(Metallic)的滑块,范围是0到1,默认值为0.0。_Metallic ("Metallic", Range(0,1)) = 0.0}// 定义子着色器(SubShader),包含材质的具体渲染代码。SubShader{// 设置渲染标签,标记为不透明材质(Opaque)。Tags { "RenderType"="Opaque" }// 定义细节级别(Level of Detail,LOD),值为200。LOD 200// 开始CG代码块,定义基于Surface Shader的着色代码。CGPROGRAM// 使用标准物理光照模型(Standard lighting model),并启用所有光源的阴影。#pragma surface surf Standard fullforwardshadows// 设置Shader的目标为3.0(Shader Model 3.0),以获得更好的光照效果。#pragma target 3.0// 定义2D纹理采样器变量,用于采样_MainTex纹理。sampler2D _MainTex;// 定义输入结构体(Input),用于传递顶点的UV坐标等数据。struct Input{float2 uv_MainTex; // 接收_MainTex的UV坐标};// 定义变量,用于接收材质属性。half _Glossiness; // 光滑度half _Metallic;   // 金属度fixed4 _Color;    // 颜色// 添加实例化支持的缓冲区,用于GPU实例化技术。UNITY_INSTANCING_BUFFER_START(Props)// 可以在此处添加更多的每实例属性。UNITY_INSTANCING_BUFFER_END(Props)// 定义Surface函数,负责实现表面着色逻辑。void surf (Input IN, inout SurfaceOutputStandard o){// 从_MainTex纹理采样颜色,并乘以用户定义的颜色(_Color)。fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;// 设置材质的Albedo颜色(反射漫射颜色)。o.Albedo = c.rgb;// 使用滑块变量设置材质的金属度(Metallic)和光滑度(Smoothness)。o.Metallic = _Metallic;o.Smoothness = _Glossiness;// 设置材质的透明度(Alpha值)。o.Alpha = c.a;}ENDCG // 结束CG代码块}// 定义备用Shader,当目标设备不支持当前Shader时使用。FallBack "Diffuse"
}

依然只说新东西,可以看到有很多新东西:

        // 使用标准物理光照模型(Standard lighting model),并启用所有光源的阴影。#pragma surface surf Standard fullforwardshadows// 设置Shader的目标为3.0(Shader Model 3.0),以获得更好的光照效果。#pragma target 3.0

关于第一句:

这个所谓的标准物理光照模型PBR:

 而关于target 3.0:

与其他版本的对比:

        // 添加实例化支持的缓冲区,用于GPU实例化技术。UNITY_INSTANCING_BUFFER_START(Props)// 可以在此处添加更多的每实例属性。UNITY_INSTANCING_BUFFER_END(Props)

虽然之前的LearnOpenGL的内容中有提到过GPU的实例化,但在这里还是再复习一下:

        // 定义Surface函数,负责实现表面着色逻辑。void surf (Input IN, inout SurfaceOutputStandard o){// 从_MainTex纹理采样颜色,并乘以用户定义的颜色(_Color)。fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;// 设置材质的Albedo颜色(反射漫射颜色)。o.Albedo = c.rgb;// 使用滑块变量设置材质的金属度(Metallic)和光滑度(Smoothness)。o.Metallic = _Metallic;o.Smoothness = _Glossiness;// 设置材质的透明度(Alpha值)。o.Alpha = c.a;}

这个就是表面着色器的内容部分,其中Input是我们自定义的结构体,SurfaceOutputStandard则是Unity内置的结构体类型,用于描述材质的物理属性(如漫反射颜色、金属度、光滑度等)。

    // 定义备用Shader,当目标设备不支持当前Shader时使用。FallBack "Diffuse"

FallBack的作用已经写在注释里了,就是当前shader不适用时使用FallBack选定的shader。

效果如下:

曲面细分

如果表面着色器只是一个让你少写一些顶点着色器和片元着色器定义的工具,那他不会成为计算机图形学的核心技术,事实上,在表面着色器上还可以实现:曲面细分。

等一下。

我觉得有必要在这里插播一下一个基本的概念:片元、像素和网格的区别,因为我老是把这些概念弄混淆:

而曲面细分的对象则是网格。

 

曲面细分发生的阶段是顶点着色器到片元着色器之间的可选阶段,这个时候还没有产生片元(光栅化后输出)和片元(渲染管线流程后输出)。

而既然是要划分网格成更多的网格,那么划分的方式自然就是十分重要,常见的有以下三种:

我们来一个个用具体的代码案例测试一下:

固定数量:

Shader "Chapter5/chapter5_2"
{// 定义Shader的属性,这些属性会在材质编辑器中显示,允许用户调整。Properties{// 定义一个控制细分(Tessellation)程度的滑块,范围是1到32,默认值为4。_Tess ("Tessellation", Range(1,32)) = 4// 定义基础颜色纹理属性,默认值为白色。_MainTex ("Base (RGB)", 2D) = "white" {}// 定义用于位移映射的纹理属性,默认值为灰色。_DispTex ("Disp Texture", 2D) = "gray" {}// 定义法线贴图属性,用于控制表面细节的法线效果,默认值为bump类型。_NormalMap ("Normalmap", 2D) = "bump" {}// 定义位移强度的滑块,范围是0到1,默认值为0.3。_Displacement ("Displacement", Range(0, 1.0)) = 0.3// 定义颜色属性,用于控制整体颜色,默认值为白色。_Color ("Color", color) = (1,1,1,0)// 定义镜面反射颜色属性,默认值为灰色。_SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)}// 定义子着色器部分,包含渲染逻辑。SubShader{// 定义渲染标签,标记为不透明材质。Tags{"RenderType"="Opaque"}// 设置着色器的LOD(细节级别)为300。LOD 300// 开始CG代码块,定义基于Surface Shader的着色逻辑。CGPROGRAM// 使用BlinnPhong光照模型,启用阴影,并添加顶点位移和细分着色。#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessFixed nolightmap// 设置目标为Shader Model 4.6(支持更高级功能,如细分着色)。#pragma target 4.6// 定义应用程序顶点数据结构。struct appdata{float4 vertex : POSITION;  // 顶点位置float4 tangent : TANGENT; // 切线,用于法线映射float3 normal : NORMAL;   // 法线float2 texcoord : TEXCOORD0; // UV坐标};// 定义细分级别变量,用于控制细分程度。float _Tess;// 固定细分级别函数,返回细分强度。float4 tessFixed(){return _Tess; // 由用户设置的_Tess值决定细分强度。}// 定义用于位移贴图的纹理采样器和位移强度变量。sampler2D _DispTex;float _Displacement;// 位移函数,对顶点位置进行调整。void disp(inout appdata v){// 使用纹理采样lod方式获取位移贴图值,并乘以位移强度。float d = tex2Dlod(_DispTex, float4(v.texcoord.xy, 0, 0)).r * _Displacement;// 根据法线方向调整顶点位置,实现位移效果。v.vertex.xyz += v.normal * d;}// 定义输入结构体,用于传递UV坐标。struct Input{float2 uv_MainTex; // 主纹理的UV坐标。};// 定义其他所需变量。sampler2D _MainTex;    // 主纹理采样器。sampler2D _NormalMap;  // 法线贴图采样器。fixed4 _Color;         // 基础颜色。// Surface函数,处理表面着色逻辑。void surf(Input IN, inout SurfaceOutput o){// 从主纹理中采样颜色,并乘以用户设置的基础颜色。half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;// 设置漫反射颜色(Albedo)。o.Albedo = c.rgb;// 设置镜面反射强度为0.2。o.Specular = 0.2;// 设置光泽度为1.0。o.Gloss = 1.0;// 从法线贴图中采样法线,并解包法线数据。o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));}ENDCG // 结束CG代码块}// 当目标设备不支持当前Shader时,回退到Diffuse着色器。FallBack "Diffuse"
}
    // 定义Shader的属性,这些属性会在材质编辑器中显示,允许用户调整。Properties{// 定义一个控制细分(Tessellation)程度的滑块,范围是1到32,默认值为4。_Tess ("Tessellation", Range(1,32)) = 4// 定义基础颜色纹理属性,默认值为白色。_MainTex ("Base (RGB)", 2D) = "white" {}// 定义用于位移映射的纹理属性,默认值为灰色。_DispTex ("Disp Texture", 2D) = "gray" {}// 定义法线贴图属性,用于控制表面细节的法线效果,默认值为bump类型。_NormalMap ("Normalmap", 2D) = "bump" {}// 定义位移强度的滑块,范围是0到1,默认值为0.3。_Displacement ("Displacement", Range(0, 1.0)) = 0.3// 定义颜色属性,用于控制整体颜色,默认值为白色。_Color ("Color", color) = (1,1,1,0)// 定义镜面反射颜色属性,默认值为灰色。_SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)}

其中的_Tess用于控制细分程度,也就是我要把一个网格多分成几份。

_DispTex定义位移映射的纹理属性,_Displacement定义位移强度,_SpecColor定义反射颜色。

        // 使用BlinnPhong光照模型,启用阴影,并添加顶点位移和细分着色。#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessFixed nolightmap// 设置目标为Shader Model 4.6(支持更高级功能,如细分着色)。#pragma target 4.6

之前已经提到过的Shader Model版本,只有在4.0及以上才支持曲面细分。

关于表面着色器的定义:

        // 固定细分级别函数,返回细分强度。float4 tessFixed(){return _Tess; // 由用户设置的_Tess值决定细分强度。}// 位移函数,对顶点位置进行调整。void disp(inout appdata v){// 使用纹理采样lod方式获取位移贴图值,并乘以位移强度。float d = tex2Dlod(_DispTex, float4(v.texcoord.xy, 0, 0)).r * _Displacement;// 根据法线方向调整顶点位置,实现位移效果。v.vertex.xyz += v.normal * d;}

这是我们自定义的细分函数和位移函数,用于设置相关参数。

效果如图:

然后是我们基于边长的曲面细分:

Shader "Chapter5/chapter5_3"
{// 定义材质属性,用于调整材质的外观效果。Properties{// 边缘长度属性,用于控制细分曲面的细化程度,范围为2到50,默认值为15。_EdgeLength ("Edge length", Range(2,50)) = 15// 主纹理(基础颜色纹理),用于控制物体表面的颜色显示,默认值为白色。_MainTex ("Base (RGB)", 2D) = "white" {}// 位移贴图属性,用于生成位移效果,默认值为灰色纹理。_DispTex ("Disp Texture", 2D) = "gray" {}// 法线贴图属性,用于控制表面法线的细节显示,默认值为bump类型。_NormalMap ("Normalmap", 2D) = "bump" {}// 位移强度属性,范围为0到1,控制位移效果的强度,默认值为0.3。_Displacement ("Displacement", Range(0, 1.0)) = 0.3// 基础颜色属性,用于给材质添加颜色,默认值为白色。_Color ("Color", color) = (1,1,1,0)// 镜面反射颜色属性,控制镜面高光的颜色,默认值为灰色。_SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)}// 定义子着色器(SubShader),包含材质的具体渲染实现。SubShader{// 设置渲染标签,标记为不透明材质。Tags{"RenderType"="Opaque"}// 设置Shader的LOD(细节级别)为300,决定渲染质量和性能的平衡。LOD 300// 开始CG代码块,定义基于Surface Shader的渲染逻辑。CGPROGRAM// 使用BlinnPhong光照模型,启用阴影、全前向渲染、顶点位移函数(disp),以及边缘长度细分(tessEdge)。#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessEdge nolightmap// 设置目标为Shader Model 4.6(支持高级功能,如细分曲面)。#pragma target 4.6// 包含Unity内置的细分曲面库,提供细分算法支持。#include "Tessellation.cginc"// 定义顶点数据结构,用于从应用程序传递数据到GPU。struct appdata{float4 vertex : POSITION;  // 顶点位置float4 tangent : TANGENT; // 切线向量,用于法线贴图float3 normal : NORMAL;   // 法线向量float2 texcoord : TEXCOORD0; // UV坐标};// 定义边缘长度属性变量,用于控制细分程度。float _EdgeLength;// 边缘长度细分函数,基于三角形顶点的位置计算细分因子。float4 tessEdge(appdata v0, appdata v1, appdata v2){// 使用Unity内置的基于边缘长度的细分函数。return UnityEdgeLengthBasedTess(v0.vertex, v1.vertex, v2.vertex, _EdgeLength);}// 位移贴图采样器和位移强度变量,用于控制顶点位移效果。sampler2D _DispTex;float _Displacement;// 顶点位移函数,根据法线方向和位移贴图调整顶点位置。void disp(inout appdata v){// 从位移贴图中采样高度值,并乘以位移强度。float d = tex2Dlod(_DispTex, float4(v.texcoord.xy, 0, 0)).r * _Displacement;// 根据法线方向调整顶点位置,实现位移效果。v.vertex.xyz += v.normal * d;}// 定义输入结构体,用于传递UV坐标。struct Input{float2 uv_MainTex; // 主纹理的UV坐标。};// 定义其他所需变量。sampler2D _MainTex;    // 主纹理采样器。sampler2D _NormalMap;  // 法线贴图采样器。fixed4 _Color;         // 基础颜色。// Surface函数,处理表面着色逻辑。void surf(Input IN, inout SurfaceOutput o){// 从主纹理中采样颜色,并乘以基础颜色。half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;// 设置材质的漫反射颜色(Albedo)。o.Albedo = c.rgb;// 设置材质的镜面反射强度为0.2。o.Specular = 0.2;// 设置材质的光泽度为1.0。o.Gloss = 1.0;// 从法线贴图中采样法线数据,并解包为适用的法线值。o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));}ENDCG // 结束CG代码块}// 当目标设备不支持当前Shader时,回退到简单的Diffuse Shader。FallBack "Diffuse"
}

可以看到和之前的不同:

// 边缘长度属性,用于控制细分曲面的细化程度,范围为2到50,默认值为15。_EdgeLength ("Edge length", Range(2,50)) = 15

这里是边缘长度属性。

        // 定义边缘长度属性变量,用于控制细分程度。float _EdgeLength;// 边缘长度细分函数,基于三角形顶点的位置计算细分因子。float4 tessEdge(appdata v0, appdata v1, appdata v2){// 使用Unity内置的基于边缘长度的细分函数。return UnityEdgeLengthBasedTess(v0.vertex, v1.vertex, v2.vertex, _EdgeLength);}

这里的根据边长进行曲面细分的函数中,可以看到我们使用了一个Unity内置的函数:UnityEdgeLengthBasedTess()。

其余基本和之前一样。 

效果如图:

然后是基于距离的:

Shader "Chapter5/chapter5_4_distance"
{// 定义材质属性,供用户在编辑器中调整材质的参数。Properties{// 控制细分曲面因子的强度,范围是1到32,默认值为4。_Tess ("Tessellation", Range(1,32)) = 4// 最大距离。_MaxDis ("MaxDis", Range(1,100)) = 50// 最小距离。_MinDis ("MinDis", Range(1,100)) = 20// 主纹理,用于显示基础颜色纹理,默认值为白色。_MainTex ("Base (RGB)", 2D) = "white" {}// 位移贴图,用于生成位移效果,默认值为灰色纹理。_DispTex ("Disp Texture", 2D) = "gray" {}// 法线贴图,用于控制材质表面法线的细节显示,默认值为bump类型。_NormalMap ("Normalmap", 2D) = "bump" {}// 位移强度,用于调整顶点位移效果的强度,范围为0到1,默认值为0.3。_Displacement ("Displacement", Range(0, 1.0)) = 0.3// 基础颜色,用于调整材质的颜色,默认值为白色。_Color ("Color", color) = (1,1,1,0)// 镜面高光颜色,用于调整材质的镜面反射颜色,默认值为中灰色。_SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)}// 定义子着色器,用于实现材质的具体渲染效果。SubShader{// 设置渲染类型标签,将材质标记为不透明(Opaque)。Tags{"RenderType"="Opaque"}// 设置细节级别(LOD),用于控制渲染质量与性能的平衡。LOD 300// 开始CG代码块,定义Shader的具体实现逻辑。CGPROGRAM// 使用Blinn-Phong光照模型,支持阴影、前向渲染和顶点位移(disp),并启用基于距离的细分曲面(tessDistance)。#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessDistance nolightmap// 设置目标Shader模型版本为4.6,支持高级图形功能(如细分曲面)。#pragma target 4.6// 包含Unity内置的细分曲面库文件,提供相关算法和工具。#include "Tessellation.cginc"// 定义顶点数据结构,用于传递顶点信息。struct appdata{float4 vertex : POSITION;  // 顶点位置。float4 tangent : TANGENT; // 切线向量,用于法线贴图。float3 normal : NORMAL;   // 法线向量。float2 texcoord : TEXCOORD0; // UV纹理坐标。};// 定义细分曲面强度属性。float _Tess;float _MaxDis;float _MinDis;// 基于距离的细分曲面函数,使用Unity内置的算法。float4 tessDistance(appdata v0, appdata v1, appdata v2){// 设置最小和最大细分距离。float minDist = _MinDis; // 最小距离,细分强度较大。float maxDist = _MaxDis; // 最大距离,细分强度较小。// 调用Unity的内置函数,根据顶点与相机的距离计算细分因子。return UnityDistanceBasedTess(v0.vertex, v1.vertex, v2.vertex, minDist, maxDist, _Tess);}// 位移贴图采样器和位移强度变量,用于生成顶点位移效果。sampler2D _DispTex;float _Displacement;// 顶点位移函数,通过位移贴图调整顶点位置。void disp(inout appdata v){// 从位移贴图中采样位移值(高度值),并根据位移强度调整。float d = tex2Dlod(_DispTex, float4(v.texcoord.xy, 0, 0)).r * _Displacement;// 根据顶点法线方向对顶点进行偏移,实现位移效果。v.vertex.xyz += v.normal * d;}// 定义输入结构体,用于传递UV坐标等信息。struct Input{float2 uv_MainTex; // 主纹理的UV坐标。};// 定义所需的采样器和颜色变量。sampler2D _MainTex;    // 主纹理采样器。sampler2D _NormalMap;  // 法线贴图采样器。fixed4 _Color;         // 基础颜色。// Surface函数,用于处理材质的表面着色逻辑。void surf(Input IN, inout SurfaceOutput o){// 从主纹理中采样颜色,并与基础颜色相乘。half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;// 设置材质的漫反射颜色(Albedo)。o.Albedo = c.rgb;// 设置材质的镜面反射强度。o.Specular = 0.2;// 设置材质的光泽度(Gloss)。o.Gloss = 1.0;// 从法线贴图中采样法线数据,并解包为适用的法线值。o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));}ENDCG // 结束CG代码块}// 设置回退着色器。当目标设备不支持当前Shader时,使用简单的Diffuse Shader。FallBack "Diffuse"
}

可以看到主要的区别依然是集中在这么几个地方:
 

        // 最大距离。_MaxDis ("MaxDis", Range(1,100)) = 50// 最小距离。_MinDis ("MinDis", Range(1,100)) = 20

在材质属性中定义好最大最小距离。

// 使用Blinn-Phong光照模型,支持阴影、前向渲染和顶点位移(disp),并启用基于距离的细分曲面(tessDistance)。
#pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessDistance nolightmap

表面着色器的一些设置:光照模型采用布林-冯模型,添加阴影、前向渲染,顶点着色器使用名为disp的函数(实际上是位移函数),曲面细分启用名为tessDistance的函数,禁用光照贴图。

        // 定义细分曲面强度属性。float _Tess;float _MaxDis;float _MinDis;// 基于距离的细分曲面函数,使用Unity内置的算法。float4 tessDistance(appdata v0, appdata v1, appdata v2){// 设置最小和最大细分距离。float minDist = _MinDis; // 最小距离,细分强度较大。float maxDist = _MaxDis; // 最大距离,细分强度较小。// 调用Unity的内置函数,根据顶点与相机的距离计算细分因子。return UnityDistanceBasedTess(v0.vertex, v1.vertex, v2.vertex, minDist, maxDist, _Tess);}

关于曲面细分的函数,我们使用Unity的内置函数:

效果如图:

这样我们就完成了——固定数量、基于边长、基于距离的曲面细分方法,但是难道我们就一定只能按照网格的空间几何结构来划分吗?

其实不然:

Shader "Chapter5/chapter5_5_phong"
{Properties{_EdgeLength ("边缘长度", Range(2,50)) = 5   // 边缘长度,控制细分的精度_Phong ("Phong 强度", Range(0,1)) = 0.5    // Phong高光强度_MainTex ("基础纹理 (RGB)", 2D) = "white" {} // 基础纹理,默认为白色_Color ("颜色", color) = (1,1,1,0)            // 颜色,默认为白色}SubShader{Tags{"RenderType"="Opaque" // 渲染类型为不透明}LOD 300 // 设置细节等级CGPROGRAM#pragma surface surf Lambert vertex:dispNone tessellate:tessEdge tessphong:_Phong nolightmap#include "Tessellation.cginc"  // 引入细分曲面相关函数// 顶点数据结构struct appdata{float4 vertex : POSITION;  // 顶点位置float3 normal : NORMAL;    // 法线向量float2 texcoord : TEXCOORD0; // 纹理坐标};// 顶点处理函数,空函数,不做操作void dispNone(inout appdata v){}// Phong高光强度和边缘长度float _Phong;float _EdgeLength;// 细分计算函数,基于边缘长度来决定细分的精度float4 tessEdge(appdata v0, appdata v1, appdata v2){return UnityEdgeLengthBasedTess(v0.vertex, v1.vertex, v2.vertex, _EdgeLength);}// 输入结构体,包含纹理坐标struct Input{float2 uv_MainTex; // 纹理坐标};// 颜色和纹理采样器fixed4 _Color;sampler2D _MainTex;// 表面函数,定义了如何从纹理获取颜色并应用void surf(Input IN, inout SurfaceOutput o){half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; // 从纹理获取颜色并乘以颜色属性o.Albedo = c.rgb;  // 将RGB颜色设置为Albedoo.Alpha = c.a;     // 将Alpha通道设置为透明度}ENDCG}FallBack "Diffuse"  // 如果此Shader不可用,则回退使用“Diffuse”Shader
}

这段代码就是一个基于边长和Phong强度进行曲面细分的着色器代码。

什么是Phong强度?

在代码中:

#pragma surface surf Lambert vertex:dispNone tessellate:tessEdge tessphong:_Phong nolightmap

光照模型我们选择了Lambert,顶点着色器使用dispNone函数,曲面细分使用tessEdge函数而tessphong:_Phong中tessphong代表启用tessphong也就是基于Phong的曲面细分函数。

其他大致没有区别,效果如下:

现在让我们综合上述的内容,尝试做一个比较厉害的东西:水面渲染。

代码如下:

Shader "Chapter5/chapter5_wave"
{Properties{_Color ("Color", Color) = (0,0,1,1) // 水体基础颜色_WaveHeight ("WaveHeight", float) = 1 // 波浪高度_WaveSpeed ("WaveSpeed", float) = 1 // 波浪速度_WaveGap ("WaveGap", float) = 1 // 波浪间隔_FoamTex ("FoamTex", 2D) = "white" {} // 泡沫纹理_NormalTex ("NormalTex", 2D) = "white" {} // 法线贴图_NormalScale ("NormalScale", float) = 1 // 法线强度_NormalSpeed ("NormalSpeed", float) = 1 // 法线贴图移动速度}SubShader{Tags{"RenderType" = "Transparent" // 渲染类型为透明"Queue" = "Transparent" // 渲染队列为透明"IgnoreProjector" = "True" // 忽略投影器}LOD 200 // 细节级别CGPROGRAM// 使用 Standard 光照模型,启用透明混合,保留透明度,启用全向阴影#pragma surface surf Standard alpha:fade keepalpha fullforwardshadows vertex:vertexDataFunc#pragma target 3.0 // 目标着色器模型为 3.0sampler2D _MainTex; // 主纹理(未使用)struct Input{float2 uv_FoamTex; // 泡沫纹理的 UV 坐标float4 screenPos; // 屏幕空间坐标};fixed4 _Color; // 水体基础颜色float _WaveHeight; // 波浪高度float _WaveSpeed; // 波浪速度float _WaveGap; // 波浪间隔sampler2D _NormalTex; // 法线贴图float _NormalScale; // 法线强度float _NormalSpeed; // 法线贴图移动速度// 顶点着色器函数void vertexDataFunc(inout appdata_full v, out Input o){UNITY_INITIALIZE_OUTPUT(Input, o); // 初始化输出结构// 计算波浪高度,使用正弦函数模拟波浪效果fixed height = sin(_Time.y * _WaveSpeed + v.vertex.z * _WaveGap + v.vertex.x) * _WaveHeight;// 根据法线方向调整顶点位置,形成波浪效果v.vertex.xyz += v.normal * height;}// 表面着色器函数void surf(Input IN, inout SurfaceOutputStandard o){// 法线贴图// 根据时间偏移法线贴图的 UV 坐标float2 speed = _Time.x * float2(_WaveSpeed, _WaveSpeed) * _NormalSpeed;// 采样法线贴图,并解包法线fixed3 bump1 = UnpackNormal(tex2D(_NormalTex, IN.uv_FoamTex.xy + speed)).rgb;fixed3 bump2 = UnpackNormal(tex2D(_NormalTex, IN.uv_FoamTex.xy - speed)).rgb;// 合并两个法线贴图的结果fixed3 bump = normalize(bump1 + bump2);// 调整法线强度bump.xy *= _NormalScale;bump = normalize(bump);// 将法线赋值给输出结构o.Normal = bump;// 水体颜色o.Albedo = _Color;// 设置透明度o.Alpha = _Color.a;}ENDCG}FallBack "Diffuse" // 回退到 Diffuse Shader
}

我们来一点一点分析:

        _Color ("Color", Color) = (0,0,1,1) // 水体基础颜色_WaveHeight ("WaveHeight", float) = 1 // 波浪高度_WaveSpeed ("WaveSpeed", float) = 1 // 波浪速度_WaveGap ("WaveGap", float) = 1 // 波浪间隔_FoamTex ("FoamTex", 2D) = "white" {} // 泡沫纹理_NormalTex ("NormalTex", 2D) = "white" {} // 法线贴图_NormalScale ("NormalScale", float) = 1 // 法线强度_NormalSpeed ("NormalSpeed", float) = 1 // 法线贴图移动速度

参数的含义都写在注释里了。

        Tags{"RenderType" = "Transparent" // 渲染类型为透明"Queue" = "Transparent" // 渲染队列为透明"IgnoreProjector" = "True" // 忽略投影器}

这里多了一个IgnoreProjector,这个是我们不常见的Tag,其含义是:

这里的所谓的Projector则是:

        // 使用 Standard 光照模型,启用透明混合,保留透明度,启用全向阴影#pragma surface surf Standard alpha:fade keepalpha fullforwardshadows vertex:vertexDataFunc

 使用表面着色器,标准光照模型,保留透明度。

        // 顶点着色器函数void vertexDataFunc(inout appdata_full v, out Input o){UNITY_INITIALIZE_OUTPUT(Input, o); // 初始化输出结构// 计算波浪高度,使用正弦函数模拟波浪效果fixed height = sin(_Time.y * _WaveSpeed + v.vertex.z * _WaveGap + v.vertex.x) * _WaveHeight;// 根据法线方向调整顶点位置,形成波浪效果v.vertex.xyz += v.normal * height;}

顶点着色器函数,首先可以看到一个

UNITY_INITIALIZE_OUTPUT(Input, o); // 初始化输出结构

这句代码的作用是:

然后是我们模拟波浪的函数:

            // 计算波浪高度,使用正弦函数模拟波浪效果fixed height = sin(_Time.y * _WaveSpeed + v.vertex.z * _WaveGap + v.vertex.x) * _WaveHeight;// 根据法线方向调整顶点位置,形成波浪效果v.vertex.xyz += v.normal * height;

我们根据正弦函数计算得到的实时波浪高度在法线方向修改顶点位置来实现模拟波浪的效果。

然后是表面着色器函数:

        // 表面着色器函数void surf(Input IN, inout SurfaceOutputStandard o){// 法线贴图// 根据时间偏移法线贴图的 UV 坐标float2 speed = _Time.x * float2(_WaveSpeed, _WaveSpeed) * _NormalSpeed;// 采样法线贴图,并解包法线fixed3 bump1 = UnpackNormal(tex2D(_NormalTex, IN.uv_FoamTex.xy + speed)).rgb;fixed3 bump2 = UnpackNormal(tex2D(_NormalTex, IN.uv_FoamTex.xy - speed)).rgb;// 合并两个法线贴图的结果fixed3 bump = normalize(bump1 + bump2);// 调整法线强度bump.xy *= _NormalScale;bump = normalize(bump);// 将法线赋值给输出结构o.Normal = bump;// 水体颜色o.Albedo = _Color;// 设置透明度o.Alpha = _Color.a;}

这里主要看我们对于法线贴图的一系列处理:

我们根据时间偏移乘以波浪速度,然后分别正向和反向采样法线后再叠加在一起,最后调整强度之后返回给输出的结构体。

效果如图:

几何着色器

几何着色器是卡在顶点着色器和片元着色器之间的一个着色器,我们可以选择性地使用。

几何着色器接受的是顶点着色器的输出(曲面细分之后),他最大的作用就是将顶点着色器输出的图元(一个点或者一个图形)修改成其他的不同的图形。直白地说,我们可以这样理解:对于顶点着色器来说,输出的就是一系列经过空间变换的顶点数据包,然后对于这一系列顶点数据包,几何着色器可以将其进行一系列处理达成一些视觉效果等。

[maxcertexcount(N)]
void ShaderName (PrimitiveType InputVertexType InputName[NumElements],inout StreamOutputObjectVertexType) OutputName){// 几何着色器具体实现
}

这是几何着色器的一般写法。

其中:

 且:

概括地说,我们几何着色器的输入必须是点,线,图,多段相邻的线以及多个相邻的图,输出则要求必须每次调用的顶点数乘以输出顶点结构体的标量数小于最大调用顶点数。

现在让我们来用几何着色器分别处理点、线、图输入,用代码实例来加快理解:

首先是输出点的:

Shader "Chapter6/chapter6_point"
{
Properties{_MainTex ("Texture", 2D) = "white" {}  // 声明一个名为_MainTex的2D纹理属性,默认值为白色纹理}SubShader{Tags { "RenderType"="Opaque" }  // 设置渲染类型为Opaque(不透明)LOD 100  // 设置LOD(Level of Detail)为100,表示较低的细节级别Pass{CGPROGRAM#pragma vertex vert  // 使用名为vert的顶点着色器#pragma geometry geom  // 使用名为geom的几何着色器#pragma fragment frag  // 使用名为frag的片段着色器#include "UnityCG.cginc"  // 引入UnityCG常用的着色器库// 输入顶点结构体,包含顶点位置和纹理坐标struct a2v{float4 vertex : POSITION;  // 顶点位置float2 uv : TEXCOORD0;     // 顶点纹理坐标};// 顶点着色器到几何着色器之间的中间结构体struct v2g{float4 vertex : POSITION;  // 顶点位置float2 uv : TEXCOORD0;     // 顶点纹理坐标};// 从几何着色器到片段着色器之间的数据结构struct g2f{float4 vertex : SV_POSITION;  // 变换后的顶点位置float2 uv : TEXCOORD0;        // 纹理坐标};// 声明_MainTex纹理采样器和ST变换矩阵sampler2D _MainTex;float4 _MainTex_ST;// 顶点着色器函数v2g vert (a2v v){v2g o;o.vertex = v.vertex;  // 传递顶点位置o.uv = TRANSFORM_TEX(v.uv, _MainTex);  // 变换纹理坐标return o;}// 几何着色器函数[maxvertexcount(1)]  // 每个几何着色器输出1个顶点void geom(triangle v2g input[3], inout PointStream<g2f> outStream){// 计算输入的3个顶点的平均位置和纹理坐标float4 vertex = float4(0, 0, 0, 0);float2 uv = float2(0, 0);vertex = (input[0].vertex + input[1].vertex + input[2].vertex) / 3;  // 计算平均顶点位置uv = (input[0].uv + input[1].uv + input[2].uv) / 3;  // 计算平均纹理坐标// 输出到片段着色器的数据g2f o;o.vertex = UnityObjectToClipPos(vertex);  // 将顶点位置转换为裁剪空间o.uv = uv;  // 设置纹理坐标outStream.Append(o);  // 将输出数据加入到流中}// 片段着色器函数fixed4 frag (g2f i) : SV_Target{fixed4 col = tex2D(_MainTex, i.uv);  // 从_MainTex纹理中采样颜色return col;  // 返回采样的颜色}ENDCG}}}

            #pragma vertex vert  // 使用名为vert的顶点着色器#pragma geometry geom  // 使用名为geom的几何着色器#pragma fragment frag  // 使用名为frag的片段着色器

可以看到我们在pragma阶段使用了名为geom的几何着色器。

            // 输入顶点结构体,包含顶点位置和纹理坐标struct a2v{float4 vertex : POSITION;  // 顶点位置float2 uv : TEXCOORD0;     // 顶点纹理坐标};// 顶点着色器到几何着色器之间的中间结构体struct v2g{float4 vertex : POSITION;  // 顶点位置float2 uv : TEXCOORD0;     // 顶点纹理坐标};// 从几何着色器到片段着色器之间的数据结构struct g2f{float4 vertex : SV_POSITION;  // 变换后的顶点位置float2 uv : TEXCOORD0;        // 纹理坐标};

定义了三个阶段的作为输入/输出的结构体,主要成员就是顶点位置和顶点纹理坐标。

            // 几何着色器函数[maxvertexcount(1)]  // 每个几何着色器输出1个顶点void geom(triangle v2g input[3], inout PointStream<g2f> outStream){// 计算输入的3个顶点的平均位置和纹理坐标float4 vertex = float4(0, 0, 0, 0);float2 uv = float2(0, 0);vertex = (input[0].vertex + input[1].vertex + input[2].vertex) / 3;  // 计算平均顶点位置uv = (input[0].uv + input[1].uv + input[2].uv) / 3;  // 计算平均纹理坐标// 输出到片段着色器的数据g2f o;o.vertex = UnityObjectToClipPos(vertex);  // 将顶点位置转换为裁剪空间o.uv = uv;  // 设置纹理坐标outStream.Append(o);  // 将输出数据加入到流中}

可以看到我们的参数:

triangle v2g input[3], inout PointStream<g2f> outStream

然后是我们的Append方法:

效果如图:

然后我们来输出线的:

Shader "Chapter6/chapter6_line"
{Properties{_MainTex ("Texture", 2D) = "white" {} // 声明一个名为_MainTex的2D纹理属性,默认值为白色纹理}SubShader{Tags{"RenderType"="Opaque"} // 设置渲染类型为Opaque(不透明)LOD 100 // 设置LOD(Level of Detail)为100,表示较低的细节级别Pass{CGPROGRAM#pragma vertex vert  // 使用名为vert的顶点着色器#pragma geometry geom  // 使用名为geom的几何着色器#pragma fragment frag  // 使用名为frag的片段着色器#include "UnityCG.cginc"  // 引入UnityCG常用的着色器库// 输入顶点结构体,包含顶点位置和纹理坐标struct a2v{float4 vertex : POSITION; // 顶点位置float2 uv : TEXCOORD0; // 顶点纹理坐标};// 顶点着色器到几何着色器之间的中间结构体struct v2g{float4 vertex : POSITION; // 顶点位置float2 uv : TEXCOORD0; // 顶点纹理坐标};// 从几何着色器到片段着色器之间的数据结构struct g2f{float4 vertex : SV_POSITION; // 变换后的顶点位置float2 uv : TEXCOORD0; // 纹理坐标};// 声明_MainTex纹理采样器和ST变换矩阵sampler2D _MainTex;float4 _MainTex_ST;// 顶点着色器函数v2g vert(a2v v){v2g o;o.vertex = v.vertex; // 传递顶点位置o.uv = TRANSFORM_TEX(v.uv, _MainTex); // 变换纹理坐标return o;}// 几何着色器函数[maxvertexcount(6)] // 最大顶点输出数为6个(因为每条边输出2个顶点,总共3条边)void geom(triangle v2g input[3], inout LineStream<g2f> outStream){// 遍历三角形的3个顶点for (int i = 0; i < 3; i++){g2f o;// 处理当前顶点,将其转换为裁剪空间坐标并设置纹理坐标o.vertex = UnityObjectToClipPos(input[i].vertex); // 将顶点位置从对象空间转换到裁剪空间o.uv = input[i].uv; // 传递纹理坐标// 将当前顶点添加到输出流outStream.Append(o);// 计算当前顶点的下一个顶点(形成边)int next = (i + 1) % 3; // 使用环形索引来获得下一个顶点// 处理下一个顶点o.vertex = UnityObjectToClipPos(input[next].vertex); // 将下一个顶点的位置转换到裁剪空间o.uv = input[next].uv; // 传递下一个顶点的纹理坐标// 将下一个顶点添加到输出流outStream.Append(o);}}// 片段着色器函数fixed4 frag(g2f i) : SV_Target{fixed4 col = tex2D(_MainTex, i.uv); // 从_MainTex纹理中采样颜色return col; // 返回采样的颜色}ENDCG}}}

可以看到我们的几何着色器有所变化:

            // 几何着色器函数[maxvertexcount(6)] // 最大顶点输出数为6个(因为每条边输出2个顶点,总共3条边)void geom(triangle v2g input[3], inout LineStream<g2f> outStream){// 遍历三角形的3个顶点for (int i = 0; i < 3; i++){g2f o;// 处理当前顶点,将其转换为裁剪空间坐标并设置纹理坐标o.vertex = UnityObjectToClipPos(input[i].vertex); // 将顶点位置从对象空间转换到裁剪空间o.uv = input[i].uv; // 传递纹理坐标// 将当前顶点添加到输出流outStream.Append(o);// 计算当前顶点的下一个顶点(形成边)int next = (i + 1) % 3; // 使用环形索引来获得下一个顶点// 处理下一个顶点o.vertex = UnityObjectToClipPos(input[next].vertex); // 将下一个顶点的位置转换到裁剪空间o.uv = input[next].uv; // 传递下一个顶点的纹理坐标// 将下一个顶点添加到输出流outStream.Append(o);}}

分别遍历三个点并且将该点添加到输出流中。

效果如图:

关于输出面的:如果只是照常输出面的话我们只能看到一个正常的球,没啥意思,所以我们来做点新东西。

Shader "Chapter6/chapter6_triangle_ani"
{Properties{_MainTex ("Texture", 2D) = "white" {} // 主纹理_BottomColor ("Bottom Color", Color) = (0, 0, 0, 1) // 底部颜色_TopColor ("Top Color", Color) = (1, 1, 1, 1) // 顶部颜色_ExtrudeMaxValue ("Extrude Max Value", Range(0, 1)) = 1 // 最大挤出值_ExtrudeRandomValue ("Extrude Random Value", Range(0, 1)) = 1 // 随机挤出值_ExtrudeSpeed ("Extrude Speed", Float) = 1 // 挤出速度}SubShader{Tags{"RenderType"="Opaque"} // 渲染类型为不透明LOD 100 // 细节级别Pass{Cull Off // 关闭背面剔除CGPROGRAM#pragma vertex vert  // 顶点着色器#pragma geometry geom  // 几何着色器#pragma fragment frag  // 片段着色器#include "UnityCG.cginc"  // 包含Unity的CG库// 顶点着色器输入结构struct a2v{float4 vertex : POSITION; // 顶点位置float2 uv : TEXCOORD0; // 纹理坐标};// 顶点着色器输出结构struct v2g{float4 vertex : POSITION; // 顶点位置float2 uv : TEXCOORD0; // 纹理坐标};// 几何着色器输出结构struct g2f{float4 vertex : SV_POSITION; // 裁剪空间中的顶点位置float2 uv : TEXCOORD0; // 纹理坐标float4 color : COLOR; // 顶点颜色};sampler2D _MainTex; // 主纹理float4 _MainTex_ST; // 主纹理的缩放和偏移float4 _BottomColor; // 底部颜色float4 _TopColor; // 顶部颜色float _ExtrudeMaxValue; // 最大挤出值float _ExtrudeRandomValue; // 随机挤出值float _ExtrudeSpeed; // 挤出速度// 顶点着色器v2g vert(a2v v){v2g o;o.vertex = v.vertex; // 传递顶点位置o.uv = TRANSFORM_TEX(v.uv, _MainTex); // 转换纹理坐标return o;}// 计算法线float3 ConstructNormal(float3 v1, float3 v2, float3 v3){return normalize(cross(v2 - v1, v3 - v1)); // 通过叉积计算法线}// 几何着色器[maxvertexcount(21)] // 最大顶点数void geom(triangle v2g input[3], uint pid : SV_PrimitiveID, inout TriangleStream<g2f> outStream){// 计算挤出量,基于时间和速度// 计算基础挤出量:基于时间的周期性变化float baseExtrude = cos(_Time.y * 2 * UNITY_PI * _ExtrudeSpeed); // 范围 [-1, 1]baseExtrude = baseExtrude * 0.5 + 0.5; // 映射到 [0, 1]baseExtrude *= _ExtrudeMaxValue; // 控制挤出量的最大幅度// 计算随机挤出量:基于时间和图元 ID 的随机变化float randomSeed = pid * 853.8425415; // 使用图元 ID 作为随机种子float randomExtrude = sin(randomSeed + _Time.y * 2 * UNITY_PI * _ExtrudeSpeed); // 范围 [-1, 1]randomExtrude = randomExtrude * 0.5 + 0.5; // 映射到 [0, 1]randomExtrude *= _ExtrudeRandomValue; // 控制随机挤出量的强度// 最终挤出量 = 基础挤出量 + 随机挤出量float extrudeAmount = baseExtrude + randomExtrude;// 计算法线并乘以挤出量float3 normal = ConstructNormal(input[0].vertex, input[1].vertex, input[2].vertex);normal = normal * extrudeAmount;g2f o;// 生成三角形的侧面for (int i = 0; i < 3; i++){int index = (i + 1) % 3;// 底部顶点o.vertex = input[i].vertex;o.vertex = UnityObjectToClipPos(o.vertex);o.uv = input[i].uv;o.color = _BottomColor;outStream.Append(o);// 顶部顶点o.vertex = input[i].vertex;o.vertex.xyz += normal;o.vertex = UnityObjectToClipPos(o.vertex);o.uv = input[i].uv;o.color = _TopColor;outStream.Append(o);// 下一个底部顶点o.vertex = input[index].vertex;o.vertex = UnityObjectToClipPos(o.vertex);o.uv = input[index].uv;o.color = _BottomColor;outStream.Append(o);outStream.RestartStrip(); // 结束当前三角形带// 顶部顶点o.vertex = input[i].vertex;o.vertex.xyz += normal;o.vertex = UnityObjectToClipPos(o.vertex);o.uv = input[i].uv;o.color = _TopColor;outStream.Append(o);// 下一个底部顶点o.vertex = input[index].vertex;o.vertex = UnityObjectToClipPos(o.vertex);o.uv = input[index].uv;o.color = _BottomColor;outStream.Append(o);// 下一个顶部顶点o.vertex = input[index].vertex;o.vertex.xyz += normal;o.vertex = UnityObjectToClipPos(o.vertex);o.uv = input[index].uv;o.color = _TopColor;outStream.Append(o);outStream.RestartStrip(); // 结束当前三角形带}// 生成顶部三角形for (int i = 0; i < 3; i++){o.vertex = input[i].vertex;o.vertex.xyz += normal;o.vertex = UnityObjectToClipPos(o.vertex);o.uv = input[i].uv;o.color = _TopColor;outStream.Append(o);}outStream.RestartStrip(); // 结束当前三角形带}// 片段着色器fixed4 frag(g2f i) : SV_Target{fixed4 col = tex2D(_MainTex, i.uv) * i.color; // 采样纹理并乘以颜色return col;}ENDCG}}
}
    Properties{_MainTex ("Texture", 2D) = "white" {} // 主纹理_BottomColor ("Bottom Color", Color) = (0, 0, 0, 1) // 底部颜色_TopColor ("Top Color", Color) = (1, 1, 1, 1) // 顶部颜色_ExtrudeMaxValue ("Extrude Max Value", Range(0, 1)) = 1 // 最大挤出值_ExtrudeRandomValue ("Extrude Random Value", Range(0, 1)) = 1 // 随机挤出值_ExtrudeSpeed ("Extrude Speed", Float) = 1 // 挤出速度}

设置了一个最大挤出值和随机挤出值,保证挤出值小于等于最大挤出值,然后每一个面的挤出值都是随机的。

依然主要看几何着色器:

// 几何着色器
[maxvertexcount(21)] // 最大顶点数
void geom(triangle v2g input[3], uint pid : SV_PrimitiveID, inout TriangleStream<g2f> outStream)
{// 计算挤出量,基于时间和速度// 计算基础挤出量:基于时间的周期性变化float baseExtrude = cos(_Time.y * 2 * UNITY_PI * _ExtrudeSpeed); // 范围 [-1, 1]baseExtrude = baseExtrude * 0.5 + 0.5; // 映射到 [0, 1]baseExtrude *= _ExtrudeMaxValue; // 控制挤出量的最大幅度// 计算随机挤出量:基于时间和图元 ID 的随机变化float randomSeed = pid * 853.8425415; // 使用图元 ID 作为随机种子float randomExtrude = sin(randomSeed + _Time.y * 2 * UNITY_PI * _ExtrudeSpeed); // 范围 [-1, 1]randomExtrude = randomExtrude * 0.5 + 0.5; // 映射到 [0, 1]randomExtrude *= _ExtrudeRandomValue; // 控制随机挤出量的强度// 最终挤出量 = 基础挤出量 + 随机挤出量float extrudeAmount = baseExtrude + randomExtrude;// 计算法线并乘以挤出量float3 normal = ConstructNormal(input[0].vertex, input[1].vertex, input[2].vertex);normal = normal * extrudeAmount;g2f o;// 生成三角形的侧面for (int i = 0; i < 3; i++){int index = (i + 1) % 3;// 底部顶点o.vertex = input[i].vertex;o.vertex = UnityObjectToClipPos(o.vertex);o.uv = input[i].uv;o.color = _BottomColor;outStream.Append(o);// 顶部顶点o.vertex = input[i].vertex;o.vertex.xyz += normal;o.vertex = UnityObjectToClipPos(o.vertex);o.uv = input[i].uv;o.color = _TopColor;outStream.Append(o);// 下一个底部顶点o.vertex = input[index].vertex;o.vertex = UnityObjectToClipPos(o.vertex);o.uv = input[index].uv;o.color = _BottomColor;outStream.Append(o);outStream.RestartStrip(); // 结束当前三角形带// 顶部顶点o.vertex = input[i].vertex;o.vertex.xyz += normal;o.vertex = UnityObjectToClipPos(o.vertex);o.uv = input[i].uv;o.color = _TopColor;outStream.Append(o);// 下一个底部顶点o.vertex = input[index].vertex;o.vertex = UnityObjectToClipPos(o.vertex);o.uv = input[index].uv;o.color = _BottomColor;outStream.Append(o);// 下一个顶部顶点o.vertex = input[index].vertex;o.vertex.xyz += normal;o.vertex = UnityObjectToClipPos(o.vertex);o.uv = input[index].uv;o.color = _TopColor;outStream.Append(o);outStream.RestartStrip(); // 结束当前三角形带}// 生成顶部三角形for (int i = 0; i < 3; i++){o.vertex = input[i].vertex;o.vertex.xyz += normal;o.vertex = UnityObjectToClipPos(o.vertex);o.uv = input[i].uv;o.color = _TopColor;outStream.Append(o);}outStream.RestartStrip(); // 结束当前三角形带
}
[maxvertexcount(21)] // 最大顶点数
void geom(triangle v2g input[3], uint pid : SV_PrimitiveID, inout TriangleStream<g2f> outStream)

最大顶点数21,参数中接受顶点着色器的三角形输出(三个顶点),然后是一个独一的三角形图元ID,输出一个三角形图元流。

                // 计算挤出量,基于时间和速度// 计算基础挤出量:基于时间的周期性变化float baseExtrude = cos(_Time.y * 2 * UNITY_PI * _ExtrudeSpeed); // 范围 [-1, 1]baseExtrude = baseExtrude * 0.5 + 0.5; // 映射到 [0, 1]baseExtrude *= _ExtrudeMaxValue; // 控制挤出量的最大幅度

计算挤出的量,用一个cos函数得到初始值,移动到[0,1]区间之后乘以最大挤出量。

           // 计算随机挤出量:基于时间和图元 ID 的随机变化float randomSeed = pid * 853.8425415; // 使用图元 ID 作为随机种子float randomExtrude = sin(randomSeed + _Time.y * 2 * UNITY_PI * _ExtrudeSpeed); // 范围 [-1, 1]randomExtrude = randomExtrude * 0.5 + 0.5; // 映射到 [0, 1]randomExtrude *= _ExtrudeRandomValue; // 控制随机挤出量的强度

这部分是随机的挤出量,这里可以看到我们生成随机种子的方法:用我们的独一的图元ID去乘以一个数。

                // 最终挤出量 = 基础挤出量 + 随机挤出量float extrudeAmount = baseExtrude + randomExtrude;// 计算法线并乘以挤出量float3 normal = ConstructNormal(input[0].vertex, input[1].vertex, input[2].vertex);normal = normal * extrudeAmount;

最终挤出量等于基础挤出量加上随机挤出量,我们计算出法线之后乘以这个最终挤出量即可。

效果如图:

相关文章:

Unity-Shader详解-其四

今天我们来聊Unity特有的表面着色器以及很少提到的几何着色器。 表面着色器 在前文关于光照的计算中&#xff0c;我们学会了很多&#xff1a;我们学习了一系列光照模型&#xff0c;比如专门针对漫反射的兰伯特模型和改进的半兰伯特模型&#xff0c;又比如由高光、漫反射和环境…...

Lua 元表和元方法

元表(Metatable)和元方法(Metamethod)是Lua中实现面向对象编程、操作符重载和自定义行为的重要机制。 元表 元表是一个普通的Lua表&#xff0c;可以附加到另一个表上&#xff0c;用于定义或修改该表的行为。每个表都可以有自己的元表。 setmetatable(tab,metatab) 将metatab设…...

GESP2024年3月认证C++八级( 第二部分判断题(6-10))

海伦公式参考程序&#xff1a; #include <iostream> #include <cmath> // 引入cmath库以使用sqrt函数using namespace std;double calculateTriangleArea(int a, int b, int c) {// 使用海伦公式double s (a b c) / 2.0; // 半周长return sqrt(s * (s - a) *…...

Nacos源码—3.Nacos集群高可用分析一

大纲 1.Nacos集群的几个问题 2.单节点对服务进行心跳健康检查和同步检查结果 3.集群新增服务实例时如何同步给其他节点 4.集群节点的健康状态变动时的数据同步 5.集群新增节点时如何同步已有服务实例数据 1.Nacos集群的几个问题 问题一&#xff1a;在单机模式下&#xff…...

信息系统项目管理师-软考高级(软考高项)​​​​​​​​​​​2025最新(九)

个人笔记整理---仅供参考 第九章项目范围管理 9.1管理基础 9.2项目范围管理过程 9.3规划范围管理 9.4收集需求 9.5定义范围 9.6创建WBS 9.7确认范围 9.8控制范围...

DeepSeek学术论文写作全流程指令

一、选题与领域界定 研究热点捕捉 指令: “在[研究领域]中,现有文献对[具体问题]的[哪方面]研究不足?基于近5年文献归纳3个待突破方向,需结合高频关键词和交叉学科维度。” 示例: “在深度学习医疗影像分析中,现有文献对小样本训练的泛化性研究不足?基于2019-2023年顶会…...

【ArUco boards】标定板检测

之前定位用的Charuco标定板做的&#xff08;https://blog.csdn.net/qq_45445740/article/details/143897238&#xff09;&#xff0c;因为实际工况中对标定板的尺寸有要求&#xff0c;大概是3cm*2cm这个尺寸&#xff0c;加上选用的是ChAruco标定板&#xff0c;导致每一个aruco码…...

2025 年 408 真题及答案

2025 年 408 真题 历年408真题及答案下载直通车 1、以下 C 代码的时间复杂度是多少&#xff1f;&#xff08;&#xff09; int count 0; for (int i0; i*i<n; i)for (int j0; j<i; j)count;A O(log2n)B O(n)C O(nlogn)D O(n2) 2、对于括号匹配问题&#xff0c;符号栈…...

设计模式每日硬核训练 Day 18:备忘录模式(Memento Pattern)完整讲解与实战应用

&#x1f504; 回顾 Day 17&#xff1a;中介者模式小结 在 Day 17 中&#xff0c;我们学习了中介者模式&#xff08;Mediator Pattern&#xff09;&#xff1a; 用一个中介者集中管理对象之间的通信。降低对象之间的耦合&#xff0c;适用于聊天系统、GUI 控件联动、塔台调度等…...

ByteArrayOutputStream 类详解

ByteArrayOutputStream 类详解 ByteArrayOutputStream 是 Java 中用于在内存中动态写入字节数据的输出流(ByteArrayOutputStream和ByteArrayInputStream是节点流),位于 java.io 包。它不需要关联物理文件或网络连接,所有数据都存储在内存的字节数组中。 1. 核心特性 内存缓冲…...

Linux中web服务器的部署及优化

前言&#xff1a;Nginx 和 Apache HTTP Server 是两款非常流行的 Web 服务器。 Nginx 简介&#xff1a;Nginx 是一款轻量级的高性能 Web 服务器、反向代理服务器以及电子邮件&#xff08;IMAP/POP3&#xff09;代理服务器。由俄罗斯人伊戈尔・赛索耶夫开发&#xff0c;其在处…...

使用Mathematica绘制Sierpinski地毯

在Mathematica中内置的绘制Sierpinski地毯的函数&#xff1a; SierpinskiCurve[n] gives the line segments representing the n-step Sierpiński curve. 注意&#xff0c;直接运行这个函数&#xff0c;返回的是Line对象&#xff0c;例如&#xff1a; 运行如下代码&#xf…...

Qt 信号槽机制底层原理学习

简介 Qt的信号和槽&#xff08;Signals and Slots&#xff09;是Qt开发团队创造的一种特殊回调机制&#xff0c;提供了非常简洁易用的事件触发-函数调用机制。 原理学习 虽然上层使用简单&#xff0c;但底层实现机制却复杂的不得了&#xff0c;这里简单的学习一下大概原理。…...

【Java学习笔记】包

包&#xff08;package&#xff09; 包的本质&#xff1a;实际上就是创建不同的文件夹或者目录来保存类文件 包的三大作用 区分相同名字的类 当类很多的时候可以更方便的管理类 控制访问范围 使用方法 关键字&#xff1a;import—>导入&#xff08;引入&#xff09; …...

进程的程序替换——exec系列函数的使用

目录 前言 一、替换函数 二、程序替换的本质 一些细节&#xff1a; 三、程序替换与环境变量间的关系 1.介绍其他参数的意义并总结 2.自定义环境变量 1&#xff09;通过execcle传参全局环境变量 2&#xff09;通过execcle传参自定义环境变量 3&#xff09;将自定义环境变量通过p…...

【论文阅读】DETR+Deformable DETR

可变形注意力是目前transformer结构中经常使用的一种注意力机制&#xff0c;最近补了一下这类注意力的论文&#xff0c;提出可变形注意力的论文叫Deformable DETR&#xff0c;是在DETR的基础上进行的改进&#xff0c;所以顺带着把原本的DETR也看了一下。 一、DETR DETR本身是…...

ArchLinux卡死在GRUB命令行模式修复

ArchLinux卡死在GRUB命令行模式修复 文章目录 ArchLinux卡死在GRUB命令行模式修复前言一、 系统配置1.系统配置2.磁盘分区信息 二、重建GRUB引导1.插入带ArchLinux ISO的U盘&#xff0c;BIOS选择U盘启动并进入ArchLinux安装界面。2.挂载btrfs根目录分区3.挂载/boot分区4.进入ch…...

Docker 容器 - Dockerfile

Docker 容器 - Dockerfile 一、Dockerfile 基本结构二、Dockerfile 指令详解2.1 FROM2.2 MAINTAINER2.3 COPY2.4 ADD2.5 WORKDIR2.6 VOLUME2.7 EXPOSE2.8 ENV2.9 RUN2.10 CMD2.11 ENTRYPOINT 三、Dockerfile 创建镜像与模板3.1 Dockerfile 镜像3.2 镜像管理3.3 Dockerfile 模板…...

C++ 中二级指针的正确释放方法

C 中二级指针的正确释放 一、什么是二级指针&#xff1f; 简单说&#xff0c;二级指针就是指向指针的指针。 即&#xff1a; int** p;它可以指向一个 int*&#xff0c;而 int* 又指向一个 int 类型的变量。 常见应用场景 动态二维数组&#xff08;例如 int** matrix&#x…...

解释器模式(Interpreter Pattern)

解释器模式&#xff08;Interpreter Pattern&#xff09; 是行为型设计模式之一&#xff0c;通常用于处理“语言”类问题&#xff0c;比如计算器、编程语言的解析等。它的核心思想是通过建立一个解释器&#xff0c;解析并解释由语法规则描述的语言&#xff0c;通常以**抽象语法…...

编译原理期末重点-个人总结——1 概论

概述 计算机语言的分类 低级语言&#xff1a;机器语言&#xff08;唯一能被计算机执行的&#xff09;&#xff0c;汇编语言 高级语言&#xff1a;JAVA &#xff0c;C 执行高级语言或汇编语言的步骤 高级语言程序或汇编语言程序> &#xff08;通过解释 或 翻译&#xff09;转…...

五一作业-day04

文章目录 1. **ps -ef是显示当前系统进程的命令,统计下当前系统一共有多少进程**2. **last命令用于显示所用用户最近1次登录情况,awk可以取出某一列,现在要取出last命令第1列并去重统计次数**3. **secure日志是用户的登录日志,过滤出secure日志中的Failed password的次数(用课堂…...

Java按字节长度截取字符串指南

在Java中&#xff0c;由于字符串可能包含多字节字符(如中文)&#xff0c;直接按字节长度截取可能会导致乱码或截取不准确的问题。以下是几种按字节长度截取字符串的方法&#xff1a; 方法一&#xff1a;使用String的getBytes方法 java public static String substringByBytes(…...

[特殊字符]Git 操作实战:如何将本地项目提交到远程 Gitee 仓库

在日常开发中&#xff0c;我们经常需要将本地开发的项目同步到远程代码仓库中&#xff08;如 GitHub、Gitee 等&#xff09;&#xff0c;以便团队协作或备份管理。本文将以 Gitee&#xff08;码云&#xff09; 为例&#xff0c;详细讲解如何将本地已有项目提交到远程仓库&#…...

【信息系统项目管理师-论文真题】2008上半年论文详解(包括解题思路和写作要点)

更多内容请见: 备考信息系统项目管理师-专栏介绍和目录 文章目录 试题1:企业级信息系统项目管理体系的建立1、写作要点2、解题思路项目管理流程和项目管理的工具试题2:项目的质量管理1、写作要点2、解题思路项目的早期阶段如何制定项目质量管理计划如何确保项目质量管理计划…...

C语言|函数的递归调用

函数的递归调用 (逐层分解&#xff0c;逐层合并) 自己调用自己&#xff0c;必须要知道什么时候停止调用&#xff0c;不然会造成电脑死机。 【知识点】 1 函数调用是通过栈实现的。 多个函数嵌套调用时&#xff0c;会按照先调用后返回的原则进行返回。 2 函数递归必须满足的两…...

QT 在圆的边界画出圆

QT 在圆的边界画出圆 QT 在圆的边界画出实心圆 在Qt中&#xff0c;要实现在圆的边界上绘制图形&#xff0c;你需要使用QPainter类来在QWidget或其子类的paintEvent中绘制。下面我将通过一个简单的例子来说明如何在Qt中绘制一个圆&#xff0c;并在其边界上绘制其他图形&#x…...

Guass数据库实验(数据字典设计、交叉表设计)

Assignment 2: Database Design 目录 Assignment 2: Database Design 数据库创建 新建用户bit&#xff0c;并创建数据库模式ass2 使用datastdui以该用户远程登陆 创建学科数据字典相关表 学科门类表 一级学科表 二级学科表 三级学科表 学科变更历史表 插入数据字典…...

算法题(139):牛可乐和魔法封印

审题&#xff1a; 本题需要我们将数组中包含在区间x~y之间的数据个数找到并输出 思路&#xff1a; 方法一&#xff1a;暴力解法 首先我们可以直接遍历一次数组&#xff0c;找到x的索引&#xff0c;然后再找到y的索引&#xff0c;并计算最终的元素个数&#xff0c;这里就要有O&a…...

LeetCode热题100--189.轮转数组--中等

1. 题目 给定一个整数数组 nums&#xff0c;将数组中的元素向右轮转 k 个位置&#xff0c;其中 k 是非负数。 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步: [5,6,…...

DeepSeek-Prover-V2:数学定理证明领域的新突破

前言 在人工智能飞速发展的当下&#xff0c;模型的迭代与创新层出不穷。 五一假期期间&#xff0c;DeepSeek 再次发力&#xff0c;推出了令人瞩目的新模型 ——DeepSeek-Prover-V2。 与大众期待的 R2 通用推理模型不同&#xff0c;这次 DeepSeek 将目光聚焦于数学定理证明领…...

调试——GDB、日志

调试——GDB、日志 1. gdb常用指令2. 如何生成core文件并调试&#xff1f;3. 如何调试正在运行的程序4. 调试多进程程序5. 调试多线程程序6. log日志 gcc编译器可以帮我们发现语法错误&#xff0c;但是对业务逻辑错误却无能为力。当我们想找出逻辑错误时&#xff0c;就需要调试…...

ARM子程序调用与返回

子程序&#xff08;也叫过程、函数、方法&#xff09;是一个能被调用和执行并返回到调用点那条指令的代码 段。 两个问题&#xff1a;如何将参数传递给子程序或从子程序中传递出来&#xff1f;怎么从子程序返回到调用点&#xff1f; 指令BSR Proc_A调用子程序Proc_A。 处理器将…...

WSL 安装 Debian 后,apt get 如何更改到国内镜像网址?

提问&#xff1a;Debian apt install 如何更改到国内镜像网址&#xff1f; 在 Debian 系统中&#xff0c;你可以通过修改 /etc/apt/sources.list 文件&#xff0c;将软件源更改为国内镜像网址&#xff0c;以加快软件包的下载速度。下面为你详细介绍操作步骤&#xff1a; 1. 备…...

SpringCloud GateWay网关

1、网关介绍 微服务网关&#xff08;Microservices Gateway&#xff09;是微服务架构中的核心组件&#xff0c;充当所有客户端请求的统一入口&#xff0c;负责请求的路由、过滤和聚合等操作。它是微服务与外部系统&#xff08;如Web、移动端&#xff09;之间的中间层&#xff0…...

可视化大屏开发全攻略:技术与实践指南

引言 在数字化浪潮席卷全球的当下&#xff0c;数据已成为企业乃至整个社会发展的核心驱动力。从繁华都市的交通管控中心&#xff0c;到大型企业的数据运营中枢&#xff0c;可视化大屏无处不在&#xff0c;以直观、震撼的方式展示着数据的魅力与价值。它就像是一扇通往数据世界…...

如何设计一个为QStackWidget的界面切换动画?

目录 前言 接口考虑 实现的思路 前言 笔者这段时间沉迷于给我的下位机I.MX6ULL做桌面&#xff0c;这里抽空更新一下QT的东西。这篇文章是跟随CCMoveWidget一样的文章&#xff0c;尝试分享自己如何书写这份代码的思考的过程 接口考虑 笔者不太想使用继承的方式重新写我们的…...

LeetCode 0790.多米诺和托米诺平铺:难想条件的简单动态规划

【LetMeFly】790.多米诺和托米诺平铺&#xff1a;难想条件的简单动态规划 力扣题目链接&#xff1a;https://leetcode.cn/problems/domino-and-tromino-tiling/ 有两种形状的瓷砖&#xff1a;一种是 2 x 1 的多米诺形&#xff0c;另一种是形如 "L" 的托米诺形。两种…...

模拟芯片设计中数字信号处理一些常用概念(一)

模拟芯片设计中经常用时域场景思考来解决问题,但实际上很多地方如果采用频域角度思考,解决问题更快更方便。 时域和频域的对照关系如下: a、如果时域信号是周期的,那么它的频谱就是离散的。 b、如果时域信号是非周期的,那么它的频谱就是连续的。 c、如果时域信号是离散的…...

c++进阶——AVL树主要功能的模拟实现(附带旋转操作讲解)

文章目录 AVL树的实现AVL树的概念及引入AVL树调整问题AVL树的实现AVL树的结构AVL树的插入插入的流程更新平衡因子的原则实现插入的基本框架(插入 调整平衡因子)旋转操作右单旋左单旋左右双旋右左双旋 合并旋转代码 测试部分平衡检测接口测试用例 对于其他接口的说明 AVL树的实…...

一个电商场景串联23种设计模式:创建型、结构型和行为型

理解了&#xff01;你希望有一个具体的项目案例&#xff0c;能够涵盖所有23种设计模式&#xff0c;并且将它们分类为创建型、结构型和行为型。这个需求非常好&#xff0c;能够帮助你从实际的应用场景理解每种设计模式的用法。 为了实现这个目标&#xff0c;我将为你设计一个电…...

浅拷贝和深拷贝的区别

Person p1 new Person(10);Person p2 p1;p2.age 20;System.out.println(p1p2); // trueSystem.out.println(p1.age); // 20 这种做法只是复制了对象的地址&#xff0c;即两个变量现在是指向了同一个对象&#xff0c;任意一个变量&#xff0c;操作了对象的属性&#xff0c;都…...

Java开发者面试实录:微服务架构与Spring Cloud的应用

面试场景 面试官: 请介绍一下你的基本情况。 程序员: 大家好&#xff0c;我叫张小明&#xff0c;今年27岁&#xff0c;硕士学历&#xff0c;拥有5年的Java后端开发经验。主要负责基于Spring Boot开发企业级应用&#xff0c;以及微服务架构的设计和实现。 面试官: 好的&#…...

在Ubuntu系统中安装桌面环境

在 Ubuntu 系统中安装桌面环境可以通过包管理器 apt 或工具 tasksel 实现。以下是详细的安装方法和常见桌面环境的选择&#xff1a; --- ### **1. 准备系统更新** 在安装前&#xff0c;建议更新软件源和系统包&#xff1a; bash sudo apt update && sudo apt upgrade…...

多语言笔记系列:Polyglot Notebooks 中使用 xUnit 单元测试

Polyglot Notebooks 中使用 xUnit 单元测试 本文目录 Polyglot Notebooks 中使用 xUnit 单元测试[TOC](本文目录)Polgylot Notebooks 并没有直接支持单元测试框架。不能像VS里那样方便的进行单元测试。简单远行的话&#xff0c;可以使用下面的方案&#xff01;1、引入必要的NuG…...

Cisco Packet Tracer 选项卡的使用

目录 设备Config选项卡的使用 Realtime and Simulation模式&#xff08;数据包跟踪与分析&#xff09; 设备Desktop选项卡的使用 设备Config选项卡的使用 Hostname NVRAM Startup Config----Load 加载 INTERFACE 点击on Save 如果&#xff0c;不把Running Config保存为Sta…...

杨校老师竞赛课之C++备战蓝桥杯初级组省赛

目录 1. 灯塔 题目描述 输入描述 输出描述 输入样例1 输出样例1 输入样例2 输出样例2 数据说明 2. 子区间 题目描述 输入描述 输出描述 输入样例 输出样例 数据说明 3. 染色 题目描述 输入描述 输出描述 输入样例1 输出样例1 输入样例2 输出样例2 数据…...

gcc/g++用法摘记

链接静态库 gcc main.o -L/path/to/libs -lmylib -o myprogram 【待续】...

kotlin 扩展函数

Kotlin 扩展函数的定义与使用 定义扩展函数 Kotlin 的扩展函数是一种强大的机制&#xff0c;允许开发者为已有的类添加额外的功能&#xff0c;而无需继承该类或对其进行任何修改。这种特性极大地提高了代码的灵活性和可读性。 扩展函数可以通过在函数名称前指定目标类型的接…...

机器人强化学习入门学习笔记

(1)物理引擎 物理引擎就是模拟真实世界物理规律的软件工具。它会根据你给定的物体、质量、形状、力等信息,计算这些物体在时间上的运动和相互作用。如果你设计了一个机器人,那物理引擎就是“虚拟现实世界”,让机器人在里面“活起来”,模拟它走路、抓东西、摔倒等动作。而…...