大家好,我是阿赵。之前一直有网友问我拿昼夜变化demo的源码。但由于那个是几年前突然兴起写的一个例子,也没有特意保存,后来换电脑了就找不到了。
也正是这种原因,所以我最近写文章,都会把源码直接贴上来。这样其实是为了方便我自己以后有需要时能直接找回来而已。
昼夜变化的例子里面,分成了2个部分:
1、天空盒的过渡效果
2、LightMap的过渡效果
由于那个例子是当时随手写的,LightMap过渡的部分,实在是通用性不强,所以我暂时不打算公开源码,等以后考虑得成熟一点再说吧。
天空盒的过渡效果倒是非常的简单,简单到单独写一篇文章来接受我也觉得有点没东西写,所以这篇文章会顺便介绍一下CubeMap的一些扩展知识,和不使用Unity内置天空盒,而用一个球体作为天空盒的做法。
先来看看效果:


天空盒渐变


一、天空盒渐变过渡的shader实现

1、完整Shader

Shader "azhao/SkyBoxTrans"
{
	Properties
	{
		_SkyTex1("SkyTex1",CUBE) = "white"{}
		_SkyTex2("SkyTex2",CUBE) = "white"{}
		_SkyRange("SkyRange",Range(0,1)) = 0

	}
		SubShader
		{
			Tags { "RenderType" = "Opaque" "Queue" = "BackGround" }
			LOD 100

			Pass
			{
				CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag


				#include "UnityCG.cginc"

				struct appdata
				{
					float4 vertex : POSITION;
					float3 normal : NORMAL;
				};

				struct v2f
				{
					float4 vertex : SV_POSITION;
					float3 worldPos : TEXCOORD0;
					float3 worldNormal : TEXCOORD1;
				};

				samplerCUBE _SkyTex1;
				samplerCUBE _SkyTex2;
				float _SkyRange;



				v2f vert(appdata v)
				{
					v2f o;

					o.vertex = UnityObjectToClipPos(v.vertex);
					o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
					o.worldNormal = UnityObjectToWorldNormal(v.normal);
					return o;
				}

				float4 frag(v2f i) : SV_Target
				{
					float3 worldViewDir = UnityWorldSpaceViewDir(i.worldPos);
					worldViewDir = normalize(worldViewDir)*-1;
					float3 worldReflect = reflect(worldViewDir,i.worldNormal);
					float4 col = texCUBE(_SkyTex1, worldReflect)*(1 - _SkyRange) + texCUBE(_SkyTex2, worldReflect)*_SkyRange;
					return col;
				}
				ENDCG
			}
		}
}

2、说明

这个shader实在是非常的简单,其实就是采样了2张CubeMap,然后通过一个插值来做过渡而已。
那么使用的时候,就可以用C#写一个脚本,通过shader名创建一个Material材质球,并且用RenderSettings.skybox赋值给RenderSettings的全局天空盒。然后通过给材质球的_SkyTex1和_SkyTex2赋值,再通过_SkyRange来控制2个skybox之间的插值过渡就可以了。
由于过于简单,所以C#代码我就不写了。

二、第二种天空盒做法

如果不想用Unity的RenderSettings里面的skybox作为天空盒显示,而想通过做一个很大的球体作为天空盒,也是可以的。

这里我用Unity自带的Sphere创建一个球体,并且把它的缩放放大到5000倍,然后把上面用于天空球的材质赋给物体,再从摄像机里面看,会发现看不到这个天空盒。

看不到的原因有2个:

1.由于默认的shader的cull是back的,而unity自带的球体法线方向是向外的,所以摄像机在球体内部看不到

2.把球体放大5000倍之后,摄像机默认的远端裁剪是1000,这时候已经超出了视锥的裁剪范围了。

所以需要对cull和 o.vertex.z做修改

1.cull 改成front

2.由于o.vertex = UnityObjectToClipPos(v.vertex);这里已经是转换成裁剪空间的坐标了,所以我们把o.vertex.z = 0;让球体在裁剪空间的z坐标变成0,那么摄像机就肯定看得到了。

最后要注意的是,由于这个球是模拟天空盒来渲染的,所以需要修改渲染队列"Queue" = “BackGround”

最后,由于是从内部看,所以上下会颠倒,所以在计算UV的时候,就不需要乘以-1了。

unity渐变shader ware_unity

完整的shader

Shader "azhao/SkyBoxModelTrans"
{
	Properties
	{
		_SkyTex1("SkyTex1",CUBE) = "white"{}
		_SkyTex2("SkyTex2",CUBE) = "white"{}
		_SkyRange("SkyRange",Range(0,1)) = 0

	}
		SubShader
		{
			Tags { "RenderType" = "Opaque" "Queue" = "BackGround" }
			LOD 100

			Pass
			{
				cull front
				CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag


				#include "UnityCG.cginc"

				struct appdata
				{
					float4 vertex : POSITION;
					float3 normal : NORMAL;
				};

				struct v2f
				{
					float4 vertex : SV_POSITION;
					float3 worldPos : TEXCOORD0;
					float3 worldNormal : TEXCOORD1;
				};

				samplerCUBE _SkyTex1;
				samplerCUBE _SkyTex2;
				float _SkyRange;



				v2f vert(appdata v)
				{
					v2f o;

					o.vertex = UnityObjectToClipPos(v.vertex);
					o.vertex.z = 0;
					o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
					o.worldNormal = UnityObjectToWorldNormal(v.normal);
					return o;
				}

				float4 frag(v2f i) : SV_Target
				{
					float3 worldViewDir = UnityWorldSpaceViewDir(i.worldPos);
					worldViewDir = normalize(worldViewDir);
					float3 worldReflect = reflect(worldViewDir,i.worldNormal);
					float4 col = texCUBE(_SkyTex1, worldReflect)*(1 - _SkyRange) + texCUBE(_SkyTex2, worldReflect)*_SkyRange;
					return col;
				}
				ENDCG
			}
		}
}

三、CubeMap制作

上面贴完代码了,下面就开始扩展内容了。首先来看看CubeMap是怎样制作的。Unity内部提供了2种方式

1、使用6张贴图生成CubeMap

unity渐变shader ware_skybox_02

所谓的CubeMap,Cube是立方体的意思。一个立方体有6个面,所以传统的CubeMap做法,就是给Cube的这6个面分别贴上“上下左右前后”的贴图,让6个面形成一个无缝连接的Cube。

Unity引擎里面可以创建这种格式的CubeMap

unity渐变shader ware_skybox_03


unity渐变shader ware_skybox_04

把6张图按照指定的方位拖到框里面,就可以看到效果

unity渐变shader ware_天空盒_05

值得注意的是,Unity给了一个警告:

unity渐变shader ware_unity渐变shader ware_06

这个警告的意思大概是降低face size是一个毁灭性的操作,你需要重新指定这些贴图来修复分辨率的问题。

unity渐变shader ware_游戏引擎_07

这是什么意思呢?可以看看这个cubemap文件,会发现这个文件非常的大。实际上这个CubeMap里面的6张贴图并不是引用关系,而是内置在CubeMap里面的。在指定图片的时候,会发现有个Face Size的选项,这个选项规定了拖进去的图片生成CubeMap时的分辨率,Unity会将图片修改成指定的分辨率,并将6张图合并生成一个CubeMap文件。
如果在已经指定生成好了CubeMap之后,再去修改Face Size,其实是并不会直接生效的,还需要把6张贴图重新指定一次,才能生成新的CubeMap文件。
这种生成CubeMap的方式是Legecy的,也就是Unity旧版本的支持方式,新版本的Unity官方并不是特别的建议使用这种方式的CubeMap指定方式。不过由于这种是传统的CubeMap,所以资源特别的好找。

2、使用1张全景贴图生成CubeMap

接下来这种,Unity比较建议的生成CubeMap方式。

unity渐变shader ware_unity渐变shader ware_08

只用一张贴图,然后在导入贴图选项里面把TextureShape指定为Cube

unity渐变shader ware_skybox_09

这样同样也会在Unity引擎里面生成一个CubeMap。

unity渐变shader ware_天空盒_10

但实际上并没有额外的cubemap文件生成,还是原来的png贴图文件。这样的好处是,如果我们想修改这张CubeMap的分辨率,直接在贴图导入设置里面修改就可以了。

unity渐变shader ware_天空盒_11

3、6张贴图的Cube转换成1张全景图

如果我们已经有了6张贴图组成的CubeMap,想把它变成一张全景图,需要怎样操作呢?
我这里写了一个脚本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
public class CreateCubeMapTex : EditorWindow
{
    static private CreateCubeMapTex _instance;

    public static CreateCubeMapTex Instance
    {
        get
        {
            if(_instance == null)
            {
                _instance = EditorWindow.GetWindow<CreateCubeMapTex>();
                _instance.maxSize = _instance.minSize = new Vector2(500, 200);
                _instance.titleContent = new GUIContent("CubeMap转换全景图");
            }
            return _instance;
        }
    }
    [MenuItem("Tools/CubeMap转换全景图")]
    static void ShowWin()
    {
        CreateCubeMapTex.Instance.Show();
    }
    private Cubemap cubemapTex;
    private Object folder;
    private string[] texTypes = new string[] { "jpg", "png", "tga" };
    private int texTypeIndex = 0;
    void OnGUI()
    {
        GUILayout.BeginHorizontal();
        GUILayout.Label("需要转换的CubeMap",GUILayout.Width(120));
        cubemapTex = (Cubemap)EditorGUILayout.ObjectField(cubemapTex, typeof(Cubemap), false, GUILayout.Width(200));
        GUILayout.EndHorizontal();
        GUILayout.BeginHorizontal();
        GUILayout.Label("保存的文件夹", GUILayout.Width(120));
        folder = EditorGUILayout.ObjectField(folder, typeof(Object), false, GUILayout.Width(200));
        GUILayout.EndHorizontal();
        GUILayout.BeginHorizontal();
        GUILayout.Label("选择保存的类型:", GUILayout.Width(120));
        texTypeIndex = EditorGUILayout.Popup(texTypeIndex, texTypes, GUILayout.Width(200));
        GUILayout.EndHorizontal();
        if (GUILayout.Button("转换", GUILayout.Width(80), GUILayout.Height(30)))
        {
            CreateFun();
        }
    }

    private void CreateFun()
    {
        if (cubemapTex == null)
        {
            ShowTips("请先指定需要转换的cubemap");
            return;
        }
        if (folder == null)
        {
            ShowTips("请先指定保存的文件夹");
            return;
        }
        int width = cubemapTex.width;
        int height = cubemapTex.height;

        //生成一个空物体,上面挂摄像机、skybox组件
        GameObject tempGo = new GameObject();
        Camera cam = tempGo.AddComponent<Camera>();
        //生成一个新的材质球,使用unity内置的shader:Skybox/Cubemap
        Skybox skybox = tempGo.AddComponent<Skybox>();
        Material mat = new Material(Shader.Find("Skybox/Cubemap"));
        //把指定的CubeMap赋予给材质球,然后材质球赋予给skybox组件
        mat.SetTexture("_Tex", cubemapTex);
        skybox.material = mat;
        //让摄像机除了指定的skybox,其他东西都看不到
        cam.cullingMask = 0;

        //新建一张RenderTexture,用于摄像机渲染输出
        RenderTexture cubemap = new RenderTexture(width*2, height*2, 32);
        cubemap.dimension = UnityEngine.Rendering.TextureDimension.Cube;
        //把摄像机看到的东西渲染输出到RenderTexture
        cam.RenderToCubemap(cubemap, 63, Camera.MonoOrStereoscopicEye.Mono);
        //再新建一张RenderTexture,用于接受经过转换后的全景图
        RenderTexture equirect = new RenderTexture(width*2, height, 32);
        cubemap.ConvertToEquirect(equirect, Camera.MonoOrStereoscopicEye.Mono);

        RenderTexture origRT = RenderTexture.active;
        RenderTexture.active = equirect;
        //创建需要保存的Texture2D,并复制像素
        Texture2D tex = new Texture2D(equirect.width, equirect.height, TextureFormat.ARGB32, false, true);
        tex.ReadPixels(new Rect(0, 0, tex.width, tex.height), 0, 0);
        RenderTexture.active = origRT;
        GL.Clear(true, true, Color.black);
        tex.Apply();

        //根据指定的保存格式,保存图片
        byte[] bytes = null;
        if(texTypeIndex == 0)
        {
            bytes = tex.EncodeToJPG();
        }
        else if(texTypeIndex == 1)
        {
            bytes = tex.EncodeToPNG();
        }
        else if(texTypeIndex == 2)
        {
            bytes = tex.EncodeToTGA();
        }
        string savePath = AssetDatabase.GetAssetPath(folder);
        savePath = savePath + "/" + cubemapTex.name + "_Panorama."+texTypes[texTypeIndex];
        System.IO.File.WriteAllBytes(savePath, bytes);

        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();

        //删除刚才生成的临时文件
        GameObject.DestroyImmediate(tempGo);
        GameObject.DestroyImmediate(mat);
        GameObject.DestroyImmediate(cubemap);
        GameObject.DestroyImmediate(equirect);

        ShowTips("生成全景图成功,地址:" + savePath);
    }


    private void ShowTips(string str)
    {
        EditorUtility.DisplayDialog("提示", str, "确定");
    }
}

unity渐变shader ware_unity_12

这个脚本放在Editor文件夹里面,然后就可以在菜单”Tools/CubeMap转换全景图”里面打开这个工具窗口。

指定CubeMap、保存的文件夹和需要保存的图片格式,就可以转换CubeMap为一张全景图了。

unity渐变shader ware_unity_13

刚才6张的skybox贴图,经过转换后,变成了这样一张全景图。
虽然很多人拿到工具能用就行,但原理我也解释一下吧:
1.我没找到Unity有直接转换的方法,但Unity有提供把摄像机看到的东西渲染到Cube格式的RenderTexture的方法,所以这个工具的核心就是camera.RenderToCubemap方法
2.为了在转换过程中不受到其他场景里面的元素的影响,所以我新建了一个空的GameObject,并给他加上了Camera组件和SkyBox组件。Camera组件指定cullingMask = 0让它只能渲染指定的Skybox。然后SkyBox组件是可以让摄像机不是读取RenderSetting里面的SkyBox,而是挂在组件上的Skybox
3.剩下的事情就很简单了,RenderToCubemap把摄像机看到的东西渲染到Cube格式的RenderTexture,做转换,然后保存RenderTexture就行了。

四、CubeMap旋转

上面使用CubeMap采样的shader,都是通过世界空间观察方向和世界法线做反射计算,得到的值作为UV的,所以如果我们旋转球体本身,会发现天空球的方向是固定的,并不会跟随物体旋转而旋转。
这里再提供一个可以旋转采样CubeMap的UV的方法。其实就是构造一个旋转矩阵,让反射方向沿着某个轴旋转一定的角度。
完整Shader

Shader "azhao/SkyBoxModelTransRota"
{
	Properties
	{
		_SkyTex1("SkyTex1",CUBE) = "white"{}
		_SkyTex2("SkyTex2",CUBE) = "white"{}
		_SkyRange("SkyRange",Range(0,1)) = 0
		_RotaAxis("RotaAxis",Vector) = (0,1,0)
		_RotaAngle("RotaAngle",Range(-1,1)) = 0
		_RotaCenter("RotaCenter",Vector) = (0,0,0)
	}
		SubShader
		{
			Tags { "RenderType" = "Opaque" "Queue" = "BackGround" }
			LOD 100

			Pass
			{
				cull front
				CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag


				#include "UnityCG.cginc"

				struct appdata
				{
					float4 vertex : POSITION;
					float3 normal : NORMAL;
				};

				struct v2f
				{
					float4 vertex : SV_POSITION;
					float3 worldPos : TEXCOORD0;
					float3 worldNormal : TEXCOORD1;
				};

				samplerCUBE _SkyTex1;
				samplerCUBE _SkyTex2;
				float _SkyRange;
				float3 _RotaAxis;
				float _RotaAngle;
				float3 _RotaCenter;

				float3 RotateAroundAxis(float3 center, float3 position, float3 axis, float angle)
				{
					position -= center;
					float C = cos(angle);
					float S = sin(angle);
					float t = 1 - C;
					float m00 = t * axis.x * axis.x + C;
					float m01 = t * axis.x * axis.y - S * axis.z;
					float m02 = t * axis.x * axis.z + S * axis.y;
					float m10 = t * axis.x * axis.y + S * axis.z;
					float m11 = t * axis.y * axis.y + C;
					float m12 = t * axis.y * axis.z - S * axis.x;
					float m20 = t * axis.x * axis.z - S * axis.y;
					float m21 = t * axis.y * axis.z + S * axis.x;
					float m22 = t * axis.z * axis.z + C;
					float3x3 finalMatrix = float3x3(m00, m01, m02, m10, m11, m12, m20, m21, m22);
					return mul(finalMatrix, position) + center;
				}


				v2f vert(appdata v)
				{
					v2f o;

					o.vertex = UnityObjectToClipPos(v.vertex);
					o.vertex.z = 0;
					o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
					o.worldNormal = UnityObjectToWorldNormal(v.normal);
					return o;
				}

				float4 frag(v2f i) : SV_Target
				{
					float3 worldViewDir = UnityWorldSpaceViewDir(i.worldPos);
					worldViewDir = normalize(worldViewDir);
					float3 worldReflect = reflect(worldViewDir,i.worldNormal);
					float3 cubeUV = RotateAroundAxis(_RotaCenter, worldReflect, normalize(_RotaAxis), _RotaAngle*UNITY_PI);
					float4 col = texCUBE(_SkyTex1, cubeUV)*(1 - _SkyRange) + texCUBE(_SkyTex2, cubeUV)*_SkyRange;
					return col;
				}
				ENDCG
			}
		}
}