1.1.1 高级纹理
立方体纹理(Cubemap)是环境映射(Environment Mapping)的一种实现方法。环境映射可以模拟物体周围的环境,而使用了环境映射的物体可以看起来像镀了层金属一样反射出周围的环境。
立方体纹理一共包含了6张图像,这些图像对应了一个立方体的6个面。立方体的每个面表示沿着世界空间下的轴向(上下左右前后)观察所得的图像。而对立方体纹理采样开发者需要提供一个3D方向。这个方向矢量从立方体的中心出发,当它向外部延伸时就会和立方体的6个纹理之一发生相交,而采样得到的结果就是由该交点计算而来的。
使用立方体纹理的好处在于,它的实现简单快速,而且得到的效果也比较好。而它的缺点为,例如当场景中引入了新的物体、光源或者物体发生移动时,开发者需要重新生成立方体纹理。除此之外,立方体纹理也仅可以反射环境,但不能反射使用了该立方体纹理的物体本身。因为立方体纹理不能模拟多次反射的结果。因此应该尽量对于凸面体而不是凹面体使用立方体纹理(因为凹面体回反射自身)。
天空盒子:
天空盒子(sky box)是游戏中用于模拟背景的一种方法。它用来模拟天空(尽管仍可以用它模拟室内等背景),它是一个盒子。当我们在背景中使用了天空盒子时,整个场景被包围在一个立方体内。这个立方体每个面使用的技术就是立方体纹理映射技术。
通过新建一个material并把shader的选择项改为skybox/6sided。
图 1.65 示意图
在Unity中,天空盒子是在所有不透明物体之后渲染的,而其背后使用的网格式一个立方体或一个细分后的球体。
创建用于环境映射的立方体纹理:
除了天空盒子,立方体纹理最常见的用处是用于环境映射。通过这种办法,可以模拟出金属质感的材质。
在Unity5中,创建用于环境映射的立方体纹理的办法有三种:
[1]直接由一些特殊布局的纹理创建(提供一张具有特殊布局的纹理,例如类似立方体展开图的交叉布局、全景布局等。然后把该纹理的Texture Type设置为CubeMap。在基于物理的渲染中,通常使用一张HDR图像来生成高质量的Cubemap)
[2]手动创建一个Cubemap资源(在项目资源中创建一个Cubemap,然后将6张纹理拖拽到他的面板中。在Unity5中,官方推荐使用第一种方法创建立方体纹理,因为第一种方法可以对纹理数据进行压缩,而且支持边缘修正、光滑反射(glossy reflection)和HDR等功能)
(前两种方法都需要提前准备好立方体纹理的图像,它们得到的立方体纹理常常是背场景中的物体所共用)(而通常开发者希望根据物体在场景中位置的不同,生成它们各自不同的立方体纹理)
[3]由脚本生成(使用脚本,利用Unity提供的Camera.RenderToCubemap函数来实现。该函数可以从任意位置观察到的场景图像存储到6张图像中,从而创建出该位置上对应的立方体纹理)
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 () { // create temporary camera for rendering GameObject go = new GameObject( "CubemapCamera"); go.AddComponent<Camera>(); // place it on the object go.transform.position = renderFromPosition.position; // render into cubemap go.GetComponent<Camera>().RenderToCubemap(cubemap); // destroy temporary camera DestroyImmediate( go ); } [MenuItem("GameObject/Render into Cubemap")] static void RenderCubemap () { ScriptableWizard.DisplayWizard<RenderCubemapWizard>( "Render cubemap", "Render!"); } }
(准备完上述代码后,使用上面相同的场景后创建一个空的GameObject对象,使用该GameObject的位置信息来渲染立方体纹理。然后新建一个存储的立方体纹理,project试图 -> Creat -> Legacy -> Cubemap来创建,同时还需要在其面板上勾选Readable选项。然后在Unity菜单栏选择GameObject -> Render into Cubemap,打开在脚本中实现的用于渲染立方体纹理的窗口,并把第一步的GameObject和第二步的Cubemap_0若给Render From Position和Cubemap选项。然后点击窗口中的Render!按钮可观察到现象。而Face size选项越大,渲染出来的立方体纹理分辨率越大,效果越好,占用的内存更大。)
图 1.66 示意图
反射:
想要模拟反射效果只需要通过入射光线的方向和表面法线方向来计算反射方向,再利用反射方向对立方体纹理采样即可。
图 1.67 示意图
实践步骤:
[1]新建场景,完成上一节中设置的东西,得到对应的cubemap
[2]创建材质,并把下面shader代码给新建的shader后给该物体,然后在reflection cubemap添加对应的cubemap
代码如下:
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); // Compute the reflect dir in world space o.worldRefl = reflect(-o.worldViewDir, o.worldNormal); //计算该顶点的反射方向,通过reflect函数实现,同时摄像机反射到摄像机中的光线方向可以通过光路可逆的原则来反向求得。 //计算视角方向关于顶点法线的反射方向来求得入射光线的方向 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)); // Use the reflect dir in world space to access the cubemap fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb * _ReflectColor.rgb; //利用反射方向来对立方体纹理采样,而没有对worldRefl归一化是因为采样的参数仅仅是作为方向变量传递给texCUBE,因此没有必要归一化 UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); // Mix the diffuse color with the reflected color fixed3 color = ambient + lerp(diffuse, reflection, _ReflectAmount) * atten; //ReflectAmount进行混合漫反射颜色和反射颜色,并和环境光照相加后返回 return fixed4(color, 1.0); } ENDCG } } FallBack "Reflective/VertexLit" }
1.1.2 折射
折射公式:
图 1.68 折射
步骤:
[1]与上步骤差不多
[2]删除shader文件原有代码,附上下代码:
Shader "Unity Shaders Book/Chapter 10/Refraction" { Properties { //声明4个属性 _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); // Compute the refract dir in world space o.worldRefr = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio); //使用refract函数计算折射方向,第一个参数为入射光线的方向,其必须是归一化的矢量; //第二个参数时表面法线,法线方向同样需要归一化后的;第三个参数时入射光线所在介质的折射率和折射光线所在介质的折射率之间的壁纸 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)); // Use the refract dir in world space to access the cubemap fixed3 refraction = texCUBE(_Cubemap, i.worldRefr).rgb * _RefractColor.rgb; UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); // Mix the diffuse color with the refract color fixed3 color = ambient + lerp(diffuse, refraction, _RefractAmount) * atten; return fixed4(color, 1.0); } ENDCG } } FallBack "Reflective/VertexLit" }
图 1.69 示意图
菲尼尔反射:
其近似公式为:
是一个反射系数,用于控制反射的强度, 是视角方向,是表面法线
图 1.70 示意图
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; fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldViewDir, worldNormal), 5); //菲尼尔近似等式 fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir)); fixed3 color = ambient + lerp(diffuse, reflection, saturate(fresnel)) * atten; return fixed4(color, 1.0); } ENDCG } } FallBack "Reflective/VertexLit" }
1.1.3 渲染纹理
现代的GPU允许把整个三维场景渲染到一个中间缓冲中,即渲染目标纹理,而不是传统的帧缓冲或后备缓冲。与之相关的是多重渲染目标,该技术是指GPU允许把场景同时渲染到多个渲染目标纹理中,而不再需要为每个渲染目标纹理单独渲染完整的场景。延迟渲染是使用多重渲染目标的一个应用。
Unity为渲染目标纹理定义了一种专门的纹理类型:渲染纹理(Render Texture)。该摄像机的渲染结果将会实时更新到渲染纹理中,同时还可以选择渲染的分辨率、滤波等纹理属性。另一种则是屏幕后处理时使用GrabPass命令或OnRenderImage函数来获取当前屏幕图像。
镜子效果:
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 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; // Mirror needs to filp x o.uv.x = 1 - o.uv.x; //反转x分量的纹理坐标,镜子的图像都是左右相反 return o; } fixed4 frag(v2f i) : SV_Target { return tex2D(_MainTex, i.uv); //对纹理进行采样和输出 } ENDCG } } FallBack Off }
图 1.71 示意图
玻璃效果:
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 //控制折射程度,若为0则该玻璃只包含反射效果,若为1则只包含折射效果 } SubShader { // We must be transparent, so other objects are drawn before this one. Tags { "Queue"="Transparent" "RenderType"="Opaque" } // This pass grabs the screen behind the object into a texture. // We can access the result in the next pass as _RefractionTex GrabPass { "_RefractionTex" } //通过GrabPass来获取屏幕图像 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); o.scrPos = ComputeGrabScreenPos(o.pos); //得到对应被抓去的屏幕图像的采样坐标 o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex); //_MainTex的采样坐标 o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap); //_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)); // Get the normal in tangent space fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw)); // Compute the offset in tangent space float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy; //对屏幕图像的采样坐标进行偏移, _Distortion 越大偏移越大,则变形程度越大 i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy; fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.w).rgb; // Convert the normal to world space 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" }
1.1.4 程序纹理
程序纹理(Procedural Texture)指的是那些由计算机生成的图像。使用程序纹理的好处在于开发者可以使用各种参数来控制纹理的外观,而属性不仅仅是颜色属性,还可以完全不同类型的图案属性,从而可以得到丰富的动画和视觉效果。
简单的程序纹理:
using UnityEngine; using System.Collections; using System.Collections.Generic; [ExecuteInEditMode] public class ProceduralTextureGeneration : MonoBehaviour { public Material material = null; #region Material properties [SerializeField, SetProperty("textureWidth")] 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; //声明Texture2D纹理变量 // 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(); //调用_UpdateMaterial()来生成程序纹理 } private void _UpdateMaterial() { if (material != null) { m_generatedTexture = _GenerateProceduralTexture(); //调用_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; } }
Unity的程序材质:
利用Substance Designer进行生成纹理。可以在Unity的资源商店或网络中获取许多免费或付费的substance材质,在里面可以直接拖拽到相应的模型上即可。并且其十分多边形,可以调节属性来控制纹理的外观,生成看似完全不同的纹理。
1.2 Unity shader入门精要笔记(十一)
1.2.1 shader中的内置变量
动画效果往往都是把时间添加到一些变量的计算中,以便在时间变化时画面也可以随之变化。Unity Shader提供了一系列关于时间的内置变量来允许开发者更方便地在Shader中访问时间,实现各种动画效果。
图 1.72 Unity内置的时间变量
1.2.2 纹理动画
在各种资源比较局限的移动平台上,开发者往往使用纹理动画来代替复杂的粒子系统等模拟各种动画效果。
序列帧动画:
最常见的纹理动画之一就是序列帧动画,其原理很简单,依次播放一系列关键帧图像,当播放速度达到一定数值时,其看起来就是一个连续的动画。其优点在于灵活性很强,不需要任何物理计算就可以得到细腻的动画效果。其缺点在于每张关键帧图像都不一样,从而制造一张出色的序列帧纹理所需要的美术工程量很大。
Shader "Unity Shaders Book/Chapter 11/Image Sequence Animation" { Properties { //声明多个属性 _Color ("Color Tint", Color) = (1, 1, 1, 1) _MainTex ("Image Sequence", 2D) = "white" {} //包含了所有关键帧图像的纹理 _HorizontalAmount ("Horizontal Amount", Float) = 4 //该图像在水平方向包含的关键帧图像的个数 _VerticalAmount ("Vertical Amount", Float) = 4 //该图像在竖直方向包含的关键帧图像的个数 _Speed ("Speed", Range(1, 100)) = 30 //控制序列帧动画的播放速度 } SubShader { Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"} //序列帧通常是透明纹理,从而需要设置Pass相关状态,以渲染透明效果 Pass { Tags { "LightMode"="ForwardBase" } ZWrite Off //关闭深度写入 Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; float _HorizontalAmount; float _VerticalAmount; float _Speed; struct a2v { float4 vertex : POSITION; float2 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { //前三列代码计算行列数 float time = floor(_Time.y * _Speed); //_Time.y是该场景加载后经过的时间,把其与速度属性相乘得到模拟的时间,并用floor对结果值取整来得到整数时间time float row = floor(time / _HorizontalAmount); //计算行数 float column = time - row * _HorizontalAmount; //计算列数 // half2 uv = float2(i.uv.x /_HorizontalAmount, i.uv.y / _VerticalAmount); // uv.x += column / _HorizontalAmount; // uv.y -= row / _VerticalAmount; half2 uv = i.uv + half2(column, -row); uv.x /= _HorizontalAmount; uv.y /= _VerticalAmount; fixed4 c = tex2D(_MainTex, uv); c.rgb *= _Color; return c; } ENDCG } } FallBack "Transparent/VertexLit" } Shader "Unity Shaders Book/Chapter 11/Image Sequence Animation" { Properties { //声明多个属性 _Color ("Color Tint", Color) = (1, 1, 1, 1) _MainTex ("Image Sequence", 2D) = "white" {} //包含了所有关键帧图像的纹理 _HorizontalAmount ("Horizontal Amount", Float) = 4 //该图像在水平方向包含的关键帧图像的个数 _VerticalAmount ("Vertical Amount", Float) = 4 //该图像在竖直方向包含的关键帧图像的个数 _Speed ("Speed", Range(1, 100)) = 30 //控制序列帧动画的播放速度 } SubShader { Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"} //序列帧通常是透明纹理,从而需要设置Pass相关状态,以渲染透明效果 Pass { Tags { "LightMode"="ForwardBase" } ZWrite Off //关闭深度写入 Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; float _HorizontalAmount; float _VerticalAmount; float _Speed; struct a2v { float4 vertex : POSITION; float2 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { //前三列代码计算行列数 float time = floor(_Time.y * _Speed); //_Time.y是该场景加载后经过的时间,把其与速度属性相乘得到模拟的时间,并用floor对结果值取整来得到整数时间time float row = floor(time / _HorizontalAmount); //计算行数 float column = time - row * _HorizontalAmount; //计算列数 // half2 uv = float2(i.uv.x /_HorizontalAmount, i.uv.y / _VerticalAmount); // uv.x += column / _HorizontalAmount; // uv.y -= row / _VerticalAmount; half2 uv = i.uv + half2(column, -row); uv.x /= _HorizontalAmount; uv.y /= _VerticalAmount; fixed4 c = tex2D(_MainTex, uv); c.rgb *= _Color; return c; } ENDCG } } FallBack "Transparent/VertexLit" }
滚动的背景:
很多2D游戏使用不断滚动的背景来模拟游戏角色在场景中的穿梭,这些背景包含多个层来模拟一种视差效果。背景的实现便是利用纹理动画。
Shader "Unity Shaders Book/Chapter 11/Scrolling Background" { Properties { //声明新的属性 _MainTex ("Base Layer (RGB)", 2D) = "white" {} //第一层(较远)的背景纹理 _DetailTex ("2nd Layer (RGB)", 2D) = "white" {} //第二层较远的纹理 _ScrollX ("Base layer Scroll Speed", Float) = 1.0 //第一层的滚动速度 _Scroll2X ("2nd layer Scroll Speed", Float) = 1.0 //第二层的滚动速度 _Multiplier ("Layer Multiplier", Float) = 1 //控制纹理的整体亮度 } SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry"} Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; sampler2D _DetailTex; float4 _MainTex_ST; float4 _DetailTex_ST; float _ScrollX; float _Scroll2X; float _Multiplier; struct a2v { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float4 uv : TEXCOORD0; }; v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); //顶点模型转换到裁剪空间中 o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0.0) * _Time.y); //通过利用TRANSFORM_TEX来得到初始纹理坐标 o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0.0) * _Time.y); //再通过_Time.y变量再水平空间上对纹理坐标进行偏移,从而达到滚动的效果 //最后将两张纹理的纹理坐标存储到同一个变量o.uv中,以减少占用的插值寄存器空间 return o; } fixed4 frag (v2f i) : SV_Target { fixed4 firstLayer = tex2D(_MainTex, i.uv.xy); //用i.uv/xy对背景纹理进行采样 fixed4 secondLayer = tex2D(_DetailTex, i.uv.zw); fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a); //使用第二层纹理的透明通道来混合两张纹理,利用lerp函数 c.rgb *= _Multiplier; //使用_Multiplier与输出颜色进行相乘从而调整背景亮度 return c; } ENDCG } } FallBack "VertexLit" }
1.2.3 顶点动画
在游戏中,开发者常常使用顶点动画来模拟飘动的旗帜、流动的小溪等效果。
流动的河流:
河流的模拟是顶点动画最常见的应用之一,其原理通常是利用正弦函数等来模拟水流的波动效果。
Shader "Unity Shaders Book/Chapter 11/Water" { Properties { _MainTex ("Main Tex", 2D) = "white" {} //河流的纹理 _Color ("Color Tint", Color) = (1, 1, 1, 1) //用于控制整体颜色 _Magnitude ("Distortion Magnitude", Float) = 1 //控制水流波动的幅度 _Frequency ("Distortion Frequency", Float) = 1 //用于控制波动频率 _InvWaveLength ("Distortion Inverse Wave Length", Float) = 10 //用于控制波长的倒数,其越大,波长越小 _Speed ("Speed", Float) = 0.5 //河流纹理的移动速度 } SubShader { // Need to disable batching because of the vertex animation Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"} //通过disablebatching标签可以直接指明是否对该subshader采用批处理,而这些需要特殊处理的shader通常指包含了模型空间的顶点动画的shader //因为批处理会合并所有相关的模型,而这些模型各自的模型空间就会丢失,而在河流模拟中需要在物体的模型空间下对顶点位置进行偏移,从而需要取消对该shader进行批处理 Pass { Tags { "LightMode"="ForwardBase" } ZWrite Off //关闭深度写入 Blend SrcAlpha OneMinusSrcAlpha //开启混合模式 Cull Off //关闭剔除功能,从而让水流的每个面都能显示 CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; float _Magnitude; float _Frequency; float _InvWaveLength; float _Speed; struct a2v { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert(a2v v) { //在顶点着色器中进行相关的顶点动画 v2f o; float4 offset; offset.yzw = float3(0.0, 0.0, 0.0); //首先计算顶点位移量,因为只希望对顶点的x方向进行位移,因此yzw的位移量被设置为0 offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude; //利用_Frequency属性和内置的 _Time.y来控制正弦函数的频率 //同时为了让不同的位置具有不同的位移,从而需要加上模型空间下的位置分量,并乘以 _InvWaveLength来控制波长,最后通过乘以_Magnitude属性来控制波动幅度从而得到最终的位移 o.pos = UnityObjectToClipPos(v.vertex + offset); //将位移量添加到顶点位置上,并进行正常的顶点变换即可 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); o.uv += float2(0.0, _Time.y * _Speed); return o; } fixed4 frag(v2f i) : SV_Target { //对纹理采样再添加颜色控制即可 fixed4 c = tex2D(_MainTex, i.uv); c.rgb *= _Color.rgb; return c; } ENDCG } } FallBack "Transparent/VertexLit" }
广告牌:
广告牌技术(Billboarding)会根据视角方向来旋转一个被纹理着色的多边形,使得多边形看起来总是面对摄像机。广告牌技术被用于很多应用,如渲染烟雾、云朵、闪光效果等。
广告牌技术的本质就是构建旋转矩阵,一个旋转矩阵需要3个基向量,广告牌技术使用的基向量通常是表面法线(normal)、指向上的方向(up)以及指向右的方向(right)。除此之外还需要一个瞄点(anchor location)。瞄点在旋转过程中是固定不变的,从而确定多边形在空间中的位置。
right = up x normal
Shader "Unity Shaders Book/Chapter 11/Billboard" { Properties { _MainTex ("Main Tex", 2D) = "white" {} //广告牌显示的透明纹理 _Color ("Color Tint", Color) = (1, 1, 1, 1) //用于控制显示整体颜 _VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1 //调整固定法线还是固定指向上的方向,即约束垂直方向的程度 } SubShader { // Need to disable batching because of the vertex animation Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"} //关闭批处理,因为本例子中需要处理模型的各顶点 Pass { Tags { "LightMode"="ForwardBase" } ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Cull Off //关闭剔除功能,从而让广告牌的每个面都能显示出来 CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; fixed _VerticalBillboarding; struct a2v { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert (a2v v) { //顶点着色器是核心,所有的计算都是从模型空间下进行 v2f o; // Suppose the center in object space is fixed float3 center = float3(0, 0, 0); //选择模型空间的原点作为广告牌的锚点 float3 viewer = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1)); //通过内置变量获取模型空间下的视角位置 float3 normalDir = viewer - center; //根据观察位置和锚点计算目标法线方向 // If _VerticalBillboarding equals 1, we use the desired view dir as the normal dir // Which means the normal dir is fixed // Or if _VerticalBillboarding equals 0, the y of normal is 0 // Which means the up dir is fixed normalDir.y =normalDir.y * _VerticalBillboarding; //根据_VerticalBillboarding控制垂直方向上的约束度,当_VerticalBillboarding为0,则意味着向上方向固定为(0,1,0) normalDir = normalize(normalDir); //计算得到的法线方向进行归一化操作来得到单位矢量 // Get the approximate up dir // If normal dir is already towards up, then the up dir is towards front float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0); //为防止法线方向和向上方向平行,对法线方向的y分量进行判断,以得到合适的向上方向 float3 rightDir = normalize(cross(upDir, normalDir)); //根据法线方向和粗略的向上方向得到向右方向,并进行归一化 upDir = normalize(cross(normalDir, rightDir)); //根据准确的法线方向和向右方向得到最后的向上方向 // Use the three vectors to rotate the quad float3 centerOffs = v.vertex.xyz - center; float3 localPos = center + rightDir * centerOffs.x + upDir * centerOffs.y + normalDir * centerOffs.z; //得到新的顶点位置 o.pos = UnityObjectToClipPos(float4(localPos, 1)); //把模型空间的顶点位置变换到裁剪空间中 o.uv = TRANSFORM_TEX(v.texcoord,_MainTex); return o; } fixed4 frag (v2f i) : SV_Target { //对纹理进行采样,再与颜色值相乘 fixed4 c = tex2D (_MainTex, i.uv); c.rgb *= _Color.rgb; return c; } ENDCG } } FallBack "Transparent/VertexLit" }