- 本篇博客主要为个人学习所编写读书笔记,不用于任何商业用途,以及不允许任何人以任何形式进行转载。
- 本篇博客会补充一些扩展内容(例如其他博客链接)。
- 本篇博客还会提供一些边读边做的效果截图。文章内所有数学公式都由Latex在线编辑器生成。
- 本篇博客主要提供一个“glance”,知识点的总结。如有需要请到书店购买正版。
- 博客提及所有官方文档基于2022.2版本,博客会更新一些书中的旧的知识点到2022.2版本。
- 如有不对之处欢迎指正。
- 我创建了一个游戏制作交流群:637959304 进群密码:(CSGO的拆包密码)欢迎各位大佬一起学习交流,不限于任何平台(U3D、UE、COCO2dx、GamesMaker等),以及欢迎编程,美术,音乐等游戏相关的任何人员一起进群学习交流。
屏幕后处理效果
- 屏幕后处理效果(screen post-processing effects):在渲染完整个屏幕图像后,再对这个图像进行一系列操作。使用该技术可以为游戏画面添加更多的艺术效果,例如景深(Depth of Field)、运动模糊(Motion Blur)等。
- OnRenderImage,抓取屏幕函数:MonoBehavior.OnRenderImage(RenderTexture src,RenderTexture dest) 。Unity会把当前渲染得到的图像存储在src中,然后通过函数的操作把目标渲染纹理dest显示到屏幕上。
- Graphics.Blit:利用该函数完成对渲染纹理的处理
//三种函数声明,dest=null时会直接将结果显示在屏幕上。mat是材质,pass是调用指定编号的pass,pass=-1就会一次调用所有Pass
Blit(Texture src,RenderTexture dest);
Blit(Texture src,RenderTexture dest,Material mat,int pass = -1);
Blit(Texture src,Material mat,int pass = -1);
调整亮度、饱和度、对比度
- 调整屏幕的亮度、饱和度、对比度:该部分需要编写一些C#代码来进行综合完成。
//C#脚本代码,该脚本直接挂载在摄像机下
using UnityEngine;
using System.Collections;
public class BrightnessSaturationAndContrast : PostEffectsBase {
public Shader briSatConShader;
private Material briSatConMaterial;
public Material material {
get {
briSatConMaterial = CheckShaderAndCreateMaterial(briSatConShader, briSatConMaterial);
return briSatConMaterial;
}
}
[Range(0.0f, 3.0f)]
public float brightness = 1.0f;
[Range(0.0f, 3.0f)]
public float saturation = 1.0f;
[Range(0.0f, 3.0f)]
public float contrast = 1.0f;
void OnRenderImage(RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_Brightness", brightness);
material.SetFloat("_Saturation", saturation);
material.SetFloat("_Contrast", contrast);
Graphics.Blit(src, dest, material);
}
else
{
Graphics.Blit(src, dest);
}
}
}
//该Shader直接传入public Shader briSatConShader;
Shader "Example/Brightness Saturation And Contrast" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_Brightness ("Brightness", Float) = 1
_Saturation("Saturation", Float) = 1
_Contrast("Contrast", Float) = 1
}
SubShader {
Pass {
//屏幕后处理实际上是在场景中绘制一个与屏幕同宽高的四边形面片,为了放置其对其他物体产生影响,需要关闭深度写入,放置挡住后面被渲染的物体。
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
half _Brightness;
half _Saturation;
half _Contrast;
struct v2f {
float4 pos : SV_POSITION;
half2 uv: TEXCOORD0;
};
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed4 renderTex = tex2D(_MainTex, i.uv);
fixed3 finalColor = renderTex.rgb * _Brightness;
//饱和度
fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b;
fixed3 luminanceColor = fixed3(luminance, luminance, luminance);
finalColor = lerp(luminanceColor, finalColor, _Saturation);
//对比度
fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
finalColor = lerp(avgColor, finalColor, _Contrast);
return fixed4(finalColor, renderTex.a);
}
ENDCG
}
}
Fallback Off
}
场景窗口
游戏窗口边缘检测
- 边缘检测:利用边缘检测算子对图像进行卷积(convolution)操作。(俗称:开卷)如果相邻像素之间存在差别明显的颜色、亮度、纹理等属性,那么就可以判定他们之间有一条边界。
- 什么是卷积:传送门,这个讲的很不错,还有很多例子和应用。
- 边缘检测算子:Sobel、Prewitt、Roberts。传送门
Shader "Example/Shader09"
{
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_EdgesOnly("Edges Only",Float) = 1.0
_EdgesColor("Edges Color",Color) = (0,0,0,1)
_BackgroundColor("Background Color",Color)=(1,1,1,1)
}
SubShader {
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
fixed4 _EdgesColor;
fixed4 _BackgroundColor;
float _EdgesOnly;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[9]: TEXCOORD0;
};
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
const half Gx[9] = {-1,0,1,-1,0,1,-1,0,1};
const half Gy[9] = {-1,-1,-1,0,0,0,1,1,1};
for(int j = 0 ; j < 9 ; j++)
o.uv[j] = uv + _MainTex_TexelSize.xy * half2(Gx[j],Gy[j]);
return o;
}
fixed luminance(fixed4 color) {
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
//核心卷积计算,注意不要把卷积算子抄错了,抄错一个效果就完全不一样
half Sobel(v2f i)
{
const half Gx[9] = {-1,-2,-1,0,0,0,1,2,1};
const half Gy[9] = {-1,0,1,-2,0,2,-1,0,1};
half edgeX = 0;
half edgeY = 0;
half texColor = 0;
for(int j = 0 ; j < 9 ; j++)
{
texColor = luminance(tex2D(_MainTex, i.uv[j]));
edgeX += texColor * Gx[j];
edgeY += texColor * Gy[j];
}
return 1 - abs(edgeX) - abs(edgeY);
}
fixed4 frag(v2f i) : SV_Target
{
half edge = Sobel(i);
fixed4 withEdgeColor = lerp(_EdgesColor,tex2D(_MainTex,i.uv[4]),edge);
fixed4 onlyEdgeColor = lerp(_EdgesColor,_BackgroundColor,edge);
return lerp(withEdgeColor,onlyEdgeColor,_EdgesOnly);
}
ENDCG
}
}
Fallback Off
}

高斯模糊
- 高斯模糊:利用高斯核进行卷积计算,每个元素的计算基于如下公式。σ是标准方差(一般取1),x和y分别对应了当前位置到卷积核中心的整数距离。为了确保滤波后的图像不会变暗,我们需要对高斯核中的权重进行归一化,让每个权重除以所有权重的和。

//在OnRenderImage需要进行两次Graphics.Blit,即需要用到两个Pass来实现高斯模糊
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
//进行比例缩放的采样,减少处理所需像素个数,提高性能能,同时可以得到更好的模糊效果
int rtW = src.width/downSample;
int rtH = src.height/downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(src, buffer0);
for (int i = 0; i < iterations; i++) {
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the vertical pass
Graphics.Blit(buffer0, buffer1, material, 0);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the horizontal pass
Graphics.Blit(buffer0, buffer1, material, 1);
//释放缓存
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
Graphics.Blit(buffer0, dest);
RenderTexture.ReleaseTemporary(buffer0);
}
else
{
Graphics.Blit(src, dest);
}
}
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 12/Gaussian Blur" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;
struct v2f {
float4 pos : SV_POSITION;
//第一个坐标存储当前采样纹理坐标,剩下四个坐标是高斯模糊中要用到的对领域采样的纹理坐标
half2 uv[5]: TEXCOORD0;
};
//片元着色器计算消耗性能会比顶点高,所以顶点着色器中计算,一次Pass进行竖直方向的计算,一次Pass进行水平方向的计算
v2f vertBlurVertical(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
//这里的*1.0,*2.0应该是进行纹理偏移
o.uv[0] = uv;
o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
return o;
}
v2f vertBlurHorizontal(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
return o;
}
//滤波函数
fixed4 fragBlur(v2f i) : SV_Target {
//5X5的高斯核化简成一个1x5和5x1的一维向量,v = {0.0545,0.2442,0.4026,0.2442,0.0545} h^T={0.0545,0.2442,0.4026,0.2442,0.0545}所以可以直接化简成如下的weight矩阵,只需要三个变量即可
float weight[3] = {0.4026, 0.2442, 0.0545};
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
for (int it = 1; it < 3; it++) {
//根据上述的对称性化简,这里进行两次迭代
sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it];
sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
}
return fixed4(sum, 1.0);
}
ENDCG
ZTest Always Cull Off ZWrite Off
Pass {
NAME "GAUSSIAN_BLUR_VERTICAL"
CGPROGRAM
//注意一下这里两次的名称不一样,代表分别进行的两次高斯模糊计算
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
Pass {
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
}
FallBack "Diffuse"
}

- Bloom扩散效果:可以将较亮的区域扩散到周围的去榆中,造成一种朦胧的效果。
- 原理:根据设定的阈值提取较亮的区域,然后存储到一个纹理渲染中,然后进行高斯模糊处理,模拟光线扩散效果,最后再和原图像进行混合
运动模糊
- 实现方法:1、利用一块积累缓存来混合多张连续的图像,然后取平均值输出。但消耗性能过大。
2、利用速度缓存存储各个像素当前的运动速度,然后利用这些值来决定模糊的方向和大小。
(在书中的下一章节会进行基于速度映射图的运动模糊,故这里就不再深究代码方面的内容)