Shader笔记十一 高级纹理

高级纹理

立方体纹理
立方体纹理(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菲涅尔近似等式

    F为反射系数,控制菲涅尔反射强度,v为视角方向,n为法线方向
  • Empricial菲涅尔近似等式

    bias、scale和power是控制项

许多车漆、水面等材质的渲染经常使用菲涅尔反射模拟更加真实的反射效果。
使用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;
}
}

实例效果:


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值