前面的基础纹理包括法线纹理、渐变纹理和遮罩纹理等。这些纹理都属于低纬(一维或二维)纹理。
立方体纹理(Cubemap)实现环境映射
渲染纹理(Render Texture)
程序纹理(Procedure Texture)
一、立方体纹理
立方体纹理(Cubemap)是环境映射的一种实现方法。
环境映射可以模拟物体周围的环境,而使用了环境映射的物体可以看起来像镀了一层金属一样反射出周围的环境
立方体纹理:
一共包含6张图像,这些图像应对了一个立方体的6个面
立方体的每个面表示沿着世界空间下的轴向(上下左右前后)观察所得图像
如何对这种纹理采样?
- 提供一个三维纹理坐标,这个三维纹理坐标表示了我们在世界空间下的一个3D方向。
- 这个方向矢量从立方体的中心出发,当他向外部延申时就会和立方体的6个纹理之一发生相交
- 而采样得到的结果就是由该交点计算而来的
好处:
实现简单快速且得到的效果好。
缺点:
当场景中引入一个新物体、光源,或者物体发生移动时,就需要重新生成立方体纹理
立方体纹理也仅可以反射环境,但不能反射使用了该立方体纹理的物体本身、因为立方体不能模拟多次反射的结果
尽量对凸面体而不要对凹面体使用立方体纹理(凹面体会反射自身)
1.天空盒子
天空盒(Skybox):游戏中用于模拟背景的一种方法。用来模拟天空且时一个盒子。
在场景中使用天空盒,整个场景被包围在一个立方体内。立方体的每个面使用的技术就是立方体纹理映射技术
wrap Mode设置为Clamp
Tint Color:控制该材质的整体颜色
Exposure:调整天空盒亮度
Rotation:调整天空盒沿+y轴方向的选择角度
天空盒会应用于该场景中的所有摄像机,如果希望某些摄像机使用不同的天空盒,可以通过向该摄像机添加Skybox组件来覆盖掉之前的设置
Unity中,天空盒是在所有不透明物体渲染之后渲染的,而其背后使用的网格是一个立方体或者一个细分后的球体
2.创建用于环境映射的立方体纹理
环境映射:用于模拟出金属质感的材质
用于创建环境映射的立方体的三种方法:
1.直接由一些特殊布局的纹理创建
需要提供一张具有特殊布局的纹理(例如类似立方体展开图的交叉布局、全景布局等),把该纹理的Texture Type设置为Cubemap即可,unity会为我们做好剩下的事情。在基于物理的渲染中,通常会使用一张HDR图像来生成高质量的Cubemap
2.手动创建一个Cubemap资源,再把6张图赋给它
在项目资源中创建一个Cubemap,然后把6张纹理拖拽到它的面板中。这是官方推荐使用的一种方法创建立方体纹理,这是因为第一种方法可以对纹理数据进行压缩,而且可以支持边缘修正、光滑反射(glossy reflection)和HDR等功能
以上两种都需要提前准备好立方体纹理的图像,它们得到的立方体纹理往往是被场景中的物体所共用的。在理想状况下,我们希望根据物体在场景中的位置的不同,生成它们各自不同的立方体纹理,这是就需要在Unity中用脚本来创建。
3.由脚本生成
利用Unity提供的Camera.RenderToCubemap函数来实现的。Camera.RenderToCubemap函数可以把从任意位置观察到的场景图像存储到6张图像,从而创建出该位置上对应的立方体纹理
在unity脚本手册中,给出了如何使用Camera.RenderToCubemap函数来创建立方体纹理的代码。
Face size值越大,渲染出来的立方体纹理分辨率越大,效果越好,但需要占用的内存也越大,这可以由面板最下方显示的内存大小得到
准备好立方体纹理后就可以对物体使用环境映射技术。最常见的应用就是反射和折射
3.反射
使用了反射效果的物体通常看起来就像镀了一层金属。
模拟反射效果:
通过入射光线方向和表面法线方向来计算反射方向,再利用反射方向对立方体纹理采样即可。
淦!!!!没保存!!!又要重写....
4.折射
折射的定义:当光线从一种介质斜射入另一种介质种时,传播方向一般会发生改变。当给定入射时,我们可以使用斯涅尔定律计算反射角。
当光从介质1沿着和表面法线夹角为的方向斜射入介质2时,可以应用如下公式计算折射光线与法线的夹角:
其中,和分别为两个介质的折射率。折射率是一项非常重要的物理常数,如空气的折射率是1,玻璃的折射率是1.5。
通常来说,当得到的折射方向后我们就会直接使用它来对立方体进行纹理采样,但这不符合物理规律。
对一个透明物体来说,一种更准确的模拟方法需要计算两次,一次是当光线进入它的内部时,另一次是从它内部射出时。但想要在实时渲染中吗,模拟出第二次折射方向比较复杂,而仅仅模拟第一次得到的效果视觉上看起来也差不多。
refract函数:
第一个参数是入射光线的方向,必须是归一化后的矢量
第二个参数是表面法线,法线方向同样需要归一化后的
第三个参数是入射光线所在介质的折射率和折射光线所在介质的折射率之间的比值
5.菲涅尔反射
在实时渲染中,我们经常会使用菲涅尔反射来根据视角方向控制反射射程。菲涅尔现象即当光线照射到物体表面上时,一部分发生反射,一部分进入物体内部,发生折射或散射。被反射的光和入射光之间存在一定的比率关系。这个比率关系可以通过菲涅尔等式计算。、
如何计算菲涅尔反射?
使用菲涅尔等式,可以在边界处模拟反射光强和折射光强/漫反射光强之间的变化。在车漆,水面等材质的渲染中经常使用。
二、渲染纹理
一个摄像机的输出结果会输出到颜色缓冲中,并显示到我们的屏幕上。现在的GPU允许我们把整个三维场景渲染到一个中间缓冲中,即渲染目标纹理(RTT),而不是传统的帧缓冲或后备缓冲。多重渲染目标(MRT):指GPU允许我们把场景同时渲染到多个渲染目标纹理中,而不再需要为每个渲染目标纹理单独渲染完整的场景。延迟渲染就是使用多重渲染目标的一个应用。
unity为渲染目标纹理定义了一种专门的纹理类型——渲染纹理
使用渲染纹理的方式:
- 在Project目录下创建一个渲染纹理,然后把某个摄像机的渲染目标设置成该渲染纹理,这样一来该摄像机的渲染结果就会实时更新到渲染纹理中,而不会显示在屏幕上。使用这种方法还能选择渲染纹理的分辨率,滤波模式等纹理属性。
- 在屏幕后处理时使用GrabPass命令或OnRenderImage函数来获取当前屏幕图像。Unity会把这个图像放到一张和屏幕分辨率等同的渲染纹理中,下面我们可以在自定义的Pass中把它们当成普通纹理,从而实现各种屏幕特效。
1.镜子效果
- 创建一个摄像机,调整好大小位置等参数以使其对准我们想要渲染的画面
- Create>Render Texture创建一个渲染纹理
- 把该渲染纹理拖到该摄像机的Target Texture(渲染纹理)上
- 最后把该渲染纹理拖到我们创建的材质上
代码部分呢,我们的参数只需要一张纹理,即我们创建渲染纹理
渲染tags设置为
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
在顶点shader中,我们求得顶点位置和uv即可
接着要把从摄像机中拿到的图像沿着X轴分量镜像反转
o.uv.x = 1 - o.uv.x;//翻转x分量的纹理坐标,因为镜像相反
最后在片元shader中,用我们处理后的uv坐标去采样这张渲染纹理就可以得到实时的镜像效果了
Shader "Shader/Mirror"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct appdata
{
float4 vertex : POSITION;
float3 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.uv.x = 1 - o.uv.x;//翻转x分量的纹理坐标,因为镜像相反
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return tex2D(_MainTex, i.uv);
}
ENDCG
}
}
}
又自己尝试了一下,好有趣的效果,好喜欢
可以调整渲染问纹理的分辨率大小,但小的分辨率会使图像模糊不清,可以用更高的分辨率或更多的抗锯齿采样解决,大胆高分辨率会影响带宽和性能,应尽量使用较小的分辨率
2.玻璃效果
在Unity中,我们可以在unity shader中使用一种特殊的Pass来完成获取屏幕图像的目的,这就是GrabPass。
在shader中定义一个GrabPass,unity会把当前屏幕的图像绘制在一张纹理中,以便我们在后续的Pass中访问它。
我们通常会使用GrabPass来实现诸如玻璃等透明材质的模拟,与使用简单透明混合不同,使用GrabPass可以让我们对该物体后面的图像进行更复杂的处理,例如使用法线来模拟折射效果,而不是简单的和原屏幕颜色进行混合
注意:使用GrabPass的时候,要额外小心物体的渲染队列设置。因为GrabPass通常用于渲染透明队列,尽管代码里并不包含混合指令,但我们往往仍然需要把物体的渲染队列设置成透明队列(即“Queue”=“Transparent”)。这样才能保证当渲染该物体时,所有不透明物体都已经被绘制在屏幕上,从而获取正确的屏幕图像
使用GrabPass模拟玻璃效果:
- 使用一种法线纹理修改模型的法线信息
- 然后使用反射方法,通过一个Cubemap来模拟玻璃反射
- 模拟折射时使用Grabpass来获取玻璃后面的屏幕图像
- 使用切线空间下的法线对屏幕纹理坐标偏移
- 再对屏幕图像进行采样来近似模拟折射的效果
Shader "shader/Glass"
{
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//用于控制折射程度
//当_RefractAmount的值为0时,该玻璃只包含反射效果,当_RefractAmount的值为1时。该玻璃只包含折射效果
}
SubShader
{
//在SubShader的标签中将渲染队列设置成Transparent
//尽管后面的Render被设置为Opaque,这两者看似矛盾,但实际上服务于不同的需求
//把Queue设置成Transparent,可以确保该物体渲染时,其他所有不透明物体都已经被渲染到屏幕上
//都在无法正确得到“透过玻璃看到的图像”
//设置RenderType则是为了在使用着色器替换时,该物体可以在需要时被正确渲染
//这通常发生在我们需要的带摄像机的深度和法线纹理时
Tags { "Queue"="Transparent" "RenderType"="Opaque" }
//通过关键词GrabPass定义了一个抓取屏幕图像的Pass
//在这个Pass中我们定义了一个字符串
//该字符串内部的名称决定了抓取得到的屏幕图像会被存入哪个纹理中
//我们也可以省略该字符串,但直接声明纹理名称的方法往往可以得到更高的性能
GrabPass { "_RefractionTex" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 uv: TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float4 scrPos : TEXCOORD0;
float4 uv : TEXCOORD1;
float4 TtoW0 : TEXCOORD2;
float4 TtoW1 : TEXCOORD3;
float4 TtoW2 : TEXCOORD4;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
samplerCUBE _Cubemap;
float _Distortion;
fixed _RefractAmount;
//对应了使用GrabPass时指定的纹理名称
sampler2D _RefractionTex;
//可以让我们得到该纹理的纹素大小,我们需要在对屏幕图像的采样坐标进行偏移时使用该变量
float4 _RefractionTex_TexelSize;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.scrPos = ComputeGrabScreenPos(o.pos);
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.uv, _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;
}
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;
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);
fixed3 reflCol = texCUBE(_Cubemap, reflDir).rgb * texColor.rgb;
fixed3 finalColor = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount;
return fixed4(finalColor, 1);
}
ENDCG
}
}
FallBack "Diffuse"
}
GrabPass支持两种形式:
- 直接使用GrabPass{ },然后在后续的Pass中直接使用_GrabTexture来访问屏幕图像。但是,当场景中有多个物体都使用了这样的形式来抓取屏幕时,这种方法的性能消耗比较大,因为对于每一个使用它的物体,Unity都会为它单独进行一次昂贵的屏幕抓取操作。但这种方法可以让每一个物体得到不同的屏幕图像,这取决于它们的渲染队列及渲染它们时当前的屏幕缓冲中的颜色
- 使用GrabPass { “TextureName” },我们可以在后续的Pass中使用TextureName来访问屏幕。使用这种方法可以抓取屏幕,但Unity只会在每一帧时为第一个使用Texture的纹理的物体执行一次抓取屏幕的操作,而这个纹理同样可以在其他Pass中被访问。这种方法更高效,因为不管场景中有多少物体使用了该命令,每一帧中Unity都会执行一次抓取工作,但这也意味着所有物体都会使用同一张屏幕图像。但大对数情况下这已经足够了
3.渲染纹理 vs.GrabPass
实现难度上:
实现简单,只需要在Shader中写几行代码就可以实现抓取屏幕的目的
而渲染纹理首先要创建一个渲染纹理和一个额外的摄像机,再把该摄像机的Render Target设置为新建的渲染纹理对象,最后把该渲染纹理传递给相应的Shader
效率上:
渲染纹理的效率往往要好于GrabPass,尤其在移动设备上。使用渲染纹理可以自定义渲染纹理的大小,尽管这种方法需要把部分场景再渲染一遍,但我们可以通过调整摄像机的渲染层来减少二次渲染时的场景大小,或使用其他方法来控制摄像机是否需要开启,而使用GrabPass获取到的图像分辨率和显示屏幕是一致的,这意味着在一些高分辨率的设备上可能会造成严重的宽带影响。而移动设备上,GrabPass虽然不会重新渲染场景,但它往往需要CPU直接读取后备缓冲中的数据,破坏了CPU和GPU之间的并行性,这是比较耗时的,甚至在一些移动设备上是不支持的。
命令缓冲(Command Buffers)可以用来扩展Unity的渲染流水线
使用命令缓冲我们也可以得到类似抓屏的效果,它可以在不透明物体渲染后把当前的图像复制到一个临时的渲染目标纹理中,然后在那里进行一些额外的操作。如模糊等,最后把图像传递给它的物体进行处理和显示。
除此之外,命令缓冲还允许我们实现很多特殊的效果
Unity官方手册 图像命令缓冲 一文可以详细了解
三、程序纹理
程序纹理是指那些由计算机生成的图像,我们通常使用一些特定的算法来创建个性化图案或非常真实的自然元素,如木头、石子等。
使用程序纹理的好处:可以使用各种参数来控制程序纹理的外观,而这些属性不仅仅是那些颜色属性,甚至可以是完全不同类型的图案属性,这使得我们可以得到更丰富的动画和视觉效果。
1.Unity中实现简单的程序纹理
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
[ExecuteInEditMode]//使该脚本能够在编辑器模式下运行
public class ProceduralTextureGeneration : MonoBehaviour {
public Material material = null;//声明一个材质,这个材质将使用该脚本中生成的程序纹理
//声明该程序纹理使用的各种参数
//#region和#endregion仅仅为了组织代码,无其他作用
#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
private Texture2D m_generatedTexture = null;//保存生成的程序纹理
// Use this for initialization
void Start () {
if (material == null) {
Renderer renderer = gameObject.GetComponent<Renderer>();
if (renderer == null) {
Debug.LogWarning("Cannot find a renderer.");
return;
}
material = renderer.sharedMaterial;
}
_UpdateMaterial();
}
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;
}
private Texture2D _GenerateProceduralTexture() {
Texture2D proceduralTexture = new Texture2D(textureWidth, textureWidth);
// The interval between circles
float circleInterval = textureWidth / 4.0f;
// The radius of circles
float radius = textureWidth / 10.0f;
// The blur factor
float edgeBlur = 1.0f / blurFactor;
for (int w = 0; w < textureWidth; w++) {
for (int h = 0; h < textureWidth; h++) {
// Initalize the pixel with background color
Color pixel = backgroundColor;
// Draw nine circles one by one
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
// Compute the center of current circle
Vector2 circleCenter = new Vector2(circleInterval * (i + 1), circleInterval * (j + 1));
// Compute the distance between the pixel and the center
float dist = Vector2.Distance(new Vector2(w, h), circleCenter) - radius;
// Blur the edge of the circle
Color color = _MixColor(circleColor, new Color(pixel.r, pixel.g, pixel.b, 0.0f), Mathf.SmoothStep(0f, 1.0f, dist * edgeBlur));
// Mix the current color with the previous color
pixel = _MixColor(pixel, color, color.a);
}
}
proceduralTexture.SetPixel(w, h, pixel);
}
}
proceduralTexture.Apply();
return proceduralTexture;
}
}
这个好难,代码和效果放这,日后回来细细研究
2.Unity的程序材质
有一类专门使用程序纹理的材质,叫做程序材质。
这类材质和我们之前使用的那些材质本质上相同,不同的是使用的纹理不是普通的纹理。而是程序纹理
注意:程序材质和它使用的程序纹理并不是在Unity中创建的,而是使用了一个名为Substance Designer的软件在Unity外部生成的。
Substance Designer是一个非常出色的纹理生成工具很多3A游戏项目都是使用了它生成的材质
这些材质都是以.sbsar为后缀的,可以直接把这些材质像其他资源一样拖入Unity项目中
导入这些文件到Unity后,Unity会生成一个程序纹理资源。程序纹理资源可以包含一个或多个程序材质
程序纹理的强大之处在于多变性,可以通过调整程序纹理的属性来控制纹理的外观,甚至生成看似完全不同的纹理,自由性很高