高级纹理以及复杂而真实的应用——ShaderCp10

2023-03-11,,

——20.9.7

这章主要分成三个部分 立方体纹理(cubemap) 渲染纹理(RenderTexture,rt) 和程序纹理

一、立方体纹理

立方体纹理顾名思义是一种三维的纹理形状类似于立方体,由六张图像组成。可以用于环境映射,可以模拟物体周围的环境,模拟金属感。

与之前的一维二维纹理采样方式不同,立方体纹理我们采用一个三维的纹理坐标进行取样。实际上这个三维的坐标就是一个方向矢量。从立方体的中心出发,向外部延伸时与六张纹理之一相交计算得来的。优点:快速方便,效果好。缺点:引入新的光源需要重新生成Cubemap,仅能反射环境不能反射该立方体纹理的物体本身。(不能进行模拟多次反射)简单的来说立方体纹理的建立就是从一个点向外绘制环境的纹理。所以一旦周围环境的光源发生改变就需要重新绘制。

天空盒所用的就是立方体纹理。利用立方体纹理映射技术。我们想让不同的Camera用上不同的天空盒可以给Camera添加Skybox组件来指定天空盒。

创建立方体纹理有三种方法 1.用一些特殊布局的纹理(一张图,具有立方体展开图的交叉布局,全景布局等) 2.手动创建Cubemap 3.脚本生成纹理

这里主要讲第三种。因为前两种靠资源。我们要用Camera.RenderToCubemap 从任意的位置观察到的场景存储到六张图像中。这里将使用UnityEditor.ScriptableWizard编辑器向导来创建Cubemap创建界面。OnWizardCreate() : 点击确定按钮调用此事件 OnWizardUpdate() : 当编辑器向导更新时调用

private void OnWizardCreate()
{
GameObject go = new GameObject("CubemapCamera");
go.AddComponent<Camera>();
go.transform.position = renderFromPosition.position;
go.GetComponent<Camera>().RenderToCubemap(cubemap);
DestroyImmediate(go);
}

  这里主要的方法是在渲染位置创建一个Cubemap的摄像机然后创建立方体纹理完后删除。在编辑器界面创建可以不用开始游戏运行。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor; public class RenderCubemapWizard : ScriptableWizard
{
public Transform renderFromPosition;
public Cubemap cubemap; private void OnWizardUpdate()
{
helpString = "Select transform to render from and cubemap to render into";
isValid = (renderFromPosition != null) && (cubemap != null);
} private void OnWizardCreate()
{
GameObject go = new GameObject("CubemapCamera");
go.AddComponent<Camera>();
go.transform.position = renderFromPosition.position;
go.GetComponent<Camera>().RenderToCubemap(cubemap);
DestroyImmediate(go);
} [MenuItem("GameObject/Render into Cubemap")]
static void RenderCubemap()
{
ScriptableWizard.DisplayWizard<RenderCubemapWizard>("Render cuebemap", "Render!");
}
}

  public static T DisplayWizard<T>(string title, string createButtonName)最后这个是创建的函数。就是对应标题以及按钮。下面是创建的界面。

  然后就是怎么对立方体纹理进行对应的应用。我们可以想一想有了周围的环境的纹理。可以干什么呢?最主要就是环境的反射、折射等。来提升场景中物体的真实性。然后我们就要来写ShaderLab。我仔细思考了一下我们应该从我们应该设置什么样的变量。以及存在的定理入手来推导出需要什么变量。然后怎么运用。直接讲里面的代码可能不是说特别好理解。下面因为使用的上面脚本生成的环境映射所以不是实时的。后面会介绍使用一些实时的方法。

我们先实现对环境的反射。 我们可以想一下就是一个金属的物体才会有对应环境的反射。首先我们有了Cubemap环境映射。我们就需要体现金属的金属质感是强还是弱。也就是抛光程度对应反射的强度。所以就有反射强度_ReflectAmount.然后就是原材质的颜色以及反射的颜色比如说环境因为材质反射的颜色不同。就有_Color以及_ReflectColor。然后就是主要代码。

o.worldRefl = reflect(-o.worldViewDir, o.worldNormal); //vert
fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb * _ReflectionColor.rgb; //frag
fixed3 color = ambient + lerp(diffuse, reflection, _ReflectionAmount) * atten; //frag

  第一个是reflect函数这是在Cp6里面用到的函数。当时所要解决的问题Phong光照模型需要反射角度。reflect(float3 i, float3 n) 对应的i是入射角度n是法线。返回反射方向。可以想象我们人眼是接受光线的地方,所以入射方向应该相反。也就是从环境的光发射到物体表面,根据物理我们知道入射光和反射光角度一致。我们其实是在计算这个光路的过程。与我们的人眼看的方向是逆方向。所以在前面加一个负号。两个方向都是向外的!!!第二个是Cubemap的采样结果。我们没有对其归一化是因为是方向没有必要。而我们把反射的计算放在顶点着色器的原因是虽然片元着色器可以让画面更细腻但人眼以及分辨不出。节约性能。最后一个就是我们使用lerp函数混合漫反射颜色以及反射颜色。并且乘以衰减值(点光源)。

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Unlit/10-1Reflection"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_ReflectionColor ("ReflectionColor", Color) = (1,1,1,1)
_ReflectionAmount ("_ReflectionAmount", Range(0,1)) = 1
_Cubemap ("Cubemap", Cube) = "_Skybox"{}
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
Pass
{
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc" struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
}; struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD0;
float3 worldViewDir : TEXCOORD2;
float3 worldRefl : TEXCOORD3;
SHADOW_COORDS(4)
}; fixed4 _Color;
fixed4 _ReflectionColor;
float _ReflectionAmount;
samplerCUBE _Cubemap; v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
o.worldRefl = reflect(-o.worldViewDir, o.worldNormal);
TRANSFER_SHADOW(o);
return o;
} fixed4 frag (v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(i.worldViewDir);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb * _ReflectionColor.rgb;
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 color = ambient + lerp(diffuse, reflection, _ReflectionAmount) * atten;
return fixed4(color, 1.0);
}
ENDCG
}
}FallBack "Reflective/VertexLit"
}

  然后添加上从茶壶渲染的Cubemap。这就是对应的效果。

然后我们来到了折射。有点像杯子(圆柱形)里面有水。在杯子后面有一支笔。那透过水看到的笔会是什么样的呢?可以先试一试可以发现是镜像的而且在中间发生比较大弯折。

我们这里要引出一个定理就是斯涅尔定理来计算折射角。

定律中的角度分别是入射角以及折射角。对应的n1、n2是对应介质的折射率。所以当我们有了公式那么我们需要什么呢。就是n1/n2即_RefractRatio来表示折射率之比。还有就是折射强度_RefractAmount。以及对应的颜色_Color以及_RefractColor。以及我们的环境映射_Cubemap。

o.worldRefr = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio); //vert
fixed3 refraction = texCUBE(_Cubemap, i.worldRefr).rgb * _RefractionColor.rgb; //frag
fixed3 color = ambient + lerp(diffuse, refraction, _RefractAmount) * atten; //frag

  其中重要的就是refract(float3 i, float3 n, float _RefractRatio) 对应的就是入射角、法线、以及入射介质折射率/折射介质折射率的比值。注意前面两个变量需要归一化。返回折射方向。第二个就是对应的texCube环境映射取样。

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Unlit/10-2Refraction"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_RefractionColor ("RefractionColor", Color) = (1,1,1,1)
_RefractAmount ("RefractAmount", Range(0,1)) = 1
_RefractRatio ("RefractRatio", Range(0.1,1)) = 0.5
_Cubemap ("Cubemap", Cube) = "_Skybox" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" } Pass
{
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
#include "UnityCG.cginc" struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
}; struct v2f
{
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldViewDir : TEXCOORD2;
float3 worldRefr : TEXCOORD3;
SHADOW_COORDS(4)
}; fixed4 _Color;
fixed4 _RefractionColor;
float _RefractAmount;
float _RefractRatio;
samplerCUBE _Cubemap; v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
o.worldRefr = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio);
TRANSFER_SHADOW(o);
return o;
} fixed4 frag (v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(i.worldViewDir); fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 refraction = texCUBE(_Cubemap, i.worldRefr).rgb * _RefractionColor.rgb;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
fixed3 color = ambient + lerp(diffuse, refraction, _RefractAmount) * atten;
return fixed4(color, 1.0);
}
ENDCG
}
}FallBack "Reflective/VertexLit"
}

  调整_RefractRatio到0.67就是水的折射率。效果如下。

接下来就是最后一个应用——菲涅耳反射(Fresnel)。非常常用的反射并且应用相当广泛。根据视角方向控制反射程度。实际上在描述一种光学现象就是,当光线照射到物体上面,一部分光线会发生反射,一部分进入物体内部发生折射或是散射。被反射的光与入射光之间存在一定的比率关系。这个比率关系可以通过菲涅耳进行计算。现实中的例子比如站在湖边(海边水可能不是很清澈)湖边的水是可以清澈见底的但是往湖中心看去却看不见底。于是我们就要引入公式,但因为菲涅耳等式是十分复杂的,于是我们就用近似等式来代替。Schlick菲涅耳近似等式。

FSchlick(v, n) = F0 + (1 - F0)(1 - dot(v, n))5

对应的F0是强度、v是视角方向、n是法线方向。还有一个是Empricial 菲涅耳近似等式。

FEmpricial(v, n) = max(0, min(1, bias + scale * (1- dot(v, n)power)))

然后我们主要用第一个公式。我们需要强度_FresnelScale。以及_Color,和环境映射_Cubemap。

fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldNormal, worldViewDir), 5); //frag
fixed3 color = ambient + lerp(diffuse, reflection, saturate(fresnel)) * atten; //frag

  这里就要用近似公式计算反射的光(采用环境映射_Cubemap采样结果的颜色)与入射光(漫反射光)的比例。然后用lerp来确定程度。

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Unlit/10-3Fresnel"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_FresnelScale ("FresnelScale", Range(0,1)) = 0.5
_Cubemap ("Cubemap", Cube) = "_Skybox" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
Pass
{
Tags { "LightMode"="ForwardBase" }
CGPROGRAM #pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
#include "UnityCG.cginc" struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
}; struct v2f
{
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldViewDir : TEXCOORD2;
float3 worldRefl : TEXCOORD3;
SHADOW_COORDS(4)
}; fixed4 _Color;
float _FresnelScale;
samplerCUBE _Cubemap; v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
o.worldRefl = reflect(-o.worldViewDir, o.worldNormal);
TRANSFER_SHADOW(o);
return o;
} fixed4 frag (v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(i.worldViewDir); fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb;
fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldNormal, worldViewDir), 5);
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 color = ambient + lerp(diffuse, reflection, saturate(fresnel)) * atten;
return fixed4(color, 1.0);
}
ENDCG
}
}FallBack "Reflective/VertexLit"
}

  当_FresnelScale等于1的时候。是一个全反射的物体。当_FresnelScale等于0的时候是一个具有边缘光照效果的漫反射物体。我们可以想象一下就是边缘与视角的点乘结果,因为两个方向近乎垂直。所以fresnel的值为1也就是边缘还是全反射状态(?单纯自己运算想象)。后面一些有趣的边缘操作就从这里开始。

二、渲染纹理

渲染目标纹理,把整个三维场景渲染到一个中间缓冲中(不是帧缓冲/后备缓冲)。与之相关的是多重渲染目标,场景同时渲染到多个渲染目标纹理中。延迟渲染就是其中一种。

通常使用渲染纹理的两个方法,1.Camera的Target Texture,rt将会实时的更新 2.GrabPass/OnRenderImage 获取当前屏幕图像,与当前屏幕分辨率相同。

概念有点多。有点没有办法理解。但下面这两个效果我之前都有涉及过。第一个就是镜子的效果。几个要点就是这里的镜子效果十分的苛刻。(?后面我自己来研究研究这个)首先就是因为是镜子也就是从里面相对的位置有一个摄像机。把摄像机的内容传到平面上。所以要新建一个摄像机在里面。然后给他添加rt。然后我之前是把摄像机放在镜子前面。然后就出现了下面这种无限套娃的情况。问题出现在把镜子也给渲染进去。有两个解决方法。1.改变新摄像头的ClippingPlanes让它不渲染屏幕 2.设置镜子的layer 并在新摄像机的CullingMask设置不渲染该层。但有没有发现就是下面这个还是错的。因为水壶在左边。但镜子的在右边。所以应该从镜子背后去放置新的摄像机/这样才能照到背后的图像。

Shader "Unlit/10-4Mirror"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" } Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog #include "UnityCG.cginc"
#include "Lighting.cginc" struct appdata
{
float4 vertex : POSITION;
float3 texcoord : TEXCOORD0;
}; struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
}; sampler2D _MainTex;
float4 _MainTex_ST; v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.uv.x = 1 - o.uv.x;
return o;
} fixed4 frag (v2f i) : SV_Target
{
return tex2D(_MainTex, i.uv);
}
ENDCG
}
}FallBack Off
}

  上面的shader主要就是水平翻转图像。

然后就是玻璃效果。采用了GrabPass。在URP里面不适用。在使用GrabPass的时候要小心渲染队列。GrabPass通常渲染透明物体。虽然没有混合操作。但也要把队列设成Transparent。RenderType设成Opaque是为了在使用着色器替换时能被正确渲染。

我们一样先从我们需要什么开始。首先是玻璃可能有对应的颜色。有色玻璃。那么可能需要图或者是色彩。我们这里用的是图_MainTex。然后最重要的就是玻璃表面的凹凸感。凹凸感在前面Cp7里面有说到就需要法线贴图_BumpMap。以及变形强度_Distortion。最后还有就是透过玻璃的折射以及反射的比例_RefractAmount。

GrabPass { "_RefractionTex" }
sampler2D _RefractionTex;
float4 _RefractionTex_TexelSize;
o.scrPos = ComputeGrabScreenPos(o.pos); //vert
//frag
float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
i.scrPos.xy = offset + i.scrPos.xy;
fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.w).rgb;
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
fixed3 reflDir = reflect(-worldViewDir, bump);
fixed3 finalColor = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount;

  然后上面第一行就是GrabPass的声明把屏幕截图放到_RefractionTex里面。然后一样需要为这个贴图声明变量。ComputeGrabScreenPos(留一个?我也不是很懂ComputeScreenPos以及它的区别)书上说是截取应被抓取的屏幕采样坐标。类似于确定屏幕位置?然后下面就是_Distortion变形强度越大背后物体变形程度越大。_RefractionTex_TexelSize.xy是用来对屏幕坐标进行偏移。然后我们要把切线空间下的切线通过转移矩阵转移到世界空间。用于对_Cubemap采样。最后就是反射以及折射的颜色比例。

效果也是相当真实了。然后就是要不要声明GrabPass,1.如果直接使用就会存在_GrabTexture。当场景中有多个物体使用GrabPass就会得到不同的图形。取决于他们的渲染队列 2.声明了就可以一起用。而且只用抓取一次

GrabPass以及Rt两者相比rt的效率高。而且GrabPass很多情况下不适用因为需要直接读取Cpu后备缓冲。就破坏了Cpu和Gpu的并行性。就是顺序是Cpu然后是Gpu。GrabPass是在Gpu的步骤。(?是这样吧)所以接下来就是CommandBuffers的使用。(这里留一个拓展研究?后面会另外写一个来研究这个。因为这个是在脚本里面直接获取的正确方式)

三、程序纹理

就是计算机生成图像。这里我没有什么理解就不写了。有兴趣的可以继续研究。但书里的实在是不感兴趣。

感谢你读到这里。又留下了很多坑。Cheers!

高级纹理以及复杂而真实的应用——ShaderCp10的相关教程结束。

《高级纹理以及复杂而真实的应用——ShaderCp10.doc》

下载本文的Word格式文档,以方便收藏与打印。