[Shader] Shader Cookbook 使用Unity渲染纹理的屏幕效果[8]

56 篇文章 2 订阅

  学习编写着色器最令人印象深刻的方面之一是创建自己的屏幕效果的过程,也称为后期效果。通过这些屏幕效果,我们可以创建具有Bloom、动态模糊、高动态范围(HDR)效果等令人惊叹的实时图像。今天市场上的大多数现代游戏都大量使用了这些屏幕效果,如景深效果,Bloom效果,甚至颜色校正效果。

  在第一章,后处理堆栈中,我们讨论了如何使用Unity内置的后处理堆栈,但在本章中,你将学习如何自己构建脚本系统。该系统将给你控制创建多种屏幕效果。我们将介绍RenderTexture,深度缓冲是什么,以及如何创建效果,让你的游戏的最终渲染图像的photoshop般的控制。通过为你的游戏使用屏幕效果,你不仅可以完善你的着色器写作知识,而且你还可以用Unity从零开始创建自己的难以置信的实时渲染。

在本章中,你将学习以下食谱:

  • 设置一个屏幕效果脚本系统

设置一个屏幕效果脚本系统

  在创建屏幕效果的过程中,我们抓取一个全屏图像(或纹理),使用着色器在图形处理单元(GPU)上处理它的像素,然后将其发送回Unity的渲染器,将其应用到游戏的整个渲染图像。这让我们能够实时地对游戏渲染图像执行逐像素操作,为我们提供更全面的美术控制。

  想象一下,如果你必须调整游戏中每个物体上的每种材质,只是为了调整游戏最终外观的对比度。虽然不是不可能,但这需要一些劳动来完成。通过使用屏幕效果,我们可以调整整个屏幕的最终外观,从而让我们更像photoshop一样控制游戏的最终外观。

  为了让屏幕效果系统运行起来,我们必须设置一个脚本作为游戏当前渲染图像的信使,或者Unity称为RenderTexture的东西。通过使用这个脚本将RenderTexture传递给着色器,我们可以创建一个灵活的系统来建立屏幕效果。对于我们的第一个屏幕效果,我们将创建一个非常简单的灰度效果,让我们的游戏看起来黑白分明。让我们来看看这是如何实现的。

准备

  为了让我们的屏幕效果系统运行起来,我们需要为我们当前的Unity项目创建一些资产。通过这样做,我们将为接下来的步骤做好准备。

  1. 在当前项目中,创建一个工作的新场景。
  2. 在场景中创建一个简单的球体,并给它分配一个新材质(我称之为RedMat)。这个新材质可以是任何东西,但是对于我们的例子,我们将使用标准着色器制作一个简单的红色材质。
  3. 最后,创建一个新的方向光,如果一个还没有,保存场景。
  4. 我们需要创建一个新的c#脚本并将其命名为TestRenderImage.cs。出于组织目的,从Project选项卡中创建一个名为Scripts的文件夹,将脚本放入其中。

当我们所有的资产都准备好了,你应该有一个简单的场景设置,看起来像这样:
在这里插入图片描述

怎么做……

  为了使我们的灰度屏幕效果工作,我们需要一个脚本和一个着色器。所以,我们将在这里完成这两个新项目,并将它们与适当的代码,以产生我们的第一个屏幕效果。我们的第一个任务是完成c#脚本。这会让整个系统运转起来。在这之后,我们将完成着色器,并看到我们的屏幕效果的结果。让我们按照以下步骤完成我们的脚本和着色器:

  1. 打开TestRenderImage.cs c#脚本,并开始输入一些用于存储重要对象和数据的变量。在TestRenderImage类的最顶部输入以下代码:
#region Variables
public Shader curShader;
public float greyscaleAmount = 1.0f;
private Material screenMat;
#endregion
  1. 为了让我们实时编辑屏幕效果,当Unity Editor没有播放时,我们需要在TestRenderImage类的声明上方输入以下代码行:
using UnityEngine;
[ExecuteInEditMode]
public class TestRenderImage : MonoBehaviour {
  1. 由于我们的屏幕效果是使用着色器在屏幕图像上执行像素操作,我们必须创建一个材质来运行着色器。没有这个,我们就不能访问着色器的属性。为此,我们将创建一个c#属性来检查材质,如果找不到,就创建一个。在第1步的变量声明之后输入以下代码:

    #region Properties
    Material ScreenMat
    {
    	get
    	{
    		if (screenMat == null)
    		{
    			screenMat = new Material(curShader) ;
    			screenMat.hideFlags = HideFlags.HideAndDontSave;
    		}
    		return screenMat;
    	}
    }
    #endregion
    
  2. 现在我们想在脚本中设置一些检查,这样如果在脚本开始之前没有设置属性,脚本就会禁用自己:

    void Start()
    {
    	if (!curShader && ! curShader.isSupported)
    	{
    		enabled = false;
    	}
    }
    
  3. 为了从Unity渲染器中获取渲染的图像,我们需要使用Unity提供的以下内置函数OnRenderImage()。输入以下代码,以便我们能够访问当前的RenderTexture:

    void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)
    {
    	if (curShader ! = null)
    	{
    		ScreenMat.SetFloat("_Luminosity", greyscaleAmount) ;
    		Graphics.Blit(sourceTexture, destTexture, ScreenMat) ;
    	}
    	else
    	{
    		Graphics.Blit(sourceTexture, destTexture) ;
    	}
    }
    
  4. 我们的屏幕效果有一个名为grayScaleAmount的变量,我们可以用它来控制我们想要的最终屏幕效果的灰度值。所以,在这种情况下,我们需要让值从0到1,其中0是没有灰度效果,1是完全灰度效果。我们将在Update()函数中执行这个操作,它将在游戏运行时的每一帧被调用:

    void Update ()
    {
    	greyscaleAmount = Mathf. Clamp(greyscaleAmount,
    	0. 0f, 1. 0f) ;
    }
    
  5. 最后,我们通过对脚本开始时创建的对象做一点清理来完成脚本:

    	void OnDisable()
    	{
    		if(screenMat)
    		{
    			DestroyImmediate(screenMat) ;
    		}
    	}
    
  6. 现在,我们可以将这个脚本应用到Unity中的相机(如果它编译无误)。让我们应用TestRenderImage.Cs脚本到我们场景中的主相机。您应该看到grayScaleAmount值和着色器的字段,但是脚本向控制台窗口抛出一个错误。它说它缺少一个对象的实例,因此不能适当地处理。如果你回想第4步,我们正在做一些检查,看看我们是否有着色器,以及当前平台是否支持着色器。因为我们没有给屏幕效果脚本一个着色器来工作,那么curShader变量就是null,这会抛出一个错误。让我们通过完成着色器继续我们的屏幕效果系统。

  7. 创建一个新的着色器ScreenGrayscale。为了开始我们的着色器,我们将用一些变量填充我们的Properties块,这样我们就可以向这个着色器发送数据:

Properties
{
	_MainTex ("Base (RGB) ", 2D) = "white" {}
	_Luminosity("Luminosity", Range(0. 0, 1) ) = 1. 0
}
  1. 我们的着色器现在将使用纯C for Graphics (Cg)着色器代码,而不是使用Unity内置的Surface shader代码。这将使我们的屏幕效果更加优化,因为我们只需要处理RenderTexture的像素。因此,我们将删除SubShader中之前的所有内容,在我们的着色器中创建一个新的Pass块,并使用一些我们以前没有见过的新的#pragma语句填充它:

    SubShader
    {
    	Pass
    	{
    		CGPROGRAM
    		#pragma vertex vert_img
    		#pragma fragment frag
    		#pragma fragmentoption ARB_precision_hint_fastest
    		#include "UnityCG. cginc"
    
  2. 为了访问从Unity编辑器发送到着色器的数据,我们需要在CGPROGRAM块中创建相应的变量:

```cpp
uniform sampler2D _MainTex;
fixed _Luminosity;
```
  1. 最后,我们需要做的就是设置我们的像素函数——在本例中,称为frag()。这就是屏幕效果的关键所在。这个函数将处理RenderTexture的每个像素,并返回一个新图像到我们的TestRenderImage.cs脚本:
    fixed4 frag(v2f_img i) : COLOR
    {
    	// 从RenderTexture中获取颜色,从v2f_img结构中获取uv
    	fixed4 renderTex = tex2D(_MainTex, i. uv) ;
    	// 应用光度值到我们的渲染纹理
    	float luminosity = 0.299 * renderTex. r + 0.587 * renderTex. g + 0.114 * renderTex. b;
    	fixed4 finalColor = lerp(renderTex, luminosity, _Luminosity) ;
    	renderTex.rgb = finalColor;
    	return renderTex;
    }
    
  2. 在Pass块的末尾,添加一个ENDCG语句。
  3. 最后,将FallBack行更改为以下内容:
    FallBack Off
    
  4. 最终的着色器应该是这样的:
Shader "CookbookShaders/Chapter 10/ScreenGrayscale"
{
	Properties
	{
		_MainTex("Base (RGB) ", 2D) = "white" {}
		_Luminosity("Luminosity", Range(0. 0, 1) ) = 1. 0
	}
	SubShader
	{
		Pass
		{
			CGPROGRAM
			#pragma vertex vert_img
			#pragma fragment frag
			#pragma fragmentoption ARB_precision_hint_fastest
			#include "UnityCG. cginc"
			uniform sampler2D _MainTex;
			fixed _Luminosity;
			
			fixed4 frag(v2f_img i) : COLOR
			{
				// 从RenderTexture中获取颜色,从v2f_img结构中获取uv
				fixed4 renderTex = tex2D(_MainTex, i. uv) ;
				// 应用光度值到我们的渲染纹理
				float luminosity = 0. 299 * renderTex. r + 0. 587 * renderTex. g + 0. 114 * renderTex. b;
				fixed4 finalColor = lerp(renderTex, luminosity, _Luminosity) ; 
				renderTex. rgb = finalColor;
				return renderTex;
			}
			ENDCG
		}
	}
	FallBack Off
}

  一旦着色器完成,返回到Unity,让它编译,看看是否发生了任何错误。如果不是,将新的着色器分配给TestRenderImage.cs脚本,并更改灰度量变量的值。你应该会看到游戏视图从彩色版本变成了灰色版本:

在这里插入图片描述
以下截图展示了这种屏幕效果:

在这里插入图片描述
  有了这个完成,我们现在有一个简单的方法来测试新的屏幕效果着色器,而不必一遍又一遍地编写我们的整个屏幕效果系统。让我们再深入一点,了解一下RenderTexture发生了什么,以及它在整个存在过程中是如何处理的。

它是如何工作的……

  为了让屏幕效果在Unity中运行,我们需要创建一个脚本和着色器。该脚本驱动编辑器中的实时更新,还负责从主相机捕获RenderTexture并将其传递给着色器。一旦RenderTexture到达着色器,我们就可以使用着色器执行逐像素操作。

  在脚本的开始,我们执行一些检查,以确保当前选择的构建平台实际上支持屏幕效果和着色器本身。在某些情况下,当前平台不支持屏幕效果或我们正在使用的着色器。因此,我们在Start()函数中所做的检查确保如果平台不支持屏幕系统,我们不会得到任何错误。

  一旦脚本通过了这些检查,我们通过调用内置的OnRenderImage()函数启动屏幕效果系统。该函数负责抓取RenderTexture,并使用图形将其提供给着色器。函数,并将处理过的图像返回到Unity渲染器。有关这两个功能的更多资料,请浏览以下连结:

  • OnRenderImage : http: //docs. unity3d. com/Documentation/
    ScriptReference/MonoBehaviour. OnRenderImage. html
  • Graphics. Blit : http: //docs. unity3d. com/Documentation/
    ScriptReference/Graphics. Blit. html

  一旦当前的RenderTexture到达着色器,着色器接受它,通过frag()函数处理它,并返回每个像素的最终颜色。

  你可以看到这变得多么强大,因为它让我们能够像ps一样控制游戏的最终渲染图像。这些屏幕效果的工作顺序就像Photoshop图层,在相机看到的上面。当您将这些屏幕效果一个接一个地放置时,它们将按此顺序处理。这些只是让屏幕效果工作的基本步骤,但却是屏幕效果系统如何工作的核心。

还有更多的…

  现在我们已经启动并运行了简单的屏幕效果系统,让我们看看我们可以从Unity的渲染器中获得的其他有用信息:
在这里插入图片描述
  我们可以通过打开Unity的内置深度模式来获得当前游戏中所有内容的深度。一旦这个被打开,我们就可以使用深度信息进行大量不同的效果。让我们来看看这是如何实现的。

  1. 复制我们创建两次的球体,并在下面创建一个平面:
    在这里插入图片描述
  2. 通过复制ScreenGrayscale代码并按Ctrl + d来创建一个新的着色器,一旦复制完成,将脚本重命名为SceneDepth,然后双击该着色器在脚本编辑器中打开它。
  3. 我们将创建一个主纹理(_MainTex)属性和一个用于控制景深效果的属性。在你的着色器中输入以下代码:
    Properties
    {
    	_MainTex ("Base (RGB) ", 2D) = "white" {}
    	_DepthPower("Depth Power", Range(0, 1) ) = 1
    }
    
  4. 现在,我们需要在CGPROGRAM块中创建相应的变量。我们将添加一个名为_CameraDepthTexture的变量。这是Unity通过使用UnityCG提供给我们的内置变量。cginclude通过。它给了我们来自相机的深度信息:
    Pass
    {
    	CGPROGRAM
    	#pragma vertex vert_img
    	#pragma fragment frag
    	#pragma fragmentoption ARB_precision_hint_fastest
    	#include "UnityCG. cginc"
    	uniform sampler2D _MainTex;
    	fixed _DepthPower;
    	sampler2D _CameraDepthTexture;
    
  5. 我们将通过使用Unity提供给我们的几个内置函数,UNITY_SAMPLE_DEPTH()和linear01Depth()函数来完成我们的深度着色器。第一个函数实际上从_CameraDepthTexture变量中获取深度信息,并为每个像素生成单个float值。然后Linear01Depth()函数通过将这个最终深度值取到我们可以控制的幂值来确保值在0 - 1范围内,其中0 - 1范围的中间值位于基于摄像机位置的场景中:
fixed4 frag(v2f_img i) : COLOR
{
	// 从RenderTexture中获取颜色,从v2f_img结构中获取uv
	float depth = UNITY_SAMPLE_DEPTH(tex2D( _CameraDepthTexture, i.uv.xy)) ;
	depth = pow(Linear01Depth(depth) , _DepthPower) ;
	return depth;
}
  1. 着色器完成后,让我们把注意力转向Unity编辑器,并创建一个新的脚本。选择我们的TestRenderImage脚本并复制它。将此新脚本命名为RenderDepth并在脚本编辑器中打开它。
  2. 更新脚本,使其具有与我们在上一步(RenderDepth)中重命名的相同的类名:
    using UnityEngine;
    [ExecuteInEditMode]
    public class RenderDepth : MonoBehaviour {
    
  3. 我们需要在脚本中添加一个depthPower变量,这样我们就可以让用户在编辑器中更改它的值:
    #region Variables
    public Shader curShader;
    public float depthPower = 0. 2f;
    private Material screenMat;
    #endregion
    
  4. 我们的OnRenderImage()函数需要更新,以便它传递正确的值给我们的着色器:
    void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)
    {
    	if (curShader ! = null)
    	{
    		ScreenMat. SetFloat("_DepthPower", depthPower) ;
    		Graphics. Blit(sourceTexture, destTexture, ScreenMat) ;
    	}
    	else
    	{
    		Graphics. Blit(sourceTexture, destTexture) ;
    	}
    }
    
  5. 为了完成我们的深度屏幕效果,我们需要告诉Unity打开当前相机的深度渲染。这可以通过简单地设置主相机的depthTextureMode来实现:
    void Update ()
    {
    	Camera.main.depthTextureMode = DepthTextureMode.Depth;
    	depthPower = Mathf.Clamp(depthPower, 0, 1) ;
    }
    
    设置好所有代码后,保存脚本和着色器并返回Unity,让它们都进行编译。然后,选择主相机,右键单击TextRenderImage组件,然后选择Remove component。然后,将这个新组件附加到对象上,并在里面拖放我们的新着色器。如果没有遇到错误,你应该会看到类似这样的结果:

在这里插入图片描述
下面是一个例子,如果我们进一步调整值,我们可以得到什么:

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值