- 之前学习的基础纹理------法线纹理、渐变纹理、漫反射纹理和遮罩纹理等,都属于低维纹理
10.1 立方体纹理
-
在图形学中,
立方体纹理(Cubemap)
是环境映射(Environment Mapping)
的一种实现方式 -
环境映射可以模拟物体周围的环境 ,使用了环境映射的物体可以看起来像镀了层金属一样反射出周围的环境
-
立方体纹理一共包含 6 张图像,对应 6 个面。每个面表示沿着世界空间下的轴向(上、下、左、右、前、后)观察所得的图像
-
采样方法
- 给定一个 3D 方向
- 将方向尾部与立方体中心重合(即从中心出发),头部向外延伸直到与立方体 6 个纹理之一相交
- 由该交点计算出采样结果
-
优点:
- 实现简单快速
- 效果比较好
-
缺点:
- 场景中引入新元素时,需要重新生成纹理
- 仅可反射环境,不可进行多次反射,如两金属球互相反射的情况(Unity5 引入的全局光照系统允许实现这种效果)
-
应尽量对凸面体而不要对凹面体使用立方体纹理
10.1.1 天空盒子
10.1.2 创建用于环境映射的立方体纹理
-
用来模拟金属质感的材质
-
创建方法有三种
- 直接由一些特殊布局的纹理创建 (Unity5 推荐)
- 纹理类似立方体展开图的交叉布局、全景布局等
- 把纹理的 Texture Type 设置为 Cubemap
- 手动创建一个 Cubemap 资源,再把 6 张图赋给它(Unity5 之前的方法)
- 由脚本生成
-
可根据物体在场景中的位置,生成各自的立方体纹理
-
利用 Unity 提供的 Camera.RenderToCubemap 来实现
- 函数可把从任意位置观察到的场景图像存储到 6 张图像中,从而创建出立方体纹理
-
步骤:
-
将辅助工具脚本放到 Project/Editor 下 (源码)
-
在场景中创建空的 GameObject 对象,会使用该对象的位置来渲染立方体纹理
-
新建一个用于存储的立方体纹理(Create - Rendering - Legacy Cubemap)。为了让脚本可将图像渲染到该立方体纹理中,需要在它的属性面板中勾选 Readable 选项
-
Unity 菜单(或右键菜单) - Render into Cubemap,打开对话窗口。将第 2 步创建的 GameObject 和第 3 步创建的立方体纹理分别拖入 Render From Position 和 Cubemap 中
-
点击 Render! 按钮
-
-
结果
-
- 直接由一些特殊布局的纹理创建 (Unity5 推荐)
-
准备好了立方体纹理后,就可对物体使用
环境映射技术
,常见的应用就是反射
和折射
10.1.3 反射
- 要模拟反射效果很简单,只需要通过入射光线和表面法线的方向来计算反射方向,再对立方体纹理采样即可
实现步骤:
- 新建场景,天空盒使用上面教堂那个
- 在场景中拖曳一个 Teapot 模型 (链接)
- 使用 10.1.2 的方法以 Teapot 为中心再次生成 Cubemap
- 新建材质和 Shader,Shader 赋给材质,材质赋给 Teapot 模型
- Shader 代码: (源码)
-
属性声明
jsProperties { _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" {} }
- _ReflectColor:控制反射颜色
- _ReflectAmount:控制材质的反射程度
- _Cubemap:模拟反射的环境映射纹理
-
属性对应变量声明
jsfixed4 _Color; fixed4 _ReflectColor; fixed _ReflectAmount; samplerCUBE _Cubemap;
- samplerCUBE:立方体纹理采样器
-
顶点着色器
jsv2f 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; }
- 使用 reflect 函数,通过
视角方向
和法线方向
来得出反射方向
(实际是入射方向的反方向)
- 使用 reflect 函数,通过
-
片元着色器
jsfixed4 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 * (dot(worldNormal, worldLightDir) * 0.5 + 0.5); 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); }
- 使用 texCUBE 来对立方体纹理进行采样(参数没必要进行归一化)
- 使用 _ReflectAmount 来混合漫反射颜色和反射颜色,和环境光相加后返回
- 出于性能的考虑,没有在片元着色器而是在顶点着色器中计算反射方向
-
- 将第 3 步生成的 Cubemap 拖曳到材质的 Reflection Cubemap 中
效果

10.1.4 折射
-
折射的物理原理比反射复杂一些
-
折射的物理定义:当光线从一种介质(例如空气)斜射入另一种介质(例如玻璃)时,传播方向一般会发生改变
-
当给定入射角时,可使用斯涅尔定律(Snell's Law)来计算反射角,公式:
<math xmlns="http://www.w3.org/1998/Math/MathML"> n 1 s i n θ 1 = n 2 s i n θ 2 n_1sin\theta_1=n_2sin\theta_2 </math>n1sinθ1=n2sinθ2
- <math xmlns="http://www.w3.org/1998/Math/MathML"> n 1 n_1 </math>n1 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> n 2 n_2 </math>n2 分别是两个介质的折射率
-
对一个透明物体来说,更准确的模拟方法需要计算两次折射 ,但在实时渲染中执行是比较复杂的,且只模拟一次得到的效果从视觉上看起来"也还 OK"。所以实时渲染中通常只模拟一次折射
实现步骤:
- 复制上一节的场景
- 创建新材质和 Shader,Shader 赋给材质,材质赋给 Teapot 模型
- Shader 代码:(源码)
-
属性声明
jsProperties { _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)) = 0.5 _Cubemap ("Refraction Cubemap", Cube) = "_Skybox" {} }
- _RefractRatio:介质折射率
-
顶点着色器
jsv2f 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.worldRefr = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio); TRANSFER_SHADOW(o); return o; }
- 使用 refract 函数来计算折射方向
- 第一个参数是入射光线的方向(须归一化)
- 第二个参数是表面法线方向(须归一化)
- 第三个参数是入射光线所在介质的折射率,和折射光线所在介质的折射率之比
- 使用 refract 函数来计算折射方向
-
片元着色器
jsfixed4 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.rgb; fixed3 diffuse = _LightColor0.rgb * _Color.rgb * (dot(worldNormal, worldLightDir) * 0.5 + 0.5); 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); }
- 逻辑与上面"反射"的差不多,用折射方向对立方体纹理进行采样
-
- 在材质面板中将立方体纹理拖曳赋值到 Refraction Cubemap 中
效果:

10.1.5 菲涅尔反射
菲涅尔反射描述了一种光学现象,即当光线照射到物体表面上时,一部分发生反射,一部分进入物体内部发生折射或散射。
-
在实时渲染中,常会使用菲涅尔反射(Fresnel reflection)来根据视角方向控制反射程度
-
被反射的光和入射的光之间存在一定的比率关系,这个比率关系可通过菲涅尔等式进行计算
-
例子,当站在湖边,直接低头看能看清水下的景象,但抬头看湖的远处,则无法看清水下,只能看到水面反射的环境
-
在实时渲染中,通常会使用一些近似公式来计算,Schlick 菲涅尔近似等式就是其中比较著名的:
<math xmlns="http://www.w3.org/1998/Math/MathML"> F s c h l i c k ( v , n ) = F 0 + ( 1 − F 0 ) ( 1 − v ⋅ n ) 5 F_{schlick}(v,n)=F_0+(1-F_0)(1-v\cdot n)^5 </math>Fschlick(v,n)=F0+(1−F0)(1−v⋅n)5
- <math xmlns="http://www.w3.org/1998/Math/MathML"> F 0 F_0 </math>F0 是一个反射系统
- v 是视角方向
- n 是表面法线
-
另一个应用比较广泛的等式是 Empricial 菲涅尔近似等式:
<math xmlns="http://www.w3.org/1998/Math/MathML"> F E m p r i c i a l ( v , n ) = m a x ( 0 , m i n ( 1 , b i a s + s c a l e × ( 1 − v ⋅ n ) p o w e r ) ) F_{Empricial}(v,n)=max(0,min(1,bias+scale\times (1-v\cdot n)^{power})) </math>FEmpricial(v,n)=max(0,min(1,bias+scale×(1−v⋅n)power))
- bias, scale 和 power 是控制项
实现步骤:
-
(前面步骤同上)
-
Shader 代码:(源码)
-
Properties
jsProperties { _Color ("Color Tint", Color) = (1, 1, 1, 1) _FresnelScale ("Fresnel Scale", Range(0, 1)) = 0.5 _Cubemap ("Reflection Cubemap", Cube) = "_Skybox" {} }
-
(顶点着色器代码同上)
-
片元着色器
jsfixed4 frag (v2f i) : SV_Target { fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = 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; // 使用 Schlick 菲涅尔近似等式计算 fresnel 变量 fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldViewDir, worldNormal), 5); fixed3 diffuse = _LightColor0.rgb * _Color.rgb * (dot(worldNormal, worldLightDir) * 0.5 + 0.5); fixed3 color = ambient + lerp(diffuse, reflection, saturate(fresnel)) * atten; return fixed4(color, 1.0); }
-
-
在材质面板中将立方体纹理拖曳赋值到 Refraction Cubemap 中
效果:

-
将 Fresnel Scale 调到 1 时,物体将完全反射(效果同 10.1.3)
-
将 Fresnel Scale 调到 0 时,物体的边缘具有光照效果
10.2 渲染纹理
- 现代 GPU 允许把整个三维场景渲染到一个中间缓冲中------
渲染目标纹理(Render Target Texture,RTT)
,而不是传统的帧缓冲
或后备缓冲
- 之前提到过
多重渲染目标(Multiple Render Target,MRT)
,它指的是 GPU 允许将场景同时渲染到多个渲染目标纹理中,而不再需要为每个渲染目标纹理单独渲染完整的场景。延迟渲染
就是使用多重渲染目标的一个应用 - Unity 为渲染目标纹理定义了一种专门的纹理类型 ------
渲染纹理(Render Texture)
10.2.1 镜子效果
关键步骤:
-
新建材质 MirrorMat 和 Shader,将 Shader 赋给材质
-
创建一个四边形(Quad)将它作为镜子,将 MirrorMat 材质赋给它
-
在 Project 视图下创建一个渲染纹理(Create - Rendering - Render Texture),命名为 MirrorTexture。调整属性
-
创建一个摄像机,移动到镜子的位置,调整好视角。将渲染纹理 MirrorTexture 拖曳赋到 Target Texture 上
-
Shader 代码:(源码)
10.2.2 玻璃效果
- 在 Unity Shader 中,可使用一种特殊的 Pass 来获取屏幕图像------
GrabPass
- 当我们在 Shader 中定义了一个 GrabPass 后,Unity 会把当前屏幕的图像绘制在一张纹理中,以便在后续的 Pass 中访问它
- 通常使用 GrabPass 来实现诸如玻璃等透明材质的模拟
- 可让我们对物体后面的图像进行更复杂的处理,如使用法线来模拟折射效果
- 在使用 GrabPass 时,需要额外小心物体的渲染队列设置。尽管代码里并不包含混合指令,但往往仍需要把渲染队列设置成透明队列("Queue"="Transparent"),这样可保证当渲染该物体时,其他所有的不透明物体都已经被绘制在屏幕上,从而得到正确的效果
实现步骤:
-
新建材质和 Shader,将 Shader 赋给材质
-
创建一个六面空间,在空间内创建一个立方体,将第 1 步创建的材质赋给它。在立方体内创建一个球体
-
使用立方体纹理渲染工具,以第 2 步创建的立方体为中心渲染出立方体纹理 Glass_Cubemap
-
Shader 代码:(源码)
-
Properties
jsProperties { _MainTex ("Texture", 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 }
- _MainTex:玻璃材质纹理
- _BumpMap:玻璃法线纹理
- _Cubemap:模拟反射的环境纹理
- _Distortion:控制模拟折射时图像的扭曲程度
- _RefractAmount:控制折射程度(0 完全反射 -> 1 完全折射)
-
定义相应的渲染队列,并使用 GrabPass 来获取屏幕图像
jsSubShader { Tags { "Queue"="Transparent" "RenderType"="Opaque" } GrabPass { "_RefractionTex" }
- 首先将渲染队列 Queue = Transparent,以确保在物体渲染前,其他不透明物体已被渲染
- RenderType=Opaque,为了在使用着色器替换(Shader Replacement)时,该物体可在需要时被正确渲染(通常发生在需要得到摄像机的深度和法线纹理时)
- 通过关键词 GrabPass 定义一个抓取屏幕图像的 Pass,其中的字符串(_RefractionTex)决定了抓取到的图像会被存入哪个名称的纹理中
-
定义各属性对应的变量
jssampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float4 _BumpMap_ST; samplerCUBE _Cubemap; float _Distortion; fixed _RefractAmount; sampler2D _RefractionTex; float4 _RefractionTex_TexelSize;
- _RefractionTex:对应上面使用 GrabPass 时指定的纹理名称
- _RefractionTex_TexelSize:该纹理的纹素大小 。如一个大小为 256*512 的纹理,纹素大小为(1/256, 1/512)。后面在对屏幕图像采样坐标进行偏移时使用该变量
-
顶点着色器
jsv2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); 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); 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; }
- computeGrabScreenPos:内置函数,可得到参数位置对应的被抓取的屏幕图像的采样坐标
- 计算 _MainTex 和 _BumpMap 的采样坐标,将它们分别存储在一个 float4 类型变量的 xy 和 zw 分量中
- 由于需要在片元着色器中将法线从切线空间变换到世界空间下,以便对 Cubemap 进行采样,所以在这里要计算出变换矩阵,并把矩阵的每一行分别存储在 TtoW0、TtoW1、TtoW2 的 xyz 分量中
-
片元着色器
jsfixed4 frag (v2f i) : SV_Target { // -------- 通过 TtoW0 等变量的 w 分量得到世界坐标,并用该值得到对应的视角方向 -------- 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)); // -------- 计算切线空间中的偏移,模拟折射效果 -------- // _Distortion 值越大,偏移量越大,玻璃背后物体看起来形变越大 // 使用切线空间下的法线来进行偏移 float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy; // 对 scrPos 透视除法得到真正的屏幕坐标 i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy; // 使用该坐标对抓取的屏幕图像 _RefractionTex 进行采样,得到模拟的折射颜色 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))); // 计算得到反射方向 fixed3 reflDir = reflect(-worldViewDir, bump); // 采样得到主纹理颜色 fixed4 texColor = tex2D(_MainTex, i.uv.xy); // 使用反射方向对 Cubemap 进行采样,并将结果与主纹理颜色相乘得到反射颜色 fixed3 reflCol = texCUBE(_Cubemap, reflDir).rgb * texColor.rgb; // 使用 _RefractAmount 对反射和折射颜色进行混合,得到最终输出颜色 fixed3 finalCol = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount; return fixed4(finalCol, 1.0); }
-
-
将 Glass_Diffuse.jpg 和 Glass_Normal.jpg 赋给材质的 Main Tex 和 Normal Map
-
把 Glass_Cubemap 赋给 Environment Cubemap
-
调整 Refract Amount 得到玻璃效果
效果:

- GrabPass 支持两种形式
- 直接使用 GrabPass {},后续的 Pass 中使用 _GrabTexture 来访问屏幕图像
- 若场景中有多个物体使用了这样的形式来抓取屏幕时,对性能消耗较大,但可让每个物体得到不同的屏幕图像
- 使用 GrabPass { "TextureName" }
- 只会在每一帧时为第一个使用名为 TextureName 的纹理的物体执行一次抓取屏幕操作,这个纹理同样可被其他 Pass 访问
- 更高效,不管场景中有多少物体使用了该命令,每一帧中只会执行一次,但所有物体都会使用同一张屏幕图像
- 直接使用 GrabPass {},后续的 Pass 中使用 _GrabTexture 来访问屏幕图像
10.2.3 渲染纹理 vs. GrabPass
标题 | 渲染纹理 | GrabPass |
---|---|---|
易用性 | 复杂 | 简单 |
效率 | 较高 | 较低 |
能否自定义纹理大小 | 是 | 否 |
- Unity5 中引入了
命令缓冲(Command Buffers)
来允许我们扩展 Unity 的渲染流水线- 可得到类似抓屏的效果,把当前图像复制到一个临时的渲染目标纹理中
10.3 程序纹理
程序纹理(Procedural Texture)
指的是那些由计算机生成的图像,我们通常使用一些特定的算法来创建个性化图案或非常 真实的自然元素(木头、石子等)
10.3.1 在 Unity 中实现简单的程序纹理
关键步骤:
- 不用为物体材质中的 Main Tex 赋任何纹理,我们要用脚本来创建此纹理
- 创建脚本,并将它拖曳到物体上以添加脚本组件
- 脚本
-
让脚本能在编辑器模式下运行,在类的开头添加
cs[ExecuteInEditMode]
-
声明一个材质,然后在各阶段处理
cspublic Material material = null;
-
Start 函数:尝试从物体上得到相应的材质 ,然后为其生成程序纹理
csvoid Start() { if (material == null) { Renderer renderer = gameObject.GetComponent<Renderer>(); if (!renderer) { Debug.LogWarning("Cannot find a renderer."); return; } material = renderer.sharedMaterial; } UpdateMaterial(); }
-
UpdateMaterial 函数:调用纹理生成函数来生成一张程序纹理 ,然后使用 SetTexture 函数将纹理赋给材质 (需要有一个名为 _MainTex 的纹理属性)
csprivate void UpdateMaterial() { if (material != null) { mGenTexture = GenProceduralTexture(); material.SetTexture("_MainTex", mGenTexture); } }
-
(纹理生成函数,略)
-
-
10.3.2 Unity 的程序材质
- 在 Unity 中有一类专门使用程序纹理 的材质,叫
程序材质
- 注意,程序材质与它使用的程序纹理并不是在 Unity 中创建的 ,而是使用了一个名为 Substance Designer 的第三方软件生成的
- Substance Designer 是一个非常出色的纹理生成工具,很多 3A 游戏都使用了它生成的材质(以 .sbsar 为后缀)
- 程序纹理的强大之处在于它的多变性,可通过调整程序纹理的属性来控制纹理外观