高级纹理
立方体纹理
在图形学中,立方体纹理(Cubemap)是环境映射(Environment Mapping)的一种实现方法。环境映射可以模拟物体周围的环境,而使用了环境映射的物体可以看起来像镀了层金属一样反射出周围的环境。
立方体纹理一共包含了6张图像,这些图像对应了一个立方体的6个面,立方体的名称也由此而来。对立方体纹理采样我们需要提供一个三维的纹理坐标,这个三维纹理坐标表示我们在世界空间下的一个3D方向。
优点
- 立方体纹理的实现简单快捷,而且得到的效果也比较好。
缺点
例如当场景中引入了新的物体、光源,或者物体发生移动时,我们就需要重新生成立方体纹理。
-
立方体纹理也仅可以反射环境,但不能反射使用了该立方体纹理的物体本身。这是因为,立方体纹理不能模拟多次反射的结果。
由于这样的原因,想要得到令人信服的渲染结果,我们应尽量对凸面体而不要对凹面体使用立方体纹理(因为凹面体纹理会反射自身)。
天空盒子
在Unity中,想要使用天空盒子非常简单。
- 新建一个材质,命名为SkyboxMat。
- 在Skybox的Unity Shader下拉菜单中选择Unity自带的Skybox/6 Sided,该材质需要6张纹理。
- 使用6张纹理对第二步的材质赋值(为了让天空盒子正常渲染,我们需要把这6张纹理的Wrap Mode设置为Clamp,以防止在接缝处出现不匹配现象。)
- 用这个天空盒纹理来替换默认的天空盒,打开菜单栏的Window->Rendering->Lighting Settings
- 为了让摄像机正常显示天空盒子,我们还需要保证渲染摄像机的Camera组件中Clear Flags被设置为Skybox。
上面的材质中,除了6张纹理属性外还有3个属性:
- Tint Color:用于控制该材质的整体颜色;
- Exposure:用于调整天空盒子的亮度;
- Rotation:用于调整天空盒子沿+y轴方向的旋转角度。
用于环境映射的立方体纹理
立方体纹理最常见的用处是用于环境映射。通过这种方法,我们可以模拟出金属质感的材质。
创建用于环境映射的立方体纹理的方法有3种:
- 第一种方法是直接由一些特殊布局的纹理创建;
- 第二种方法是手动创建一个Cubemap资源,再把6张图赋给它;
- 第三种方法是由脚本生成。
第一种方法:我们需要提供一张具有特殊布局的纹理,例如类似立方体展开图的交叉布局、全景布局等(就类似于人物贴图,换成了球形而已)。然后我们只需要把该纹理的Texture Type设置为Cubemap即可,Unity会为我们做好剩下的事情。
第二种方法:我们首先要在项目资源中创建一个Cubemap,然后把它的6张纹理拖拽到它的面板中。在Unity 5中,官方推荐使用第一种方法创建立方体纹理,这是因为第一种方法可以对纹理数据进行压缩,即可以支持边缘修正、光滑反射(glossy reflection)和HDR等功能。
第三种方法:使用Camera.RenderToCubemap函数来实现,Camera.RenderToCubemap函数可以把任意位置观察到的场景图像存储到6张图像中,从而创建出该位置上对应的立方体纹理。
-
创建一个编辑器脚本,用于将摄像机照射到的图片渲染到Cubemap中。由于该代码需要添加菜单条目,因此我们需要把它放在Editor文件夹下才能正确执行。原理如下:
在renderFromPosition(由用户指定)位置处动态创建一个摄像机,并调用Camera.RenderToCubemap函数把从当前位置观察到的图像渲染到用户指定的立方体纹理cubemap中,完成后再销毁临时摄像机。
using UnityEngine; using UnityEditor; using System.Collections; public class RenderCubemapWizard : ScriptableWizard { public Transform renderFromPosition; public Cubemap cubemap; void OnWizardUpdate () { helpString = "选择要渲染的坐标位置和要渲染的cubemap"; isValid = (renderFromPosition != null) && (cubemap != null); } void OnWizardCreate () { // 创建用于渲染的临时摄像机 GameObject go = new GameObject( "CubemapCamera"); go.AddComponent<Camera>(); // 把它放到物体坐标上 go.transform.position = renderFromPosition.position; // 渲染成cubemap go.GetComponent<Camera>().RenderToCubemap(cubemap); // 销毁临时相机 DestroyImmediate( go ); } [MenuItem("GameObject/Render into Cubemap")] static void RenderCubemap () { ScriptableWizard.DisplayWizard<RenderCubemapWizard>( "Render cubemap", "Render!"); } }
新建一个用于存储的立方体纹理(在Project视图下单击右键,选择Create->Legacy->Cubemap来创建)。为了让脚本可以顺利将图像渲染到该立方体纹理中,我们需要在它的面板中勾选Readable选项。
-
从Unity菜单栏选择GameObject->Render into Cubemap,打开我们在脚本中实现的用于渲染立方体纹理的窗口,并把第一步创建的GameObject和第二步中的纹理分别拖拽到窗口中的Render From Position和Cubemap选项,如下图所示:
单击窗口的Render!按钮,就可以把从该位置观察到的世界空间下的6张图像中渲染到纹理中,如下图所示:
需要注意的是,我们需要为Cubemap设置大小,即上图中的Face size选项。Face size值越大,渲染出来的立方体纹理分辨率越大,效果可能更好,单需要占用的内存也越大,这可以由面板最下方显示的内存大小得到。
反射
使用了反射效果的物体通常看起来就像镀了层金属。想要模拟反射效果很简单,我们只需要通过入射光线的方向和表面法线方向来计算反射方向,再利用反射方向对立方体纹理采样即可。
反射用到Shader的是在漫反射模型上进行修改的:
- 增添三个属性,分别用于控制反射颜色、反射程度和捕捉环境映射纹理
- 在顶点着色器中哪个使用reflect函数来计算该顶点的反射方向
reflect(I, N) 根据入射光线方向I和表面法向量N计算反射向量,仅对三元向量有效
- 在片元着色器中使用texCUBE函数和顶点着色器中获得的反射方向对立方体纹理进行采样,得到反射颜色
- 最后使用lerp对原有漫反射与纹理反射颜色进行插值
实现代码见下文;
折射
折射的物理原理比反射复杂一些。当给定入射角时,我们可以使用斯涅尔定律(Snell’s law)来计算反射角。当光从介质1沿着和表面法线夹角为θ1的方向斜射入介质2时,我们可以使用如下公式计算折射光线与法线的夹角θ2:
η1sinθ1=η2sinθ2
其中η1和η2分别是两个介质的折射率(index of refraction)。折射率是一项重要的物理常量,例如真空的折射率是1,而玻璃的折射率一般是1.5。下图给出了这些变量之间的关系。
反射用到Shader的是在与反射类似,依旧使用reflect函数来计算该顶点的折射方向,但不同的是采用了三个参数:
- 第一个参数即为入射光线方向,它必须是归一化后的矢量;
- 第二个参数是表面法线,法线方向同样是要归一化后的;
- 第三个参数是入射光线所在介质的折射率和折射光线所在介质的折射率之间的比值。
具体实现代码见下文。
菲涅尔反射
我们经常会使用菲涅尔反射(Fresnel reflection)来根据视角方向控制反射过程。
通俗地讲,菲涅尔反射描述了一种光学现象,即当光线照射到物体表面上时,一部分发生反射,一部分进入物体内部,发生折射或散射。被反射的光和入射光之间存在一定的比率关系,这个比率关系可以通过菲涅尔等式进行计算。
一个经常使用的例子是,当你站在湖边,直接低头看脚边的水面时,你会发现水几乎是透明的,你可以直接看到水底的小鱼和石子;但是当你抬头看远处的水面时,会发现几乎看不到水下的情景,而只能看到水面反射的环境。这就是所谓的菲涅尔效果。
真实世界的菲涅尔等式是非常复杂的,但在实时渲染中,我们通常会使用一些近似公式来计算。其中一个著名的计算公式就是Schlick菲涅尔近似等式:
FSchlick ( v , n ) = F0 + ( 1 - F0 ) ( 1 - v · n)5
其中,F0是一个反射系数,用于控制菲涅尔反射的强度,v是视角方向,n是表面法线。另一个应用比较广泛的等式是Empricial菲涅尔近似等式:
FEmpricial ( v , n ) = max ( 0 , min ( 1 , bias + scale * ( 1 - v · n ) power))
其中,bias、scale和power是控制项
下面我们将使用Schlick菲涅尔近似等式来模拟菲涅尔反射。在片元着色器中,我们套用Schlick菲涅尔公式来计算菲尼尔比率,然后用该比率插值混合漫反射光照和反射光照。具体代码实现见下文。
渲染纹理
一个摄像机的渲染结果会输出到颜色缓冲中,并显示到我们的屏幕上。现代的GPU允许我们把整个三维场景渲染到一个中间缓存中,即渲染目标纹理(Render Target Texture,RTT),而不是传统的帧缓冲或后备缓冲(back buffer)。与之相关的是多重渲染目标(Multiple Render Target ,MRT),这种技术指的是GPU允许我们把场景同时渲染到多个目标纹理中,而不再需要为每个渲染目标纹理单独渲染完整的场景。延迟渲染就是使用多重渲染目标的一个应用。
Unity为渲染目标纹理定义了一种专门的纹理类型——渲染纹理(Render Texture)。在Unity中使用渲染纹理通常有两种方式:
- 一种方式是在Project目录下创建一个渲染纹理,然后把某个摄像机的渲染目标设置成该渲染纹理,这样一来该摄像机的渲染结果就会实时更新到渲染纹理中,而不会显示在屏幕上。使用这种方法,我们还可以选择纹理的分辨率、滤波模式等纹理属性。
- 另一种方式是在屏幕后处理时使用GrabPass命令或OnRenderImage函数来获取当前屏幕图像,Unity会把这个屏幕图像放到一张和屏幕分辨率等同的渲染纹理中,下面我们可以在自定义的Pass中把它们当成普通纹理来处理,从而实现各种屏幕特效。
镜子效果
本节中,我们将实现下面效果
创建一个四边形(Quad),调整它的位置和大小,用于充当镜子。
为了得到从镜子出发观察到的场景图像,我们需要创建一个摄像机,并调整它的位置、裁剪平面、视角等,使得它显示的图像是我们希望的镜子的图像。
-
由于这个摄像机不需要直接显示在屏幕上,而是用于渲染纹理。因此我们可以直接创建一个Texture并拖拽到该摄像机的Target Texture上。下图显示了摄像机面板和渲染纹理的相关设置。
关于Quad的着色器实现也非常简单,只需要声明一个纹理属性,然后将该纹理进行左右翻转后输出,实现代码见下文。
玻璃效果
在Unity中,我们还可以在Unity Shader中使用一种特殊的Pass来完成获取屏幕图像的目的,这就是GrabPass。我们通常会使用GrabPass来实现诸如玻璃等透明材质的模拟,与使用简单的透明混合不同,使用GrabPass可以让我们对物体后面的图像进行更复杂的处理,例如使用法线来模拟折射效果,而不再是简单的和原屏幕颜色混合。
当我们在Shader中定义了一个GrabPass后,Unity会把当前屏幕的图像绘制在一张纹理中,以便我们在后续的Pass中访问它。需要注意的是,在使用GrabPass的时候,我们需要额外小心物体的渲染队列设置。正如之前所说,GrabPass通常用于渲染透明物体,尽管代码里并不包含混合指令,但我们往往仍然需要把物体的渲染队列设置成透明队列(即"Queue"="Transparent")。这样才能保证渲染物体时,所有的不透明物体都已经被绘制在屏幕上,从而获得正确的屏幕图像。
GrabPass支持两种形式
- 直接使用GrabPass{},然后在后续的Pass中直接使用_GrabTexture来访问屏幕图像。但是当场景中有多个物体都使用了这样的形式来抓取屏幕时,这种方法的性能消耗比较大,因为对于每一个使用它的物体,Unity都会为它单独进行一次昂贵的屏幕抓取操作。但这种方法可以让每个物体得到不同的屏幕图像,这取决于它们的渲染队列及渲染它们时当前的屏幕缓冲中的颜色。
- 使用GrabPass{"TextureName"},正如本节所实现,我们可以在后续的Pass中使用TextureName来访问屏幕图像。使用这种方法同样可以抓取屏幕,但Unity只会在每一帧为第一个使用名为TextureName的纹理的物体执行一次屏幕抓取操作,而这个纹理同时也可以在其它Pass中被访问。这种方法更加高效,因为不管场景中有多少物体使用了该命令,每一帧中Unity都只会执行一次抓取操作,这也意味着所有物体都会使用同一张屏幕图像。不过,在大多数情况下这已经足够了。
在本节中,我们将会使用GrabPass来模拟一个玻璃效果。学习完本节后,我们可以得到类似下图的效果。
思路:
这种效果实现非常简单,我们首先使用一张法线纹理来修改模型的法线信息,然后使用反射的方法,通过一个Cubemap来模拟玻璃反射,而在模拟折射时,则使用了GrabPass获取玻璃后面的屏幕图像,并使用切线空间下的法线对屏幕纹理坐标偏移后,再对屏幕图像进行采样来模拟近似的折射效果。
具体实现:
- 搭建环境:建立一个房间,然后放置了一个立方体和球体,其中球体位于立方体内部,这是为了模拟玻璃对内部物体的折射效果。创建一个着色器付给立方体。
- 在着色器中,我们把Queue设置成Transparent可以保证该物体渲染时,其它所有不透明物体都已经被渲染到屏幕上了,否则就可能无法正确得到“透过玻璃看到的图像”。
- 然后设置RenderType为Opaque则是为了在使用着色器替换(Shader Replacement)时,该物体可以在需要时被正确渲染。这通常发生在我们需要得到摄像机的深度和法线纹理时。
- 通过关键词CrabPass定义了一个抓取屏幕图像的Pass。在这个Pass中我们定义了一个字符串,该字符串内部的名称决定了抓取到的屏幕图像将会被存入哪个纹理中。实际上,我们可以省略声明该字符串,但直接声明纹理名称的方法往往可以得到更高的性能。
- 定义了_RefractionTex和_RefractionTex_TexelSize变量,这对应了在使用GrabPass时指定的纹理名称。_RefractionTex_TexelSize可以让我们得到该像素的纹理大小,例如一个大小为256×512的纹理,它的像素大小为(1/256,1/512)。我们需要在对屏幕图像的采样坐标进行偏移时使用该量。
- 在顶点着色器中通过调用内置的ComputeGrabScreenPos函数来得到对应被抓取的屏幕图像的采样坐标。
- 在片元着色器中我们对法线纹理进行采样,得到切线空间下的法线方向。我们使用该值和_Distortion属性以及_RefractionTex_TexelSize来对屏幕图像的采样所需的坐标进行偏移,模拟折射效果。_Distortion值越大,偏移量越大,玻璃背后的物体看起来变形程度越大。
- 在对屏幕纹理进行采样时使用了齐次除法获取视口坐标下的坐标:(i.scrPos.xy / i.scrPos.w)
- 最后,我们使用_RefractAmount属性对反射和折射颜色进行混合,作为最终的输出颜色。
着色器代码实现见下文。
渲染纹理vs.GrabPass
GrabPass的好处在于实现简单,我们只需在Shader中写几行代码就可以实现抓取屏幕的问题。
-
从效率上来讲,使用渲染纹理的效率往往要好于GrabPass,尤其在移动设备上。使用渲染纹理我们可以自定义渲染纹理的大小,尽管这种方法需要把部分场景再次渲染一遍,但我们可以通过调整摄像机的渲染层来减少二次渲染时的场景大小,或使用其它方法来控制摄像机是否需要开启。
而使用GrabPass获取到的图像分辨率和显示屏幕是一致的,这意味着在一些高分辨率的设备上可能会造成严重的带宽影响。
而且在移动设备上,GrabPass虽然不会重新渲染场景,但它往往需要CPU直接读取后备缓冲(back buffer)中的数据,破坏了CPU和GPU之间的并行性,这是比较耗时的,甚至在一些移动设备上这是不支持的。
命令缓冲(Command Buffers)
在Unity5中,Unity引入了命令缓冲(Command Buffers)来允许我们扩展Unity的渲染流水线。使用命令缓冲我们也可以得到类似抓屏的效果,它可以在不透明物体渲染后把当前的图像复制到一个临时的渲染目标纹理中,然后在那里进行一些额外的操作,例如模糊等,最后把图像传递给需要使用它的物体进行处理和显示。
有关命令缓冲区更多知识,移步:图形命令缓冲区 - Unity 手册
程序纹理
程序纹理(Procedural Texture)指的是那些由计算机生成的图像,我们通常使用一些特定的算法来创建个性化图案或非常真实的自然元素,例如木头、石子等。
使用程序纹理的好处在于我们可以使用各种参数来控制纹理的外观,而这些属性不仅仅是那些颜色属性,甚至可以是完全不同类型的图案属性,这是我们可以得到更加丰富的动画和视觉效果。
实现上图效果,需要任意采用一个带有主纹理参数的着色器,这里采用基础纹理章节中的基础纹理着色器。然后编写C#脚本语言,编写一个纹理赋值给着色器上的纹理使用。该C#脚本具体实现见下文。
Substance Designer
Substance Designer是一种使用节点创建程序材质的软件,该软件创建的程序材质可以直接导入到Unity中使用。(Unity2018及以上或版本靠近2018在Substance Designer制作材质导入后不能识别,需要在Unity商城下载Substance in Unity插件,如果Unity低版本直接支持就不需下载插件)
实现代码
反射
Shader "Unity Shaders Book/Chapter 10/Reflection"
{
Properties
{
_Color ("Color Tint", Color) = (1, 1, 1, 1)
// 用于控制反射颜色
_ReflectColor ("Reflection Color", Color) = (1, 1, 1, 1)
// 用于控制这个材质的反射程度
_ReflectAmount ("Reflect Amount", Range(0, 1)) = 1
// 用于模拟环境映射纹理
_Cubemap ("Reflection 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"
fixed4 _Color;
fixed4 _ReflectColor;
fixed _ReflectAmount;
samplerCUBE _Cubemap;
struct a2v
{
float4 vertex: POSITION;
float3 normal: NORMAL;
};
struct v2f
{
float4 pos: SV_POSITION;
float3 worldPos: TEXCOORD0;
fixed3 worldNormal: TEXCOORD1;
fixed3 worldViewDir: TEXCOORD2;
fixed3 worldRefl: TEXCOORD3;
SHADOW_COORDS(4)
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
// 计算世界空间中的反射方向
// reflect(I, N) 根据入射光线方向I和表面法向量N计算反射向量,仅对三元向量有效
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;
// 漫反射
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));
// 反射光 使用世界空间中的反射方向来访问cubemap
// texCUBE:对立方体纹理进行采样
fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb * _ReflectColor.rgb;
// 计算光线衰减与阴影信息
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
// 混合漫反射的颜色和反射的颜色 lerp:插值
fixed3 color = ambient + lerp(diffuse, reflection, _ReflectAmount) * atten;
return fixed4(color, 1.0);
}
ENDCG
}
}
FallBack "Reflective/VertexLit"
}
折射
Shader "Unity Shaders Book/Chapter 10/Refraction"
{
Properties
{
_Color ("Color Tint", Color) = (1, 1, 1, 1)
// 用于控制折射颜色
_RefractColor ("Refraction Color", Color) = (1, 1, 1, 1)
// 用于控制这个材质的折射程度
_RefractAmount ("Refraction Amount", Range(0, 1)) = 1
// 用于控制折射率
_RefractRatio ("Refraction Ratio", Range(0.1, 1)) = 0.5
// 用于模拟环境映射纹理
_Cubemap ("Refraction 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"
fixed4 _Color;
fixed4 _RefractColor;
float _RefractAmount;
fixed _RefractRatio;
samplerCUBE _Cubemap;
struct a2v
{
float4 vertex: POSITION;
float3 normal: NORMAL;
};
struct v2f
{
float4 pos: SV_POSITION;
float3 worldPos: TEXCOORD0;
fixed3 worldNormal: TEXCOORD1;
fixed3 worldViewDir: TEXCOORD2;
fixed3 worldRefr: TEXCOORD3;
SHADOW_COORDS(4)
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
// 计算世界空间的折射方向(归一化后的矢量,归一化后的表面法线,折射率)
// 根据入射光线方向I,表面法向量N和折射相对系数eta,计算折射向量。如果对给定的eta,I和N之间的角度太大,返回(0,0,0)。
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 * max(0, dot(worldNormal, worldLightDir));
// 使用世界空间中的折射方向来访问cubemap
fixed3 refraction = texCUBE(_Cubemap, i.worldRefr).rgb * _RefractColor.rgb;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
// 混合漫反射的颜色和折射的颜色 lerp:插值
fixed3 color = ambient + lerp(diffuse, refraction, _RefractAmount) * atten;
return fixed4(color, 1.0);
}
ENDCG
}
}
FallBack "Reflective/VertexLit"
}
菲涅尔反射
Shader "Unity Shaders Book/Chapter 10/Fresnel"
{
Properties
{
_Color ("Color Tint", Color) = (1, 1, 1, 1)
// 菲涅尔反射比例
_FresnelScale ("Fresnel Scale", Range(0, 1)) = 0.5
// 立方体纹理
_Cubemap ("Reflection 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"
fixed4 _Color;
fixed _FresnelScale;
samplerCUBE _Cubemap;
struct a2v
{
float4 vertex: POSITION;
float3 normal: NORMAL;
};
struct v2f
{
float4 pos: SV_POSITION;
float3 worldPos: TEXCOORD0;
fixed3 worldNormal: TEXCOORD1;
fixed3 worldViewDir: TEXCOORD2;
fixed3 worldRefl: TEXCOORD3;
SHADOW_COORDS(4)
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
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;
// 菲涅尔反射系数 = 强度系数 + (1 - 强度系数) * (1 - 视角方向 · 表面法线)的5次方
fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldViewDir, worldNormal), 5);
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));
// 最终结果 = 环境光 + 插值(漫反射,反射光,fresnel)* 阴影 saturate:取值范围0-1之间
fixed3 color = ambient + lerp(diffuse, reflection, saturate(fresnel)) * atten;
return fixed4(color, 1.0);
}
ENDCG
}
}
FallBack "Reflective/VertexLit"
}
镜子效果
Shader "Unity Shaders Book/Chapter 10/Mirror"
{
Properties
{
// 纹理属性
_MainTex ("Main Tex", 2D) = "white" { }
}
SubShader
{
Tags { "RenderType" = "Opaque" "Queue" = "Geometry" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
struct a2v
{
float4 vertex: POSITION;
float3 texcoord: TEXCOORD0;
};
struct v2f
{
float4 pos: SV_POSITION;
float2 uv: TEXCOORD0;
};
v2f vert(a2v 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 "Unity Shaders Book/Chapter 10/Glass Refraction"
{
Properties
{
// 材质纹理
_MainTex ("Main Tex", 2D) = "white" { }
// 法线贴图
_BumpMap ("Normal Map", 2D) = "bump" { }
// 环境映射纹理
_Cubemap ("Environment Cubemap", Cube) = "_Skybox" { }
// 用于控制模拟折射时图像的扭曲程度
_Distortion ("Distortion", Range(0, 100)) = 10
// 用于控制折射程度
_RefractAmount ("Refract Amount", Range(0.0, 1.0)) = 1.0
}
SubShader
{
// 队列设置为透明物体时,可以保证在其他不透明物体都渲染后绘制
// 将渲染类型设置为不透明,可以保证可以使物体在需要时被正确渲染
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
// GrabPass:用于抓取屏幕图像的pass,内部字符串决定输出纹理名称
GrabPass
{
"_RefractionTex"
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.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;
float2 texcoord: TEXCOORD0;
};
struct v2f
{
float4 pos: SV_POSITION;
// 屏幕采样坐标
float4 scrPos: TEXCOORD0;
// 切线空间到世界空间的变换矩阵
float4 uv: TEXCOORD1;
float4 TtoW0: TEXCOORD2;
float4 TtoW1: TEXCOORD3;
float4 TtoW2: TEXCOORD4;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// ComputeGrabScreenPos用于获取抓取屏幕的采样坐标
o.scrPos = ComputeGrabScreenPos(o.pos);
o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
// 世界空间下坐标
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 世界空间下法线方向
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
// 世界空间下切线方向
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
// 世界空间下副法线方向 cross:叉积
fixed3 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 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
// 世界空间下视角方向
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// 得到切线空间的法线
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
// 对屏幕图像的坐标进行偏移,模拟折射效果
// 偏移量 = 法线方向 * 扭曲程度 * 屏幕纹理的大小
float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
// 对纹理坐标进行偏移
i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
// 对屏幕纹理进行采样 (i.scrPos.xy / i.scrPos.w):用于使用齐次除法获取视口下的坐标
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)));
// 反射方向 = 基于视角方向相对于法线方向得到
// reflect(I, N) 根据入射光线方向I和表面法向量N计算反射向量,仅对三元向量有效
fixed3 reflDir = reflect(-worldViewDir, bump);
// 贴图纹理
fixed4 texColor = tex2D(_MainTex, i.uv.xy);
// 反射光 = 环境纹理采样 * 贴图纹理
fixed3 reflCol = texCUBE(_Cubemap, reflDir).rgb * texColor.rgb;
// 最终结果 = 反射光 * (1 - 控制折射程度参数) + 屏幕纹理 * 控制折射程度参数
fixed3 finalColor = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount;
return fixed4(finalColor, 1);
}
ENDCG
}
}
FallBack "Diffuse"
}
程序纹理
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
[ExecuteInEditMode]//用于编辑器下运行
public class ProceduralTextureGeneration : MonoBehaviour
{
// 声明材质
public Material material = null;
#region Material properties//材质属性
// 纹理大小
[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();
}
}
#endregion
/// <summary>生成的纹理</summary>
private Texture2D m_generatedTexture = null;
// 初始化
void Start()
{
// 检测材质是否为空
if (material == null)
{
// 获取渲染器
Renderer renderer = gameObject.GetComponent<Renderer>();
if (renderer == null)
{
Debug.LogWarning("找不到渲染器。");
return;
}
// 获取渲染器的材质
material = renderer.sharedMaterial;
}
_UpdateMaterial();
}
/// <summary>
/// 生成纹理
/// </summary>
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;
}
/// <summary>
/// 生成程序纹理
/// </summary>
/// <returns></returns>
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;
}
}