一、立方体纹理
立方体纹理是环境映射的一种实现方式,立方体纹理就是立方体的六个面,每个面有一个纹理,一般用于映射出物体周围环境。
和基础纹理不同,采样立方体纹理需要一个三维坐标,而这个三维坐标由一条向量与立方体的交点构成,注意采样时,向量是由立方体内的某一点发出,向外指的,不是立方体外的某一点指向立方体内部。
关于凹物体可能会反射自身的问题不做讨论。
Unity里的天空盒就是一种立方体纹理,将整个场景包围在一个立方体内。
1.1 创建立方体纹理:
立方体纹理主要用于环境映射,说白点就是让一个物体表面反射出周围环境的样子,但每个物体在不同地方的采样立方体纹理有差距,因此要用脚本辅助截取当前物体所处位置的立方体纹理。
脚本:
using UnityEngine;
using UnityEditor;
using System.Collections;
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!");
}
}
通过继承窗口编辑器实现。
1.2 利用反射采样立方体纹理:
利用入射方向(说反射更合理)求采样坐标,我们知道反射方向就是视角方向,根据光路可逆可以求出立方体纹理的哪个点发出的光线进入人眼,而这个点纹理颜色,就是这个片元或者说顶点的颜色,但注意采样时的向量是向外发散的,如果你计算的时候没有对视角方向取反,那么就需要对入射方向取反(取决于你在那个阶段计算),入射方向无需单位化 。
Shader "Custom/Test0"
{
Properties
{
_Color("Color",Color)=(1,1,1,1)
_CubemapColor("立方体纹理颜色",Color)=(1,1,1,1)
//越强越能看出周围环境
_ReflectLevel("立方体纹理反射水平",Range(0,1))=1
_Cubemap("立方体纹理",Cube)="_Skybox"{}
}
SubShader
{
Pass
{
Tags{"LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
samplerCUBE _Cubemap;
fixed4 _CubemapColor;
fixed _ReflectLevel;
fixed _AlphaBlend;
struct a2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
};
struct v2f
{
float4 pos:SV_POSITION;
float4 worldPos:TEXCOORD0;
float3 worldNormal :TEXCOORD1;
float3 texDir:TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos=mul(unity_ObjectToWorld,v.vertex);
float3 worldViewDir=UnityWorldSpaceViewDir(o.worldPos);
o.texDir=reflect(-worldViewDir,o.worldNormal);
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.rgb;
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
fixed3 refleResult=texCUBE(_Cubemap,i.texDir)*_CubemapColor;
fixed3 diffuse=_LightColor0*_Color
*(0.5*dot(worldLightDir,worldNormal)+0.5);
fixed3 color=ambient+lerp(diffuse,refleResult,_ReflectLevel);
return fixed4(color,1);
}
ENDCG
}
}
}
1.3 利用折射采样立方体纹理:
立方体纹理的某一点经过物体内部的折射,最终进入摄像机,过程也是求入射方向是哪个点发出的,同样我们也知道反射方向。
折射公式:
Cg函数: refract(入射方向,法线方向,
);
Shader "Custom/Test0"
{
Properties
{
_Color("Color",Color)=(1,1,1,1)
_CubemapColor("立方体纹理颜色",Color)=(1,1,1,1)
//越强越能看出周围环境
_RefractLevel("立方体纹理折射水平",Range(0,1))=1
_Cubemap("立方体纹理",Cube)="_Skybox"{}
_RefractRatio("介质比值",Range(0,1))=0.5
}
SubShader
{
Pass
{
Tags{"LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
samplerCUBE _Cubemap;
fixed4 _CubemapColor;
fixed _RefractLevel;
fixed _RefractRatio;
struct a2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
};
struct v2f
{
float4 pos:SV_POSITION;
float4 worldPos:TEXCOORD0;
float3 worldNormal :TEXCOORD1;
float3 texDir:TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos=mul(unity_ObjectToWorld,v.vertex);
float3 worldViewDir=UnityWorldSpaceViewDir(o.worldPos);
o.texDir=refract(normalize(-worldViewDir),normalize(o.worldNormal),_RefractRatio);
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.rgb;
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
fixed3 refleResult=texCUBE(_Cubemap,i.texDir)*_CubemapColor;
fixed3 diffuse=_LightColor0*_Color
*(0.5*dot(worldLightDir,worldNormal)+0.5);
fixed3 color=ambient+lerp(diffuse,refleResult,_RefractLevel);
return fixed4(color,1);
}
ENDCG
}
}
}
1.4 利用菲涅尔反射控制立方体采样:
菲涅尔,通常指的是反射光和入射光存在一定比例,比如水面,当我们站在不同角度看,有些角度可以看到水面下,有些角度就是一片白太阳光全反射。
Shader "Custom/Test0"
{
Properties
{
_Color("Color",Color)=(1,1,1,1)
_CubemapColor("立方体纹理颜色",Color)=(1,1,1,1)
//越强越能看出周围环境
_FresnelScale("菲涅尔系数",Range(0,1))=1
_Cubemap("立方体纹理",Cube)="_Skybox"{}
}
SubShader
{
Pass
{
Tags{"LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
samplerCUBE _Cubemap;
fixed4 _CubemapColor;
fixed _FresnelScale;
struct a2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
};
struct v2f
{
float4 pos:SV_POSITION;
float4 worldPos:TEXCOORD0;
float3 worldNormal :TEXCOORD1;
float3 worldViewDir :TEXCOORD2;
float3 texDir:TEXCOORD3;
};
v2f vert(a2v 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.texDir=reflect(-o.worldViewDir,o.worldNormal);
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.rgb;
fixed3 worldNormal=normalize(i.worldNormal);
i.worldViewDir=normalize(i.worldViewDir);
fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
fixed fresnel=_FresnelScale+(1-_FresnelScale)*pow(1-dot(i.worldViewDir,i.worldNormal),5);
fixed3 refleResult=texCUBE(_Cubemap,i.texDir)*_CubemapColor;
fixed3 diffuse=_LightColor0*_Color
*dot(worldLightDir,worldNormal);
fixed3 color=ambient+lerp(diffuse,refleResult,saturate(fresnel));
return fixed4(color,1);
}
ENDCG
}
}
}
当菲涅尔系数为0时,物体就只有边缘可以反射立方体纹理,从而实现了边缘发光。当然不是只有立方体纹理可以用边缘发光,最终Lerp插值那里可以是随便一个颜色。而且菲涅尔也不一定非要用立方体纹理这种混合方式,随便一张纹理什么的也可以。反正图形学第一准则:如果这个东西看起来是对的,那么它就是对的。
二、渲染纹理
就是Unity内置的Render Texture,即把场景渲染到一张纹理中,Unity允许我们在场景渲染中或者渲染完毕时获取当前的渲染图像用于做一些特殊操作。
创建Render Texture的方法。一个是在Assets里创建,然后用摄像机填充,另一个是在Shader中使用GrabPass命令或者脚本内的OnRenderImage函数。
在这里我们使用第一种方法,因为简单,而且适用于只需要一次抓取就能解决情况的Shader,一般OnRenderImage适用于需要对渲染纹理做出多次处理,在后面我们会遇到。其次,GrabPass多用于配合内置函数实现局部渲染纹理的抓取,而后者多用于整个图像的处理。
关于渲染纹理能做的事情,这里只举两个例子,一个是类似的镜子反射,即用一个摄像机拍出来背面,然后用一个面片当镜子显示渲染纹理。
Shader "Custom/Test0"
{
Properties
{
_RenderTex("渲染纹理",2D)="white"{}
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _RenderTex;
float4 _RenderTex_ST;
struct a2v
{
float4 vertex:POSITION;
float2 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD3;
};
v2f vert(a2v v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=TRANSFORM_TEX(v.texcoord,_RenderTex);
o.uv.x=1-o.uv.x;
return o;
}
fixed4 frag(v2f i):SV_Target
{
return tex2D(_RenderTex,i.uv);
}
ENDCG
}
}
}
这样写其实不是完整的镜子,有很大的局限性,首先镜面的显示内容受面片大小以及纹理的大小影响,你要不停的调整摄像机参数来达成镜子中比较真实的物体的大小,其次,你一旦转动视角,镜子内的画面并不会随之改变,从镜子的成像原理上也可以解释这种实现方法的不真实性。
第二个是玻璃折射效果,这个理解起来复杂一些。我们平常看玻璃,光线进入我们的眼睛中可以分为两种主要类型:一个是玻璃后面物体透过玻璃的折射光线,一个是玻璃表面的反射环境的反射光线。
反射光线好说,用立方体纹理采个样。
折射光线的模拟稍微复杂,就是利用内置函数抓取玻璃后面的图像,然后用玻璃的法线贴图给玻璃后面的图像做一个偏移或者说扰动。这里涉及渲染顺序的问题,我们要等所有不透明物体(当然,你要是想模拟透明物体的折射效果就把渲染顺序再往后设一级)渲染完毕后抓取当时的渲染纹理,然后用我们的法线贴图和采样纹理相加模拟出偏移效果。
Shader "Custom/Test0"
{
Properties
{
_MainTex("主要纹理",2D)="white"{}
_NormalTex("法线贴图",2D)="bump"{}
_CubeMap("立方体纹理",Cube)="_Skybox"{}
_RefractLevel("折射程度",Range(0,100))=50
_FinalScale("反射与折射的混合比例",Range(0,1))=0.5
}
SubShader
{
Tags{"Queue"="Transparent" "RenderType"="Opaque"}
GrabPass{"_RefractionTex"}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NormalTex;
float4 _NormalTex_ST;
sampler2D _RefractionTex;
float4 _RefractionTex_TexelSize;
samplerCUBE _CubeMap;
fixed _RefractLevel;
fixed _FinalScale;
struct a2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 tangent:TANGENT;
float2 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float4 uv:TEXCOORD0;
float4 scrPos:TEXCOORD1;
float4 worldPos:TEXCOORD2;
float3 worldNormal:TEXCOORD3;
};
v2f vert(a2v v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.worldPos=mul(unity_ObjectToWorld,v.vertex);
o.worldNormal=UnityObjectToWorldNormal(v.normal);
o.uv.xy=TRANSFORM_TEX(v.texcoord,_MainTex);
o.uv.zw=TRANSFORM_TEX(v.texcoord,_NormalTex);
o.scrPos=ComputeGrabScreenPos(o.pos);
return o;
}
fixed4 frag(v2f i):SV_Target
{
float3 worldViewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 tangentNormal=UnpackNormal(tex2D(_NormalTex,i.uv.zw));
//折射部分
fixed2 offset=tangentNormal.xy*_RefractLevel*_RefractionTex_TexelSize.xy;
i.scrPos.xy=offset+i.scrPos.xy;
fixed3 refract=tex2D(_RefractionTex,i.scrPos.xy/i.scrPos.w);
//反射部分
fixed3 reflectionDir=reflect(-worldViewDir,i.worldNormal);
fixed4 texColor=tex2D(_MainTex,i.uv.xy);
fixed3 reflection=texCUBE(_CubeMap,reflectionDir)*texColor;
//最终结果
fixed3 finalColor=reflection*(1-_FinalScale)+refract*_FinalScale;
return fixed4(finalColor,1);
}
ENDCG
}
}
}
渲染纹理能实现的效果非常非常多。
关于GrabPass更加详细的解释,(32条消息) Unity Shader GrabPass 使用注意的问题_Jave.Lin的博客
总结大概就是,GrabPass{ }和GrabPass{ "Name" }的区别,前者会向多张纹理输出,彼此互不干扰,后者是只要纹理名字一样,全给渲染到给定名字的纹理中。这里就会涉及到一个叠加问题,前者的结果如果物体重合多个会产生纹理叠加现象,但后者并不会。性能方面后者会更好,而前者好像还增加了DrawCall等等什么的。
关于采样渲染纹理时使用的函数ComputeGrabScreenPos的作用,输入齐次裁剪空间下的坐标,转化为对应屏幕采样的uv坐标,这个uv坐标需要除以w分量才可以使用,Unity在计算时为了不破坏插值并未在计算时除以w分量。关于这个计算过程,网上很多博客都解释的很清楚,这里就不再赘述了。
关于另一个函数ComputeScreenPos的作用,去官网查了查,Unity的意思是尽量使用ComputeGrabScreenPos函数,因为后者会在某些情况下出现纹理翻转问题。
三、程序纹理
指计算机生成的图像一般用于创建个性图案或非常真实的自然元素,例如石子,木头等等,了解即可。