高级纹理
立方体纹理
立方体纹理(CubeMap)是 环境映射(Enviroment Mapping) 的一种实现方式。立方体纹理包含6张图像,每个面表示沿着世界空间下的轴向(上、下、左、右、前、后) 观察所得的图像。对立方体纹理的采样需要提供三维纹理坐标,表示在世界空间下的一个3D方向,方向矢量从立方体中心出发,向外部延伸就会和立方体的6个纹理之一发生相交,采样结果由交点计算而来。
立方体纹理在实时渲染中最常见的应用是天空盒子以及环境映射。
天空盒子的创建通过Unity自带的Skybox/6 Sided材质创建,需要对应6张纹理,即
立方体纹理用于环境映射,模拟类似金属反射周围环境的效果,这个过程首先需要获取到指定位置的立方体纹理,再应用到反射和折射计算。
Step1 获取指定位置的立方体纹理
主要通过脚本实现,添加编辑器工具,利用Unity提供的Camera.RenderToCubemap方法生成立方体纹理,完整代码:
public class RenderCubeMapWizard : ScriptableWizard
{
public Transform renderFromPosition;
public Cubemap cubemap;
void OnWizardUpdate()
{
helpString = "Select transform to render from and cubemap to render into";
isValid=(renderFromPosition!=null)&&(cubemap!=null);
}
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 cubemap", "Render");
}
}
需要添加 “using UnityEditor”命名空间,点击“Render”脚本执行后会将对应点的立方体纹理渲染到指定的立方体纹理中。
Step2 反射计算
反射效果通过入射光线方向和表面法线得到反射方向,再利用反射方向对立方体纹理进行采样,得到反射效果。入射光线为观察方向的反方向,主要使用CG函数中的reflect函数。完整代码:
Shader "Custom/Chapter10_Reflection" {
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{
Pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
fixed4 _ReflectionColor;
float _ReflectionAmount;
samplerCUBE _Cubemap;
struct a2v{
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)
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.worldPos=mul(_Object2World,v.vertex).xyz;
o.worldNormal=UnityObjectToWorldNormal(v.normal);
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 worldViewDir=normalize(i.worldViewDir);
fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse=_LightColor0.rgb*_Color.rgb*max(dot(worldNormal,worldLightDir),0);
fixed3 reflection=texCUBE(_Cubemap,i.worldRefl).rgb*_ReflectionColor.rgb;
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
fixed3 color=ambient+lerp(diffuse,reflection,_ReflectionAmount)*atten;
return fixed4(color,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
Step3 折射计算 折射计算和反射计算类似,先计算出折射方向,再向生成立方体纹理进行采样,主要用到CG函数中的refract函数,传入三个参数,入射方向,法线方向,折射率比值(入射到反射方向)值得注意的是 传入的方向参数必须是归一化之后的方向。完整代码为:
Shader "Custom/Chapter10_Refraction" {
Properties{
_Color("Color",Color)=(1,1,1,1)
_RefractionColor("Reflection Color",Color)=(1,1,1,1)
_RefractionAmount("Reflection Amount",Range(0,1))=1
_RefractionRatio("Refraction Ratio",Range(0.1,1))=0.5
_Cubemap("Cubemap",Cube)="_Skybox"{}
}
SubShader{
Pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
fixed4 _RefractionColor;
float _RefractionAmount;
float _RefractionRatio;
samplerCUBE _Cubemap;
struct a2v{
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)
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
o.worldNormal=UnityObjectToWorldNormal(v.normal);
o.worldViewDir=UnityWorldSpaceViewDir(o.worldPos);
//将观察方向的反方向作为入射方向去计算折射方向
o.worldRefr=refract(-normalize(o.worldViewDir),normalize(o.worldNormal),_RefractionRatio);
TRANSFER_SHADOW (o);
return o;
}
fixed4 frag(v2f i):SV_Target{
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldViewDir=normalize(i.worldViewDir);
fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse=_LightColor0.rgb*_Color.rgb*max(dot(worldNormal,worldLightDir),0);
fixed3 refraction=texCUBE(_Cubemap,i.worldRefr).rgb*_RefractionColor.rgb;
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
fixed3 color=ambient+lerp(diffuse,refraction,_RefractionAmount)*atten;
return fixed4(color,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
菲涅尔反射
菲涅尔反射描述一种光学现象,当光线照射到物体表面时,一部分发生反射,一部分发生折射,一部分进入物体内部发生反射或折射。实时渲染中,经常使用菲涅尔反射,通过视角方向控制反射程度。
菲涅尔反射通过菲涅尔等式计算,真实的菲尼尔反射非常复杂,实时渲染中使用近似公式计算,两个用的比较多的近似公式:
许多车漆、水面等材质的渲染经常使用菲涅尔反射模拟更加真实的反射效果。
使用Schlick菲涅尔近似等式模拟,完整代码:
Shader "Custom/Chapter10_Fresnel" {
Properties{
_Color("Color",Color)=(1,1,1,1)
_FresnelFactor("FresnelFactor",Range(0,1))=0.5
_FreractionRatio("FreractionRatio",Range(0.1,1))=0.5
_Cubemap("Cubemap",Cube)="_Skybox"{}
}
SubShader{
Pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
float _FresnelFactor;
float _FreractionRatio;
samplerCUBE _Cubemap;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
};
struct v2f{
float4 pos:SV_POSITION;
float3 worldPos :TEXCOORD0;
float3 worldNormal:TEXCOORD1;
float3 worldViewDir:TEXCOORD2;
float3 worldRefl:TEXCOORD3;
float3 worldRefr:TEXCOORD4;
SHADOW_COORDS(5)
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
o.worldNormal=UnityObjectToWorldNormal(v.normal);
o.worldViewDir=UnityWorldSpaceViewDir(o.worldPos);
o.worldRefl=reflect(-o.worldViewDir,o.worldNormal);
o.worldRefr=refract(-normalize(o.worldViewDir),normalize(o.worldNormal),_FreractionRatio);
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i):SV_Target{
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldViewDir=normalize(i.worldViewDir);
fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse=_LightColor0.rgb*_Color.rgb*max(dot(worldNormal,worldLightDir),0);
fixed3 reflect=texCUBE(_Cubemap,i.worldRefl).rgb;
fixed3 refract=texCUBE(_Cubemap,i.worldRefr).rgb;
fixed fresnel=_FresnelFactor+(1-_FresnelFactor)*pow(1-dot(worldViewDir,worldNormal),5);
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
//fixed3 color=ambient+lerp(diffuse,reflect,saturate(fresnel))*atten;
fixed3 color=ambient+lerp(refract,reflect,saturate(fresnel))*atten;
return fixed4(color,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
最后的颜色混合部分,可以将漫反射与反射通过菲涅尔反射系数进行插值,也可以将折射与反射通过菲涅尔反射系数进行插值混合,实例效果:
关于反射和折射部分:
- step1: 得到环境立方体纹理
- step2: 使用函数计算反射或折射方向
- step3: 利用 texCUBE 对立方体纹理采样
- step4: 计算影响系数,对 漫反射与反射或折射/折射与反射 进行插值混合,得到最终颜色
渲染纹理
现代GPU允许把整个三维场景渲染到中间缓冲中,而不是帧缓冲当中,这个中间缓冲叫做渲染目标纹理(Render Target Texture RTT) ,与之对应的是多重渲染目标(Mutil-Render Target) MRT 将场景渲染到多个渲染目标纹理中。为此,Unity专门定义了一种纹理类型——渲染纹理(Render Texture)。其使用通常有两种方式:
- 创建渲染纹理,将某个摄像机的渲染目标设置成该渲染纹理,摄像机的渲染结果就会实时渲染到该纹理中
- 通过后期处理抓取当前屏幕图像,Unity将屏幕图像放到一张同等分辨率的渲染纹理中
使用渲染纹理实现镜子效果
通过一个额外的摄像机,调整到对应位置,设置渲染目标为一张渲染纹理,将该渲染纹理作为一张2D纹理,在采样是,将UV坐标的进行翻转即可,完整代码:
Shader "Custom/Chapter10_Mirror" {
Properties{
_MainTex("MainTex",2D)="white"{}
}
SubShader{
Pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
struct a2v{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
};
struct v2f{
float4 pos:SV_POSITION;
float4 uv:TEXCOORD0;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=v.texcoord;
o.uv.x=1-o.uv.x; //将uv.x分量进行翻转,实现镜子效果
return o;
}
fixed4 frag(v2f i):SV_Target{
return tex2D(_MainTex,i.uv);
}
ENDCG
}
}
FallBack "Diffuse"
}
玻璃效果
Unity Shader中可以使用GrabPass完成对屏幕图像的抓取。定义GrabPass后,Unity将当前屏幕图像绘制在一张纹理中,使用GrabPass模拟玻璃透明效果,可以对物体后面的图像做更复杂的处理(使用法线模拟折射效果),而不是像使用透明度混合,只是颜色上的混合。
在使用GrabPass进行透明效果模拟时,要注意渲染顺序的设置 ,先保证场景中所有不透明物体已经绘制在屏幕上,再对屏幕进行抓取图像,因此一般设置成 "Queue"="Transparent"
实现玻璃效果:
- Step1 获取指定位置的立方体纹理,通过反射方向采样得到反射颜色
- Step2 获取屏幕抓取图像,通过法线纹理得到法线方向作为影响值与影响因子相乘,调整获取的屏幕图像扭曲程度来模拟折射
- Step3 将两者颜色进行混合,通过混合值调整反射和折射的混合程度
完整代码:
Shader "Custom/Chapter10_GlassRefraction" {
Properties{
_MainTex("Main Tex",2D)="white"{}
_BumpTex("Bump Tex",2D)="bump"{}
_CubeMap("Cube Map",Cube)="_Skybox"{}
_Distortion("Distortion",Range(0,100))=10
_RefractAmount("Refract Amount",Range(0.0,1.0))=1.0
}
SubShader{
Tags{"Queue"="Transparent" "RenderType"="Opaque"}
//指定渲染队列为"Transparent",确保所有不透明物体先渲染完成
GrabPass {"_RefractionTex"}
//声明GrabPass 该Pass会将屏幕抓取图像存储到名为"_RefractionTex"的纹理中
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpTex;
float4 _BumpTex_ST;
samplerCUBE _CubeMap;
float _Distortion;
float _RefractAmount;
sampler2D _RefractionTex; //存储GrabPass抓取的屏幕图像
float4 _RefractionTex_TexelSize; //得到屏幕图像的纹素值,在做偏移计算时使用
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 tangent:TANGENT;
float4 texcoord:TEXCOORD0;
};
struct v2f{
float4 pos:SV_POSITION;
float4 uv:TEXCOORD0;
float4 scrPos:TEXCOORD1;
float4 TtoW0:TEXCOORD2;
float4 TtoW1:TEXCOORD3;
float4 TtoW2:TEXCOORD4;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.scrPos=ComputeGrabScreenPos(o.pos);
o.uv.xy=TRANSFORM_TEX(v.vertex,_MainTex);
o.uv.zw=TRANSFORM_TEX(v.vertex,_BumpTex);
float3 worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
fixed3 worldNormal=UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent=UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal=cross(worldNormal,worldTangent)*v.tangent.w;
o.TtoW0=(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.TtoW1=(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);
o.TtoW2=(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);
return o;
}
fixed4 frag(v2f i):SV_Target{
float3 worldPos=(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w);
fixed3 worldViewDir=normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 bump=UnpackNormal(tex2D(_BumpTex,i.uv.zw));
float2 offset=bump*_Distortion*_RefractionTex_TexelSize.xy;
i.scrPos.xy=offset+i.scrPos.xy;
fixed3 refrColor=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 reflectDir=reflect(-worldViewDir,bump);
fixed4 texColor=tex2D(_MainTex,i.uv.xy);
fixed3 reflColor=texCUBE(_CubeMap,reflectDir).rgb*texColor.rgb;
fixed3 finalColor=reflColor*(1-_RefractAmount)+refrColor*_RefractAmount;
return fixed4(finalColor,1.0);
}
ENDCG
}
}
FallBack "Tranparent/VertexLit"
}
这里需要解释一下 ComputeGrabScreenPos函数,与** ComputeScreenPos**类似,输入为顶点在裁剪空间下的坐标,得到屏幕坐标,但是这里需要注意的是, 此时得到的坐标并没有进行归一化,也就是还没有除w分量,这一点可以从ComputeScreenPos的定义中看出来:
pos是顶点在裁剪空间的坐标,_ProjectionParams.X默认情况下为1,针对不同平台可能会做翻转处理,实际上得到的结果:
也就是说,此时得到的坐标并不是最终屏幕图像上的坐标,因此在片元着色器中在对抓取的屏幕图像(实际上是对这一张纹理进行取样)取样时,会有 除以w分量的操作(得到[0-1]的UV纹理坐标)
tex2D(_RefractionTex,i.scrPos.xy/i.scrPos.w)
而这样做的原因是,在顶点着色器内直接除w分量会影响插值的结果,因而将该操作保留到片元着色器中进行逐像素处理。
最终效果:
扭曲因子的值设为100,混合值设为1(全为折射效果)
扭曲因子的值设为0,混合值设为1(全为折射效果)
可以看到这个时候,几乎看不到物体,这是由于物体表面由物体后面渲染的屏幕图像区域进行了着色,所以这种看似透明的效果是通过设置正确的渲染顺序,抓取屏幕图像实现的,而不是给物体本身设置透明材质。
程序纹理
程序纹理是通过计算机计算生成的图像,使用特定的算法创建个性化图案或非常真实的自然元素。
创建一个波点纹理
完整代码:
[ExecuteInEditMode]
public class ProceduralTextureGeneration : MonoBehaviour {
public Material material = null;
#region Material properties
[SerializeField,SetProperty("textureWidth")]
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();
}
}
#endregion
private Texture2D m_generateTexture = null;
// Use this for initialization
void Start () {
if (material == null)
{
Renderer renderers = gameObject.GetComponent<Renderer>();
if (renderers == null)
{
Debug.LogWarning("Cannot find a renderer.");
return;
}
material = GetComponent<Renderer>().sharedMaterial;
}
_UpdateMaterial();
}
// Update is called once per frame
private void _UpdateMaterial() {
if (material != null)
{
m_generateTexture = _GenerateProceduralTexture();
material.SetTexture("_MainTex",m_generateTexture);
}
}
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;
//绘制9个圆
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,1f,dist*edgeBlur));
//与之前得到的颜色混合
pixel = _MixColor(pixel, color, color.a);
}
}
proceduralTexture.SetPixel(w,h,pixel);
}
}
proceduralTexture.Apply();
return proceduralTexture;
}
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;
}
}