[Shader] Shader Cookbook 高级着色技术[9]

RuntimeMapMaker3D-Pro在这里插入图片描述


  本章将介绍一些你可以在游戏中使用的高级着色器技术。你应该记住,你在游戏中看到的许多最引人注目的效果都是通过测试着色器的极限而产生的。这本书为你提供了修改和创建着色器的技术基础,但强烈鼓励你尽可能多地玩和实验它们。制作一款优秀的游戏并不是为了追求真实感;你不应该带着复制现实的意图接近着色器,因为这是不可能发生的。相反地,你应该尝试着将着色器作为一种工具去创造出真正独特的游戏。有了本章的知识,你将能够创建你想要的材质。

在本章中,你将学习以下内容:

  • 实现一个毛皮着色器
  • 使用数组实现热图

实现一个毛皮着色器

  材质的外观取决于它的物理结构。着色器试图模拟它们,但是,在这样做的时候,他们过度简化了光的行为方式。具有复杂宏观结构的材料尤其难以渲染。许多织物和动物皮草都是如此。这个配方将向你展示如何模拟皮毛和其他材质(如草),这不仅仅是一个平坦的表面。为了做到这一点,相同的材质被多次绘制,每次都增加大小。它制造了一种皮毛的错觉。

这里展示的着色器是基于Jonathan Czeck和Aras pranckeviius的工作:

在这里插入图片描述

准备

为了使这个配方奏效,你需要一个纹理来显示你希望如何展示你的皮毛:
在这里插入图片描述

我在 Chapter 12 | Textures 文件夹中提供了两个示例代码(人造皮毛和熊猫)。

  就像之前的所有其他着色器一样,你需要创建一个新的标准表面着色器(Fur)和一个材质(FurMat)来承载它,并将其附加到一个球体上进行演示:
在这里插入图片描述

怎么做……

在这个方法中,我们可以开始修改一个标准表面着色器:

  1. 双击Fur Shader在你选择的IDE中打开它。打开后,添加以下粗体属性
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
	_FurLength ("Fur Length", Range (. 0002, 1) ) = . 25
	_Cutoff ("Alpha Cutoff", Range(0, 1) ) = 0. 5
	// how "thick"
	_CutoffEnd ("Alpha Cutoff end", Range(0, 1) ) = 0. 5
	// how thick they are at the end
	_EdgeFade ("Edge Fade", Range(0, 1) ) = 0. 4
	_Gravity ("Gravity Direction", Vector) = (0, 0, 1, 0)
	_GravityStrength ("Gravity Strength", Range(0, 1) ) = 0. 25
}
  1. 这个着色器需要你重复相同的pass几次。我们将使用在使用CgInclude recipe使您的着色器以模块化的方式工作中介绍的技术来将所有必要的代码从一个外部包中单个传递分组。让我们开始创建一个新的CgInclude,名为FurPass.Cginc,代码如下:
#pragma target 3. 0
fixed4 _Color;
sampler2D _MainTex;
half _Glossiness;
half _Metallic;
uniform float _FurLength;
uniform float _Cutoff;
uniform float _CutoffEnd;
uniform float _EdgeFade;
uniform fixed3 _Gravity;
uniform fixed _GravityStrength;
void vert (inout appdata_full v)
{
	fixed3 direction = lerp(v. normal, _Gravity *
	_GravityStrength + v. normal * (1-
	_GravityStrength) , FUR_MULTIPLIER) ;
	v.vertex.xyz += direction * _FurLength * FUR_MULTIPLIER * v.color. a;
	/* v. vertex. xyz += v. normal * _FurLength *
	FUR_MULTIPLIER * v. color. a; */
}
struct Input {
	float2 uv_MainTex;
	float3 viewDir;
};
void surf (Input IN, inout SurfaceOutputStandard o) {
	fixed4 c = tex2D (_MainTex, IN. uv_MainTex) * _Color;
	o. Albedo = c. rgb;
	o. Metallic = _Metallic;
	o. Smoothness = _Glossiness;
	//o. Alpha = step(_Cutoff, c. a) ;
	o. Alpha = step(lerp(_Cutoff, _CutoffEnd, FUR_MULTIPLIER) , c. a) ;
	float alpha = 1 - (FUR_MULTIPLIER * FUR_MULTIPLIER) ;
	alpha += dot(IN. viewDir, o. Normal) - _EdgeFade;
	o. Alpha *= alpha;
}
  1. 回到你最初的着色器,在ENDCG部分之后添加这个额外的通道:
void surf (Input IN, inout SurfaceOutputStandard o) {
	// 反照率来自被颜色着色的纹理
	fixed4 c = tex2D (_MainTex, IN. uv_MainTex) * _Color;
	o. Albedo = c. rgb;
	// 金属质感和平滑度来自滑块变量
	o. Metallic = _Metallic;
	o. Smoothness = _Glossiness;
	o. Alpha = c. a;
}
ENDCG

CGPROGRAM
#pragma surface surf Standard fullforwardshadows
alpha: blend vertex: vert
#define FUR_MULTIPLIER 0. 05
#include "FurPass. cginc"
ENDCG
  1. 回到Unity,在反照率(RGB)属性中分配FauxFur纹理。你应该注意到沿着着色器的小点
    在这里插入图片描述
  2. 增加更多的通道,逐步增加FUR_MULTIPLIER。你可以从0通过20次得到不错的结果。05到0.95:
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
alpha: blend vertex: vert
#define FUR_MULTIPLIER 0. 05
#include "FurPass. cginc"
ENDCG
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
alpha: blend vertex: vert
#define FUR_MULTIPLIER 0. 1
#include "FurPass. cginc"
ENDCG
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
alpha: blend vertex: vert
#define FUR_MULTIPLIER 0. 15
#include "FurPass. cginc"
ENDCG
// . . . 0. 2 - 0. 85 here
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
alpha: blend vertex: vert
#define FUR_MULTIPLIER 0. 90
#include "FurPass. cginc"
ENDCG
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
alpha: blend vertex: vert
#define FUR_MULTIPLIER 0. 95
#include "FurPass. cginc"
ENDCG
}
Fallback "Diffuse"
}
  1. 一旦着色器被编译并附加到一个材料上,你就可以从检查器中改变它的外观。

    毛皮长度属性决定了毛皮壳之间的空间,这将改变毛皮的长度。较长的毛皮可能需要更多的 passes 才能看起来逼真。

    Alpha Cutoff 和 Alpha Cutoff end 是用来控制皮毛的密度和它如何逐渐变薄。

    边缘褪色决定了皮毛的最终透明度和它看起来有多模糊。较软的材料应该有较高的边缘褪色值

    最后,用重力方向和重力强度曲线来模拟重力的作用:

在这里插入图片描述

它是如何工作的……

  在这个配方中提出的技术被称为朗格尔的同心毛皮壳技术或简称壳技术。它的工作原理是创建需要渲染的几何图形的越来越大的副本。如果透明度合适,它会给人一种头发连续的错觉:
在这里插入图片描述
  外壳技术非常通用,而且相对容易实现。逼真的毛皮不仅需要挤压模型的几何形状,还需要改变它的顶点。这是可能的与镶嵌着色器,这是更先进的,在本书中没有涉及。

  这个毛皮着色器中的每个通道都包含在 FurPass.cginc 中。顶点函数创建一个稍微大一点的模型版本,这是基于正常挤压的原理。此外,还考虑了重力的影响,所以我们离中心越远,重力的影响越强烈:

void vert (inout appdata_full v)
{
	fixed3 direction = lerp(v.normal, _Gravity *
	_GravityStrength + v.normal * (1 - _GravityStrength) , FUR_MULTIPLIER) ;
	v.vertex.xyz += direction * _FurLength * FUR_MULTIPLIER * v.color. a;
}

  在本例中,alpha通道用于确定毛皮的最终长度。这允许更精确的控制。

  最后,表面函数从alpha通道读取控制掩码。它使用cutoff值来决定显示哪些像素,隐藏哪些像素。这个值从第一个到最后一个毛皮外壳变化,以匹配Alpha Cutoff和Alpha Cutoff end:

o. Alpha = step(lerp(_Cutoff, _CutoffEnd, FUR_MULTIPLIER) , c. a) ;
float alpha = 1 - (FUR_MULTIPLIER * FUR_MULTIPLIER) ;
alpha += dot(IN. viewDir, o. Normal) - _EdgeFade;
o. Alpha *= alpha;

  皮毛的最终alpha值也取决于它从相机的角度,让它看起来更柔软。

有更多的…

  毛皮着色器被用来模拟毛皮。然而,它可以用于各种其他材料。它非常适用于由多层自然构成的材料,如森林的树冠,模糊的云,人类的头发,甚至是草。

  在本书的示例代码中可以看到一些通过调整参数使用相同着色器的其他示例:

在这里插入图片描述
  还有许多其他的改进可以显著增加它的现实感。你可以根据当前时间通过改变重力方向来添加一个非常简单的风动画。如果校准正确,这可以给人一种皮毛因为风而移动的印象。

  此外,当角色移动时,你可以让你的皮毛移动。所有这些小的调整有助于你的皮毛的可信度,给人一种错觉,它不只是一个静态的材料画在表面。不幸的是,这个着色器是有代价的:20个通道是非常沉重的计算。通过的次数大致决定了材质的可信度。为了得到最适合你的效果,你应该调整毛发的长度和路径。考虑到这个着色器的性能影响,最好有几个不同次数的材质;你可以在不同的距离上使用它们,节省大量的计算。


使用数组实现热图

  着色器很难掌握的一个特点是缺乏适当的文档。大多数开发人员通过摆弄代码来学习如何使用着色器,而没有深入了解发生了什么。Cg/ HLSL做了很多假设,其中一些没有适当宣传,这一事实放大了这个问题。Unity3D允许c#脚本使用SetFloat、SetInt和SetVector等方法与着色器通信。不幸的是,Unity3D没有SetArray方法,这导致许多开发人员认为Cg/HLSL也不支持数组。这是不对的。这个菜谱将向你展示如何将数组传递给着色器。记住gpu是高度优化的并行计算,在着色器中使用for循环将大大降低其性能。

  对于这个食谱,我们将实现一个热图,如下面的截图所示:
在这里插入图片描述

准备

该配方中的效果是从一组点创建一个热图。这张热图可以叠加在另一张图片上,就像前面的截图一样。

以下步骤是必要的:

  1. 创建一个带有你想要用于热图的纹理的四边形(GameObject | 3D Object | quad)。在本例中,使用了伦敦地图。为了把纹理放在四边形上,使用Unlit/ texture shader创建一个新的材质(Map),并将图像分配给Base (RGB)属性。创建完成后,将该材料拖放到四边形上。四边形对象的位置必须设置为(0,0,0)。
  2. 创建另一个四边形,并将其放在前一个四边形的上面。我们的热图将出现在这个四边形上。
  3. 在第二个四边形上附加一个新的着色器(Heatmap)和材质(HeatmapMat):
    在这里插入图片描述
  4. 为了便于可视化,选择MainCamera并将对象的位置更改为(0,0,-10),在Camera组件下,将投影属性更改为Orthographic,将大小属性更改为0.5

  在Game视图中,你现在只会看到一个白色的盒子,因为第二个方块覆盖了地图; 但我们很快就会改变这一点。

怎么做……

  这个着色器与之前创建的着色器非常不同,但它相对较短。因此,整个代码按以下步骤提供:

  1. 复制以下代码到新创建的着色器:
    Shader "CookbookShaders/Chapter 12/Heatmap"
    {
    Properties
    {
    	_HeatTex("Texture", 2D) = "white" {}
    }
    Subshader
    {
    	Tags {"Queue" = "Transparent"}
    	Blend SrcAlpha OneMinusSrcAlpha // Alpha blend
    	Pass
    	{
    		CGPROGRAM
    		#pragma vertex vert
    		#pragma fragment frag
    		struct vertInput
    		{
    			float4 pos : POSITION;
    		};
    		struct vertOutput
    		{
    			float4 pos : POSITION;
    			fixed3 worldPos : TEXCOORD1;
    		};
    		vertOutput vert(vertInput input)
    		{
    			vertOutput o;
    			o. pos = UnityObj ectToClipPos( input. pos) ;
    			o. worldPos = mul(unity_Obj ectToWorld,
    			input. pos) . xyz;
    			return o;
    		}
    		uniform int _Points_Length = 0;
    		uniform float3 _Points[20] ;
    		// (x, y, z) = position
    		uniform float2 _Properties[20] ;
    		// x = radius, y = intensity
    		sampler2D _HeatTex;
    		half4 frag(vertOutput output) : COLOR
    		{
    			// Loops over all the points
    			half h = 0;
    			for (int i = 0; i < _Points_Length; i++)
    			{
    				// 计算每个点的贡献
    				half di = distance( output.worldPos, _Points[i].xyz) ;
    				half ri = _Properties[i].x;
    				half hi = 1 - saturate(di / ri) ;
    				h += hi * _Properties[i].y;
    			}
    			// Converts (0-1) according to the heat texture
    			h = saturate(h) ;
    			half4 color = tex2D(_HeatTex, fixed2(h, 0. 5) ) ;
    			return color;
    		}
    		ENDCG
    		}
    	}
    Fallback "Diffuse"
    }
    
  2. 一旦你把这个脚本附加到你的材料,你应该为热图提供一个斜坡纹理。重要的是要配置它,使Wrap Mode设置为Clamp:
    在这里插入图片描述

NOTE
如果你的热图要用作覆盖层,那么要确保坡道纹理有alpha通道,并且纹理导入alpha is Transparency选项。

  1. 之后,将Heatmap材质的Texture属性分配给heatramp纹理,你应该会注意到那个白色的方框现在消失了:
    在这里插入图片描述
    为了显示点,现在我们将创建一个新脚本来显示我们想要绘制的点。

  2. 使用以下代码创建一个名为HeatmapDrawer的新脚本:

using UnityEngine;
public class HeatmapDrawer : MonoBehaviour
{
	public Vector4[] positions;
	public float[] radiuses;
	public float[] intensities;
	public Material material;
	void Start()
	{
		material.SetInt("_Points_Length", positions. Length) ;
		material.SetVectorArray("_Points", positions) ;
		Vector4[] properties = new Vector4[positions. Length] ;
		for (int i = 0; i < positions. Length; i++)
		{
			properties[i] = new Vector2(radiuses[i] , intensities[i] ) ;
		}
		material.SetVectorArray("_Properties", properties) ;
	}
}
  1. 将脚本附加到场景中的一个对象上,最好是四边形。然后,将为此效果创建的材质拖放到脚本的材质槽中。通过这样做,脚本将能够访问Material并初始化它。
  2. 最后,展开脚本的“位置”、“半径”和“强度”字段,并用热图的值填充它们。位置表示热图的点(在世界坐标中):
    在这里插入图片描述
  3. 半径表示它们的大小,强度表示它们对周围区域的影响程度:
    在这里插入图片描述
  4. 如果一切顺利,当你玩游戏时,你应该会注意到类似以下截图的内容:

在这里插入图片描述
  如果您没有看到这一点,请确保热图被放置在地图四边形的前面,并且两个对象都在相机的前面。同时,确保你是在Play模式; 在编辑模式下,效果不会显示出来。

它是如何工作的……

  这个着色器依赖于这本书之前没有介绍过的东西; 第一个是数组。Cg允许用以下语法创建数组:

uniform float3 _Points [20] ;

  Cg不支持大小未知的数组: 您必须预先分配所需的所有空间。前面的代码行创建了一个包含20个元素的数组。

  Unity允许我们使用许多方法来设置数组,包括SetVectorArray, SetColorArray, SetFloatArray和GetMatrixArray。

NOTE
SetVectorArray函数目前只与Vector4类一起工作。这不会给我们带来任何问题,因为你可以自动将Vector3分配给Vector4, Unity会自动为最后一个元素包含一个零。另外,也可以在Update循环中使用我们的Start代码,以便在修改值时能够看到值的变化,但这在计算上是昂贵的。

在着色器的片段函数中,有一个类似的for循环,对于材质的每个像素,查询所有的点,寻找它们对热图的贡献:

	half h = 0;
	for (int i = 0; i < _Points_Length; i ++)
	{
		// Calculates the contribution of each point
		half di = distance(output. worldPos, _Points[i] . xyz) ;
		half ri = _Properties[i] . x;
		half hi = 1 - saturate(di / ri) ;
		h += hi * _Properties[i] . y;
	}

  变量h储存所有点的热量,给定它们的半径和强度。然后用它从 ramp 纹理中查找使用哪种颜色。

  着色器和数组是一个成功的组合,特别是很少有游戏充分利用它们的潜力。然而,它们引入了一个重要的瓶颈,因为对于每个像素,着色器必须循环通过所有的点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值