【UnityShader入门精要学习笔记】第十章 高级纹理(2)渲染纹理

本文主要是介绍【UnityShader入门精要学习笔记】第十章 高级纹理(2)渲染纹理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述
本系列为作者学习UnityShader入门精要而作的笔记,内容将包括:

  • 书本中句子照抄 + 个人批注
  • 项目源码
  • 一堆新手会犯的错误
  • 潜在的太监断更,有始无终

我的GitHub仓库

总之适用于同样开始学习Shader的同学们进行有取舍的参考。


文章目录

  • 渲染纹理
    • 镜子效果
    • 玻璃效果
    • 渲染纹理 vs GrabPass
  • 程序纹理
    • 在Unity中实现简单的程序纹理
    • Unity的程序纹理


渲染纹理

摄像机的渲染结果会输出到颜色缓冲中,并显示到我们的屏幕上。现代的GPU允许我们把整个三维场景渲染到一个中间缓冲中,即渲染目标纹理(Render Target Texture ,RRT) , 而不是传统的帧缓冲或者后备缓冲(back buffer,就是我们之前讲到的进行画面切换时,颜色缓冲重新渲染时需要使用的缓冲区)。与之相关的是多重渲染目标(Multiple Render Target ,MRT) 。这种技术指的是GPU允许我们把场景同时渲染到多个渲染目标纹理中(例如4种渲染效果,正常情况下是渲染4次,但是使用MRT可以一次就渲染4种)。

Unity为RRT定义了一种专门的纹理类型——渲染纹理(Render Texture) 。这是一种为摄像机准备的纹理贴图,使用渲染纹理通常有两种方式:

  • 在Project目录下创建一个渲染纹理,然后把某个摄像机的渲染目标设置成渲染纹理,这样一来该摄像机的渲染结果就会实时更新到渲染纹理中而不会显示在屏幕上。使用这种方法,我们可以选择渲染纹理的分辨率,滤波模式等纹理属性。
  • 另一种方式是在屏幕后处理时使用GrabPass命令或者OnRenderImage函数来获取当前屏幕图像,Unity会把这个屏幕图像放到一张和屏幕分辨率等同的渲染纹理中,我们可以在自定义的Shader中将它们当作普通的纹理进行处理,从而实现各种屏幕特效(牛逼啊)

镜子效果

在这里插入图片描述

我们想要实现如图所示的一个镜子的效果,原理就是从镜子的方向布置一个摄像机,获取它的RenderTexture并进行处理。
在这里插入图片描述

shader代码:

Shader "Custom/MirrowMat_Copy"
{Properties{_MainTex("Main Tex",2D) = "white" {}}SubShader{Tags{"RenderType" = "Opaque" "Queue"="Geometry"}Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"#include "Lighting.cginc"#include "AutoLight.cginc"sampler2D _MainTex;struct v2f{float4 pos : SV_POSITION;float2 uv : TEXCOORD0;};v2f vert(appdata_base v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.uv = v.texcoord;// 镜子效果需要x轴翻转o.uv.x = 1 - o.uv.x;return o;}fixed4 frag(v2f i):SV_Target{return tex2D(_MainTex, i.uv);}ENDCG}}FallBack Off
}

简单获取表面纹理后映射回片元着色器即可。不过效果可以说是一坨()。由于分辨率太低导致图片像素过低,锯齿严重。而且和真正的镜子比起来效果不能说是一模一样,只能说天差地别。


玻璃效果

在Unity中,我们还可以通过Unity Shader中一种特殊的Pass来完成获取屏幕图像的目的,就是GrabPass。当我们在Shader中定义了一个GrabPass后,Unity会把当前屏幕的图像绘制在一张纹理中,以便我们在后续的Pass中访问它。我们通常会使用GrabPass来实现注入玻璃等透明材质的模拟。

与使用简单的透明度混合不同,使用GrabPass可以让我们对该物体后面的图像进行更复杂的处理,例如使用法线模拟折射想过,而不再是简单的和原屏幕颜色进行混合。

需要注意的是,使用GrabPass的时候,我们需要额外小心物体的渲染队列设置。因为GrabPass通常用于渲染透明物体,尽管代码里不包含混合指令,但我们往往仍然需要把物体的渲染队列设置成透明队列(Transparent)才能保证当渲染该物体时,所有的不透明物体已经被绘制在屏幕上(反正就是需要注意在渲染透明物体时的渲染队列顺序)

让我们用一个GrabPass来模拟一个玻璃效果。我们需要用到一张法线纹理来修改模型的法线信息,通过一个Cubemap来模拟玻璃的反射,而在模拟折射时,则使用GrabPass获取玻璃后面的屏幕图像,并模拟计算折射效果。

透明度应当是和反射率负相关的,因此我们需要以下变量:

  • 材质主纹理
  • 法线纹理,用于计算折射
  • 立方体纹理,用于计算环境反射效果

请看代码:

Shader "Custom/GlassRefraction_Copy"
{Properties{_MainTex("Main Tex",2D) = "white" {}_BumpMap("Normal Map",2D) = "bump" {}_Cubemap("Cube Map",Cube) = "_skybox" {}_Distortion("Distortion",Range(0,100)) = 10_RefractAmount("Refraction Amount",Range(0,1)) = 1}SubShader{//  "RenderType"="Opaque" 为了使用着色器替换(Shader Replacement)时,该物体被正确渲染Tags{"Queue" = "Transparent" "RenderType" ="Opaque"}// 抓取不透明物体渲染后的缓存,并保存到_RefractionTex纹理中GrabPass { "_RefractionTex" }Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "Lighting.cginc"#include "UnityCG.cginc"#include "AutoLight.cginc"sampler2D _MainTex;float4 _MainTex_ST;sampler2D _BumpMap;float4 _BumpMap_ST;samplerCUBE _Cubemap;float _Distortion;fixed _RefractAmount;sampler2D _RefractionTex;float4 _RefractionTex_TexelSize;struct a2v{float4 vertex:POSITION;float3 normal : NORMAL;float4 tangent : TANGENT;float4 texcoord : TEXCOORD0;};struct v2f{float4 pos : SV_POSITION;float4 scrPos : TEXCOORD0;float4 uv : TEXCOORD1;float4 TtoW0 : TEXCOORD2;  float4 TtoW1 : TEXCOORD3;  float4 TtoW2 : TEXCOORD4; };// 获取从切线空间转换到世界空间下的坐标,从而获得T2W转换矩阵v2f vert(a2v v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.scrPos = ComputeGrabScreenPos(o.pos);o.uv.xy = TRANSFORM_TEX(v.texcoord,_MainTex);o.uv.zw = TRANSFORM_TEX(v.texcoord,_BumpMap);float3 worldPos = o.pos;float3 worldNormal = UnityObjectToWorldNormal(v.normal);float3 worldTangent = mul(unity_ObjectToWorld,v.tangent);float3 worldBinormal = cross(worldNormal,worldTangent) * v.tangent.w;o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);  o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);  o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);  return o;}fixed4 frag(v2f i):SV_Target{float3 worldNormal = float3(i.TtoW0.y,i.TtoW1.y,i.TtoW2.y);float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));fixed3 bump = UnpackNormal(tex2D(_BumpMap,i.uv.zw));// 计算折射光的偏移 = 法线纹理 * 扭曲值 * 折射纹理(GrabPass的缓存颜色)float2 refractionOffset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;// 应用矩阵变换将法线变换到世界空间中bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));// 对Grab的RenderTex纹理的offset进行计算以应用折射光偏移// 此处与书中不同,书中为refractionOffset * i.scrPos.z  + i.scrPos.xyi.scrPos.xy = refractionOffset * max(0.5,(1-i.scrPos.z)) + i.scrPos.xy;// 我们不能直接对_RefractionTex进行采样,因为ComputeScreenPos得到的scrPos是使用o.pos计算得到的// 因此scrPos是齐次裁剪空间下的四维向量,若需要采样则应当先应用齐次除法转换到NDC空间下fixed3 refractionColor = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.w).rgb;fixed3 reflectDir = reflect(-worldViewDir,bump);fixed4 texColor = tex2D(_MainTex,i.uv.xy);fixed3 reflectColor = texCUBE(_Cubemap,reflectDir).rgb * texColor.rgb;fixed3 finalColor = reflectColor * (1-_RefractAmount) + refractionColor * _RefractAmount;return fixed4(finalColor, 1);}ENDCG}}Fallback "Diffuse"
}

设置"Queue" = "Transparent"是为了保证渲染时其他不透明物体已经被渲染,"RenderType" = "Opaque"是为了使用着色器替换时,该物体被正确渲染。(这通常发生在我们需要得到摄像机的深度和法线纹理时)

GrabPass定义了一个抓取图像的Pass。我们在其中声明字符串可以决定图像被存入哪个纹理中。

o.scrPos = ComputeGrabScreenPos(o.pos);语句计算了被抓取的屏幕图像的采样坐标,由于是使用clipPos进行计算的,因此计算结果也是处于四维的齐次裁剪空间下。

随后我们获取从切线空间到世界空间的变换矩阵,并进行一系列的向量计算

在使用scrPis前我们需要注意,我们先是对其应用了纹理偏移的计算以计算折射光在clip空间下的偏移。后续进行折射纹理采样时则是先将应用齐次除法转换到视口空间下再进行采样。

在这里插入图片描述

(我写的Shader使模型表面的凹凸感更充实了。出现的效果是:距离摄像机越远,玻璃的折射越不明显,因为根据公式,采样的scrPos在进行齐次除法后最终将落到屏幕空间,因此会随着摄像机的改变而改变视觉效果,而齐次空间中的z轴存储的正是深度值。因此我们可以根据深度对折射纹理进行采样)

GrabPass支持两种形式的定义:

  • 直接使用GrabPass{},然后再后续的Pass中直接使用_GrabTexture来访问屏幕图像。但是,当场景中有多个物体都使用了这样的形式来抓取屏幕时,这种方法的性能消耗比较大。因为Unity要为每个物体都抓取一次屏幕图像。但这种方法可以让每个物体得到不同的屏幕图像,这取决于它们的渲染队列以及渲染它们时当前的屏幕缓冲中的颜色。
  • 直接使用GrabPass{“TextureName”},使用该方法,Unity会在每一帧时为第一个使用名为“TextureName”的纹理的物体执行一次抓取屏幕的操作,而这个纹理同样可以被其他Pass访问。这样做的好处是:如果我们想要在多个纹理中处理抓取屏幕图像,而它们所需的屏幕图像是一样的时候,那么我们就可以使用这种方法,使得所有Pass访问的是同一张纹理图像。就不必每帧都抓取N张图像了。

渲染纹理 vs GrabPass

尽管渲染纹理和GrabPass都可以抓取屏幕图像,但实现方式略有不同。GrabPass只需在Pass中添加几行代码就可以实现屏幕纹理抓取,若是使用渲染纹理,我们还需要创建渲染纹理和一个额外的摄像机,再把摄像机的RendetTarget设置为新的渲染纹理对象,最后把渲染纹理传递给相应的Shader。

但是从效率上说,使用渲染纹理的效率往往高于GrabPass。因为我们可以自定义渲染纹理的大小,或是调整摄像机的渲染层来减少二次渲染的场景大小,或者使用其他方法来控制摄像机是否开启(灵活,纹理大小可控)

GrabPass获取的图像分辨率和显示屏幕是一致的,意味着在高分辨率的设备上会造成严重的带宽影响。且GrabPass往往需要CPU直接从back buffer中读取数据,破坏了CPU和GPU的并行性,这是比较耗时的,甚至在一些设备上不支持。

Unity5还提供了命令缓冲(Command Buffers) 来允许我们拓展Unity 的渲染流水线。此处就先略过了


程序纹理

程序纹理指的是那些由计算机生成的图形,通常用一些特定的算法来创建个性化的图案或非常真实的自然元素(例如木头,石头,没太理解)

使用程序纹理的好处是我们可以使用各种参数来控制纹理的外观,不仅仅是颜色等属性,甚至可以说是不同类型的图案属性,从而获得丰富的视觉效果。

在Unity中实现简单的程序纹理

using System.Collections;
using System.Collections.Generic;
using UnityEngine;[ExecuteInEditMode]
public class ProceduralTextureGenerationCopy : MonoBehaviour {public Material material = null;#region 材质属性// SetProperty可以使得面板上的修改也会被属性set到[SerializeField, SetProperty("textureWidth")]// 纹理大小通常是2的整幂次private int m_textureWidth = 512;public int textureWidth {get {return m_textureWidth;}set {m_textureWidth = value;_UpdateMaterial();}}[SerializeField, SetProperty("backgroundColor")]private Color m_backgroundColor = Color.white;public Color backgroundColor {get {return m_backgroundColor;}set {m_backgroundColor = value;_UpdateMaterial();}}[SerializeField, SetProperty("circleColor")]private Color m_circleColor = Color.yellow;public Color circleColor {get {return m_circleColor;}set {m_circleColor = value;_UpdateMaterial();}}[SerializeField, SetProperty("blurFactor")]private float m_blurFactor = 2.0f;public float blurFactor {get {return m_blurFactor;}set {m_blurFactor = value;_UpdateMaterial();}}#endregionprivate Texture2D m_generatedTexture = null;// Use this for initializationvoid Start () {if (material == null) {Renderer renderer = gameObject.GetComponent<Renderer>();if (renderer == null) {Debug.LogWarning("Cannot find a renderer.");return;}material = renderer.sharedMaterial;}_UpdateMaterial();}// 该函数用于根据Set生成新纹理并应用到Shader的MainTex属性上private void _UpdateMaterial() {if (material != null) {m_generatedTexture = _GenerateProceduralTexture();material.SetTexture("_MainTex", m_generatedTexture);}}// 混合颜色函数private Color _MixColor(Color color0, Color color1, float mixFactor) {Color mixColor = Color.white;mixColor.r = Mathf.Lerp(color0.r, color1.r, mixFactor);mixColor.g = Mathf.Lerp(color0.g, color1.g, mixFactor);mixColor.b = Mathf.Lerp(color0.b, color1.b, mixFactor);mixColor.a = Mathf.Lerp(color0.a, color1.a, mixFactor);return mixColor;}private Texture2D _GenerateProceduralTexture() {Texture2D proceduralTexture = new Texture2D(textureWidth, textureWidth);// 定义圆点间的间距float circleInterval = textureWidth / 4.0f;// 定义圆的半径float radius = textureWidth / 10.0f;// 定义模糊系数float edgeBlur = 1.0f / blurFactor;// 逐像素绘制for (int w = 0; w < textureWidth; w++) {for (int h = 0; h < textureWidth; h++) {// 初始化背景颜色像素Color pixel = backgroundColor;// 依次画圆for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {// 计算圆心坐标Vector2 circleCenter = new Vector2(circleInterval * (i + 1), circleInterval * (j + 1));// 计算当前像素与圆的距离 = 圆心与像素的欧氏距离 - 半径float dist = Vector2.Distance(new Vector2(w, h), circleCenter) - radius;// 进行边缘模糊Color color = _MixColor(circleColor, new Color(pixel.r, pixel.g, pixel.b, 0.0f), Mathf.SmoothStep(0f, 1.0f, dist * edgeBlur));// 与边缘颜色再进行颜色混合pixel = _MixColor(pixel, color, color.a);}}proceduralTexture.SetPixel(w, h, pixel);}}proceduralTexture.Apply();return proceduralTexture;}
}

在这里插入图片描述

原理就是我们用脚本代码逐像素处理,用数学知识来手动生成了。

Unity的程序纹理

在Unity中,有一类专门使用程序纹理的材质,叫做程序材质(Procedural Materials) ,程序材质和我们之前使用的材质本质上是一样的,不同的是,它使用的纹理是程序纹理,而这些程序纹理是使用Substance Designer 生成的。

这些程序纹理的后缀为**.sbsar** 。导入到Unity之后会生成一个程序纹理资源(Procedural Material Asset)。每个程序纹理使用了不同的纹理参数,因此也会生成不同的纹理。

不过由于Unity5.6之后内置不支持生成sbsar的纹理资源,只能使用substance提供的的插件。而旧版的插件已经不支持下载,可以在2020之后的Unity版本中安装Substance 3D for Unity插件来生成纹理。

这篇关于【UnityShader入门精要学习笔记】第十章 高级纹理(2)渲染纹理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/917640

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

【机器学习】高斯过程的基本概念和应用领域以及在python中的实例

引言 高斯过程(Gaussian Process,简称GP)是一种概率模型,用于描述一组随机变量的联合概率分布,其中任何一个有限维度的子集都具有高斯分布 文章目录 引言一、高斯过程1.1 基本定义1.1.1 随机过程1.1.2 高斯分布 1.2 高斯过程的特性1.2.1 联合高斯性1.2.2 均值函数1.2.3 协方差函数(或核函数) 1.3 核函数1.4 高斯过程回归(Gauss

数论入门整理(updating)

一、gcd lcm 基础中的基础,一般用来处理计算第一步什么的,分数化简之类。 LL gcd(LL a, LL b) { return b ? gcd(b, a % b) : a; } <pre name="code" class="cpp">LL lcm(LL a, LL b){LL c = gcd(a, b);return a / c * b;} 例题:

【学习笔记】 陈强-机器学习-Python-Ch15 人工神经网络(1)sklearn

系列文章目录 监督学习:参数方法 【学习笔记】 陈强-机器学习-Python-Ch4 线性回归 【学习笔记】 陈强-机器学习-Python-Ch5 逻辑回归 【课后题练习】 陈强-机器学习-Python-Ch5 逻辑回归(SAheart.csv) 【学习笔记】 陈强-机器学习-Python-Ch6 多项逻辑回归 【学习笔记 及 课后题练习】 陈强-机器学习-Python-Ch7 判别分析 【学

Java 创建图形用户界面(GUI)入门指南(Swing库 JFrame 类)概述

概述 基本概念 Java Swing 的架构 Java Swing 是一个为 Java 设计的 GUI 工具包,是 JAVA 基础类的一部分,基于 Java AWT 构建,提供了一系列轻量级、可定制的图形用户界面(GUI)组件。 与 AWT 相比,Swing 提供了许多比 AWT 更好的屏幕显示元素,更加灵活和可定制,具有更好的跨平台性能。 组件和容器 Java Swing 提供了许多