天空盒过渡变化效果
迪丽瓦拉
2025-05-30 00:46:19
0

大家好,我是阿赵。之前一直有网友问我拿昼夜变化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 100Pass{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 100Pass{cull frontCGPROGRAM#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();_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();//生成一个新的材质球,使用unity内置的shader:Skybox/CubemapSkybox skybox = tempGo.AddComponent();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;//把摄像机看到的东西渲染输出到RenderTexturecam.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 100Pass{cull frontCGPROGRAM#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}}
}

相关内容

热门资讯

linux入门---制作进度条 了解缓冲区 我们首先来看看下面的操作: 我们首先创建了一个文件并在这个文件里面添加了...
C++ 机房预约系统(六):学... 8、 学生模块 8.1 学生子菜单、登录和注销 实现步骤: 在Student.cpp的...
A.机器学习入门算法(三):基... 机器学习算法(三):K近邻(k-nearest neigh...
数字温湿度传感器DHT11模块... 模块实例https://blog.csdn.net/qq_38393591/article/deta...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
Redis 所有支持的数据结构... Redis 是一种开源的基于键值对存储的 NoSQL 数据库,支持多种数据结构。以下是...
win下pytorch安装—c... 安装目录一、cuda安装1.1、cuda版本选择1.2、下载安装二、cudnn安装三、pytorch...
MySQL基础-多表查询 文章目录MySQL基础-多表查询一、案例及引入1、基础概念2、笛卡尔积的理解二、多表查询的分类1、等...
keil调试专题篇 调试的前提是需要连接调试器比如STLINK。 然后点击菜单或者快捷图标均可进入调试模式。 如果前面...
MATLAB | 全网最详细网... 一篇超超超长,超超超全面网络图绘制教程,本篇基本能讲清楚所有绘制要点&#...
IHome主页 - 让你的浏览... 随着互联网的发展,人们越来越离不开浏览器了。每天上班、学习、娱乐,浏览器...
TCP 协议 一、TCP 协议概念 TCP即传输控制协议(Transmission Control ...
营业执照的经营范围有哪些 营业执照的经营范围有哪些 经营范围是指企业可以从事的生产经营与服务项目,是进行公司注册...
C++ 可变体(variant... 一、可变体(variant) 基础用法 Union的问题: 无法知道当前使用的类型是什...
血压计语音芯片,电子医疗设备声... 语音电子血压计是带有语音提示功能的电子血压计,测量前至测量结果全程语音播报࿰...
MySQL OCP888题解0... 文章目录1、原题1.1、英文原题1.2、答案2、题目解析2.1、题干解析2.2、选项解析3、知识点3...
【2023-Pytorch-检... (肆十二想说的一些话)Yolo这个系列我们已经更新了大概一年的时间,现在基本的流程也走走通了,包含数...
实战项目:保险行业用户分类 这里写目录标题1、项目介绍1.1 行业背景1.2 数据介绍2、代码实现导入数据探索数据处理列标签名异...
记录--我在前端干工地(thr... 这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前段时间接触了Th...
43 openEuler搭建A... 文章目录43 openEuler搭建Apache服务器-配置文件说明和管理模块43.1 配置文件说明...