目录
10.1 立方体纹理
前言
立方体纹理(Cubemap)是环境映射(Environment Mappi)的一种方式。环境映射可以模拟物体周围的环境,像是镀了层金属一样反射周围的环境。
立方体纹理包含六张图像,对应正方体六个面。对立方体纹理的采样需要一个三维的纹理坐标,表示在世界空间下的一个3D方向矢量。如下图,这个方向矢量从立方体中心开始向外部延伸会与立方体的六个纹理之一发生相交,采样得到的结果就是由这个焦点计算出来的。
立方体纹理的好处在于,实现简单快速,效果也比较好。缺点是场景引入新的物体、光源,或者物体发生移动时,就需要重新生成立方体纹理。立方体纹理常用于天空盒子(Skybox)以及环境映射。
10.1.1 天空盒子
天空盒子是游戏中用于模拟背景的一种方法,一般是模拟天空,也可以是室内。相当于整个场景都被包围在一个立方体内部。
Unity里面实现天空盒子很简单,只需要创建一个Skybox材质,再赋给该场景的相关设置即可。
创建Skybox材质:先创建一个新材质,然后在材质的Shader下拉菜单中选择Unity自带的Skybox/6 Sided(如下图),这个材质需要六张纹理图
然后我们给六张纹理赋值(要注意纹理的正确位置),为了天空盒子正常渲染,我们需要把这六张纹理的Wrap Mode设置为Clamp。然后得到下图的面板,材质中还有三个属性:Tint Color,Exposure,Rotation,分别用于控制材质的整体颜色、调整天空盒子的亮度、调整天空盒子沿+y轴方向的旋转角度。
然后我们直接把这个材质拖到场景中,我们也可以在Window->Rendering->Lighting->Environment里面设置天空盒。为了摄像机正常显示我们还要把Camera的Clear Flags设置为Skybox。最终可以得到下面的效果。
如果我们想要不同的摄像机用不同的Skybox,我们给摄像机添加Skybox组件。
10.1.2 创建用于环境映射的立方体纹理
在前言里面提到过,使用环境映射可以模拟出家属质感的材质。
创建用于环境映射的立方体纹理有三种方法:
- 直接由一些特殊布局的纹理创建。需要提供一张特殊布局的纹理,类似于立方体展开图的交叉布局、全景布局等。(如下图)这种方法支持对纹理数据进行压缩,支持边缘修正、光源反射和HDR功能。
- 手动创建一个Cubemap资源,再把六张图赋给它。
- 由脚本生成。前两中方法都要提前准备好立方体纹理贴图,它们得到的立方体纹理往往是被场景中的物体所共用的。但在理想情况下,我们希望工具在场景的不同位置,能有不同的立方体纹理,这时候就可以在Unity中使用脚本来创建。脚本代码如下。这个代码需要添加菜单栏条目,所以需要把它放在Editor文件夹下才能正确执行。
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;
// .RenderToCubemap函数把从当前位置观察到的图像渲染到用户指定的立方体纹理cubemap中
go.GetComponent<Camera>().RenderToCubemap(cubemap);
// 完成后销毁相机
DestroyImmediate( go );
}
[MenuItem("GameObject/Render into Cubemap")]
static void RenderCubemap () {
ScriptableWizard.DisplayWizard<RenderCubemapWizard>(
"Render cubemap", "Render!");
}
}
下面讲讲第三种方式如何使用这个代码。
1、在场景种创建一个空物体GameObject,这个空物体的位置,就是代码中要改变相机的位置,相当于得到这个位置的用于环境映射的立方体纹理。
2、在Project窗口中创建一个用于存储的立方体纹理(Create->Legacy->Cubemap),命名为Cubemap_0
3、在Unity菜单栏中选择GameObject->Render into Cubemap(这就是前面这串代码实现的工具)。然后把前两步创建的物品拖进去,点击下面的按钮Render!
做完上面的步骤之后,我们就把从指定位置观察到的世界空间下的6张图像渲染到Cubemap_0中了,如下图。Cubemap_0有一个参数Face size,这个值越大,渲染出来的立方体纹理分辨率越大,效果更好,但需要占用的内存也更大。
准备好了立方体纹理之后,我们就可以对物体使用环境映射技术。环境映射技术最常见的就是反射和折射。
10.1.3 反射
使用反射效果会让物体看起来就像是金属一样。模拟反射效果很简单,只要通过入射光线的方向和表面法线方向来计算反射方向,再利用反射方向对立方体纹理采样即可
Shader "MyShader/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;
///UnityWorldSpaceViewDir(float4 v) 输入一个世界空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向
//所以得到的其实就是入射光线的反方向。
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
//用了CG的reflect函数利用入射光线方向和法线方向计算了顶点的反射方向。
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));
//通过texCUBE函数,使用反射方向对立方体纹理采样
fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb * _ReflectColor.rgb;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
// 混合漫反射颜色和反射颜色,并与环境光相加后返回
fixed3 color = ambient + lerp(diffuse, reflection, _ReflectAmount) * atten;
return fixed4(color, 1.0);
}
ENDCG
}
}
FallBack "Reflective/VertexLit"
}
最后要记得给材质的Reflection Cubemap属性赋值,也就是我们前面制作的Cubemap_0。最后可以得到下面的效果:
10.1.4 折射
折射:当光线从一种介质(如空气)斜摄入另一种介质(如玻璃)时,传播方向一般会发生变化。我们可以根据斯涅尔定律来计算折射角,和分别是两个介质的折射率。
然后我们一般就会使用折射方向对立方体纹理进行采样。先现实世界中一般要计算两次,一次是进入内部,一次是从内部出来。但在实时渲染中实现起来比较复杂,所以我们只计算一次,事实上这样也能得到不错的效果。
Shader "MyShader/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;
//UnityWorldSpaceViewDir(float4 v) 输入一个世界空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
//计算折射方向
//使用了refract函数,三个参数分别是,入射光线方向、表面法线、透射比
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));
//使用折射法线对立方体纹理进行采样
fixed3 refraction = texCUBE(_Cubemap, i.worldRefr).rgb * _RefractColor.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"
}
同样对Reflection Cubemap属性赋值后得到下面结果:
10.1.5 菲涅尔反射
事实上一个物体经常不会只有反射或只有折射。光线照到物体表面时候,一部分发生反射,一部分会发生折射和散射。反射的光和入射的光存在一定的比率关系。菲涅尔反射(F热算呢了reflection)就是根据视角方向来控制反射程度。
计算菲涅尔函数就需要使用菲涅尔等式,在实时渲染中我们通常会使用一些近似公式来计算,如Schlick菲涅尔近似等式:
F0是一个反射系数,控制菲涅尔反射强度,v是视角方向,n是表面法线。
Empricial菲涅尔近似等式:
其中bias、scale、power是控制项。
下面我们使用Schlick菲涅尔近似等式来模型菲涅尔反射。
Shader "MyShader/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" {}//反射使用的Cubemap
}
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;
//利用公式计算菲涅尔反射
fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldViewDir, worldNormal), 5);
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));
//saturate函数取值到[0,1]
//把结果值fresnel混合漫反射光照和反射光照
fixed3 color = ambient + lerp(diffuse, reflection, saturate(fresnel)) * atten;
return fixed4(color, 1.0);
}
ENDCG
}
}
FallBack "Reflective/VertexLit"
}
下面展示了 Fresnel Scale为0时,是一个具有边缘光照效果的漫反射物体,如下图
Fresnel Scale为0.5时:
如果Fresnel Scale为1的话就表示物体将完全反射Cubemap中的图像。