前言
我们在第二篇文章用网格的uv坐标来设置四边形的颜色,那么屏幕的uv坐标是什么呢?
正文
你可以将屏幕看成一个大的正方形,别嘘我,底层的图形API也是这么看待屏幕的。
在第一章中我们提到了shader的计算流程,并且介绍到fragment shader会将计算好的颜色传给颜色缓冲,并结束工作,因为之后的内容就是交由底层图形API来将颜色缓冲内的数据内容以法向量的形式渲染到屏幕上,而这个过程将根据运行平台的不同而选择不同的图形API,所以我没有详细说,想了解只要随便找个图形API比如openGL什么的学一下就好了。
谢谢这位临时演员帮助了我们理解屏幕渲染效果是怎么一回事。图形渲染的底层实现是由unity来决定的,当unity指定了执行平台后,图形API就可以开始喷射图像了,存储在水枪里面的都是以法向量存在的渲染数据,包含颜色、深度信息等,只要图像API做一些中间处理,就可以将其射到屏幕上。
但是,在颜色缓冲区的内容真正被喷到屏幕上之前,我们仍然有机会更改其中的内容。而unity也为我们考虑到了这一点,因此它提供了一个叫OnRenderImage的函数,这是个monobehaviour事件。关于这个函数可移步Unity Doc:
https://docs.unity3d.com/Manual/ExecutionOrder.htmldocs.unity3d.com Post-processing overviewdocs.unity3d.com这个函数有一个source渲染纹理作为输入,这个输入包含了颜色缓冲里面所有的数据资讯,包括颜色、深度信息等等。以及一个destination作为输出,在这我们通常认为就是相机屏幕了。
场景准备(随便搭一搭就好了):
接下来写一个简单的C#程序来了解它的用法:
using UnityEngine;
[ExecuteInEditMode]
public class PostEffects : MonoBehaviour
{
public Material material;
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
Graphics.Blit(source, destination, material);
}
}
首先,ExecuteInEditMode表示我们可以在Edit mode中观察程序的执行。然后定义了一个材质,用来渲染屏幕,然后调用OnRenderImage这个函数,而重点则在于Graphics.Blit这个方法,它的意思大概是用这个material把这个source渲染到那个destination。
然后我会用第二节讲到的shader:
Shader "Custom/ShaderLearning"
{
SubShader
{
Tags
{
"Queue" = "Transparent"
}
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv: TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv: TEXCOORD1;
};
v2f vert(appdata v)
{
v2f o;
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.uv;
return o;
}
float4 frag(v2f i) : SV_Target
{
float4 color = float4(i.uv.r,i.uv.g, 0, 1);
return color;
}
ENDCG
}
}
}
接下来把PostEffects拖给场景内的相机。并新建一个新的material,将上面的shader赋给新材质后,将材质赋值给PostEffects的材质参数
最终的效果如上图所示。我们把整个屏幕的uv坐标作为颜色参数输出了,但这确实不是什么好玩的东西,我们可以在shader中新建一个主纹理来接收在颜色缓冲中存储的纹理(这里就是屏幕),并将其输出到屏幕上吧。
值得注意的是,Graphics.Blit方法会把source传入的纹理赋值给mateiral中的主纹理。必须将shader中的主纹理命名为_MainTex才行,不然会报错。
加入一个Properties区域,并且声明一个_MainTex的二维属性,以白色为预设颜色:
Properties{
_MainTex("Main Texture", 2D) = "white" {}
}
在CGPROGRAM内加入声明:
sampler2D _MainTex;
修改fragment shader,这次我们不仅输出纹理本身,而且还要将它乘上uv坐标:
float4 frag(v2f i) : SV_Target
{
float4 color = tex2D(_MainTex, i.uv);
color *= float4(i.uv.r, i.uv.g, 0, 1);
return color;
}
那么总算是做出一些比较有趣的渲染效果了,当然这可能也就是在你做些什么吓唬人的游戏时才有些用处了。
自问自答
当我把纹理作为输入传给OnRenderImage时,我其实不过是在将整个要渲染到整个屏幕上的东西拿出来做处理而已,这些"要渲染的东西"就是颜色咯,我可不可以把纹理看作存有很多个颜色值的数据表呢?
“一张纹理就是存储了一个数组,而里面的数据就是颜色”这样的想法是不完全对的。我们可以把任何数据内容存储进纹理中,有些纹理在我们眼中是不符合现实的
但对于我们的shader来说,他有可能是一张查找表,有可能是一张高度图,或者就是一些伪随机的噪音参数,但无论它是什么,对于shader都是有用的内容。
举个例子,看到这张图你能想到什么?要说“啊,是Minecraft里面的砖块材质!”的同学清先坐下。我想说的是,红色的颜色代表的是在uv的x值上的位移,而绿色则是在uv的y值上的位移。最终的屏幕后处理效果能呈现一种空间被扭曲的感觉,我们下章来做它的实现。
下一章:
俊銘:Shader从入门到跑路(5):实作屏幕扭曲效果zhuanlan.zhihu.com