大家好,我是阿赵。之前一直有网友问我拿昼夜变化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了。
完整的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
所谓的CubeMap,Cube是立方体的意思。一个立方体有6个面,所以传统的CubeMap做法,就是给Cube的这6个面分别贴上“上下左右前后”的贴图,让6个面形成一个无缝连接的Cube。
Unity引擎里面可以创建这种格式的CubeMap
把6张图按照指定的方位拖到框里面,就可以看到效果
值得注意的是,Unity给了一个警告:
这个警告的意思大概是降低face size是一个毁灭性的操作,你需要重新指定这些贴图来修复分辨率的问题。
这是什么意思呢?可以看看这个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方式。
只用一张贴图,然后在导入贴图选项里面把TextureShape指定为Cube
这样同样也会在Unity引擎里面生成一个CubeMap。
但实际上并没有额外的cubemap文件生成,还是原来的png贴图文件。这样的好处是,如果我们想修改这张CubeMap的分辨率,直接在贴图导入设置里面修改就可以了。
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, "确定");
}
}
这个脚本放在Editor文件夹里面,然后就可以在菜单”Tools/CubeMap转换全景图”里面打开这个工具窗口。
指定CubeMap、保存的文件夹和需要保存的图片格式,就可以转换CubeMap为一张全景图了。
刚才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
}
}
}