US3S8L3——动态生成立方体纹理

本章代码关键字

1
2
3
camera.RenderToCubemap()    // 摄像机根据所在位置动态生成立方体纹理
Cubemap // 立方体纹理类
RenderTexture // 渲染纹理类,可以作为立方体纹理

为什么要动态生成立方体纹理?

立方体纹理 中提到过,立方体纹理的其中一个最大的作用就是环境映射,
在实现反射、折射等等效果时,需要用到立方体纹理来制作对应效果,
而立方体纹理中最重要的就是组成它的 6 张 2D 纹理图片

对于之前学过的天空盒来说,6 张 2D 纹理图片可以根据想要的美术表现效果来进行自定义制作,
提前把纹理制作好,直接使用即可,这种立方体纹理往往是被提前做好的,是场景中物体们共用的,

但如果制作反射、折射等效果还是使用这样的立方体纹理,效果肯定不够理想,因为物体在场景上的位置不同,产生的对应效果也会是不同的

举例说明:以下图为例,假设我们希望立方体可以反射出周围光照,
如果只使用提前做好的立方体纹理(例如天空盒)做反射,那么周围球体等其他物体是不会被反射出来的

image

因此,为了更好更真实的表现效果,对于场景中不同位置的物体,我们应该为它们在各自的位置上生成不同的立方体纹理(6 张 2D 纹理贴图)

如何动态生成立方体纹理?

我们将结合

  1. Unity编辑器拓展

    具体知识详见:UED——Unity编辑器拓展

  2. ​Camera​ 中的 RenderToCubemap()​ 方法

这两个知识点,在对应位置生成对应的立方体纹理贴图,对于一些场景展示类项目,我们不需要实时生成,只需要在编辑器模式下生成一次即可

主要要完成的功能为:

  1. 自定义编辑器窗口,关联对象(通过对象来指定位置)和 Cubemap​ 变量
  2. 自定义窗口中有一个生成按钮,点击后使用 Camera​ 中的 RenderToCubemap()​ 自动生成对应的6张2D纹理贴图

其中编辑器窗口相关功能使用 Unity 编辑器拓展相关知识
自动生成立方体纹理贴图功能使用 Camera​ 中的 RenderToCubemap()​ 方法
该方法可以将任意位置观察到的场景图像存储到 6 张图像中

动态生成立方体纹理

camera.RenderToCubemap()​ 会基于 Camera​ 所依附的 GameObject所在的位置生成立方体纹理(立方体纹理呈现的就是此位置周围的环境)

  • 参数一:需要保存立方体纹理的 Cubemap​(也可以使用 RenderTexture​)

  • 参数二:指示渲染到立方体贴图的哪个面,默认是同时渲染六个面,传入 CubemapFace​ 枚举即可(需要转换为 int​ 类型)

    CubemapFace​ 定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public enum CubemapFace
    {
    Unknown = -1,
    PositiveX,
    NegativeX,
    PositiveY,
    NegativeY,
    PositiveZ,
    NegativeZ
    }
  • 返回值:是否渲染成功的 bool​ 值

按照上文提到的制作思路来实现逻辑:

  1. 新建一个脚本 RenderToCubemap​ 放在 Editor 文件夹中

    image

  2. 让该类继承 EditorWindow​ 将其作为一个编辑器窗口类

  3. 实现打开该窗口的静态函数

    创建编辑器窗口知识详见:UEDL2——自定义窗口拓展

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class RenderToCubeMap : EditorWindow
    {
    [MenuItem("Tools/立方体纹理动态生成/打开生成窗口")]
    static void OpenWindow()
    {
    RenderToCubeMap window = EditorWindow.GetWindow<RenderToCubeMap>("立方体纹理生成窗口");
    window.Show();
    }
    }
  4. 实现 OnGUI()​ 中的窗口布局,添加以下控件

    1. 关联位置对象的控件
    2. 关联立方体纹理贴图的空间
    3. 生成按钮 GUILayout.Button()​

    其中,要关联一个对象,需要使用 EditorGUILayout.ObjectField()​ 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class RenderToCubeMap : EditorWindow
    {
    private GameObject renderObj;
    private Cubemap cubemap;
    private void OnGUI()
    {
    GUILayout.Label("立方体纹理生成所在位置的对象");
    renderObj = (EditorGUILayout.ObjectField(renderObj, typeof(GameObject), true)) as GameObject;
    GUILayout.Label("保存立方体纹理数据的Cubemap文件");
    cubemap = (EditorGUILayout.ObjectField(cubemap, typeof(Cubemap), true)) as Cubemap;
    if (GUILayout.Button("生成立方体纹理"))
    {
    if (renderObj == null || cubemap == null)
    {
    EditorUtility.DisplayDialog("提醒", "在生成立方体纹理前,需要先关联需要生成立方体纹理的对象和保存纹理的立方体贴图", "确认");
    return;
    }
    // 动态生成立方体纹理
    // ...
    }
    }
    }
  5. 实现具体逻辑

    注意点:

    1. 我们需要在指定位置(关联的对象上)动态创建一个空物体,并为它添加摄像机 Camera​,再通过此 Camera​ 生成立方体纹理贴图

      创建该对象为临时对象,使用完毕后直接删除即可

    2. cubemap​ 上需要勾选 Readable

    3. Face size 分辨率决定了清晰度,但也决定了 Cubemap​ 的大小,因此需要根据项目的实际情况去选择

    image

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    private GameObject renderObj;
    private Cubemap cubemap;

    private void OnGUI()
    {
    GUILayout.Label("立方体纹理生成所在位置的对象");
    renderObj = (EditorGUILayout.ObjectField(renderObj, typeof(GameObject), true)) as GameObject;
    GUILayout.Label("保存立方体纹理数据的Cubemap文件");
    cubemap = (EditorGUILayout.ObjectField(cubemap, typeof(Cubemap), true)) as Cubemap;
    if (GUILayout.Button("生成立方体纹理"))
    {
    if (renderObj == null || cubemap == null)
    {
    EditorUtility.DisplayDialog("提醒", "在生成立方体纹理前,需要先关联需要生成立方体纹理的对象和保存纹理的立方体贴图", "确认");
    return;
    }
    // 动态的生成立方体纹理
    GameObject tempObj = new GameObject("临时对象");
    tempObj.transform.position = renderObj.transform.position;
    Camera camera = tempObj.AddComponent<Camera>();
    // 使用 RenderToCubemap 方法生成6张2D纹理贴图,用于立方体纹理
    camera.RenderToCubemap(cubemap);
    DestroyImmediate(tempObj);
    }
    }

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class RenderToCubeMap : EditorWindow
{
private GameObject renderObj;
private Cubemap cubemap;

[MenuItem("Tools/立方体纹理动态生成/打开生成窗口")]
static void OpenWindow()
{
RenderToCubeMap window = EditorWindow.GetWindow<RenderToCubeMap>("立方体纹理生成窗口");
window.Show();
}

private void OnGUI()
{
GUILayout.Label("立方体纹理生成所在位置的对象");
renderObj = (EditorGUILayout.ObjectField(renderObj, typeof(GameObject), true)) as GameObject;
GUILayout.Label("保存立方体纹理数据的Cubemap文件");
cubemap = (EditorGUILayout.ObjectField(cubemap, typeof(Cubemap), true)) as Cubemap;
if (GUILayout.Button("生成立方体纹理"))
{
if (renderObj == null || cubemap == null)
{
EditorUtility.DisplayDialog("提醒", "在生成立方体纹理前,需要先关联需要生成立方体纹理的对象和保存纹理的立方体贴图", "确认");
return;
}
// 动态的生成立方体纹理
GameObject tempObj = new GameObject("临时对象");
tempObj.transform.position = renderObj.transform.position;
Camera camera = tempObj.AddComponent<Camera>();
// 使用 RenderToCubemap 方法生成6张2D纹理贴图,用于立方体纹理
camera.RenderToCubemap(cubemap);
DestroyImmediate(tempObj);
}
}
}

现在就可以生成立方体纹理,假设要为场景内如下对象生成立方体纹理:

image

首先创建两个用于存储立方体纹理数据的 Cubemap​ 文件(Project 窗口右键 — Create — Legacy — Cubemap)
然后分别将 要立方体纹理生成所在位置的 GameObject​ 和 Cubemap​ 文件 关联到实现的编辑器窗口上,点击 “生成立方体纹理”

image

此时,关联的 Cubemap​ 文件就保存了立方体纹理数据,可以在预览窗口上看到:

image

将立方体纹理生成到 Render Texture 上

camera.RenderToCubemap()​ 除了将立方体纹理数据生成到 Cubemap 文件内,还可以生成到 Render Texture 文件上
其中 Render Texture 的 Dimension 属性需要设置为 Cube ,才能将数据生成到 Render Texture 内
而 Render Texture 的 size 属性决定了立方体纹理的清晰度和大小

注意!

虽然 RenderTexture​ 可以当作立方体纹理使用,但是 RenderTexture​ 的数据在默认情况下不会被保存中,
这意味着,在保存场景时,只生成一次的 RenderTexture​ 的立方体纹理数据会被直接丢弃,导致反射贴图变黑,
因此 RenderTexture不适用于持久化存储立方体纹理,而适用于运行时动态生成立方体纹理的场景

下文的例子,虽然还是在编辑器环境下为 RenderTexture​ 一次生成立方体纹理数据,
但是一旦在保存场景,贴图会直接变黑,因此,下文的编辑器生成例子仅供参考,不要用在实际运行环境内

它的生成方法和上文中使用的 Cubemap 差不多,这里不再阐述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class RenderToCubeMap : EditorWindow
{
private GameObject renderObj;
private Cubemap cubemap;
private RenderTexture renderTexture;

[MenuItem("Tools/立方体纹理动态生成/打开生成窗口")]
static void OpenWindow()
{
RenderToCubeMap window = EditorWindow.GetWindow<RenderToCubeMap>("立方体纹理生成窗口");
window.Show();
}

private void OnGUI()
{
GUILayout.Label("立方体纹理生成所在位置的对象");
renderObj = (EditorGUILayout.ObjectField(renderObj, typeof(GameObject), true)) as GameObject;

GUILayout.Label("保存立方体纹理数据的Cubemap文件");
cubemap = (EditorGUILayout.ObjectField(cubemap, typeof(Cubemap), true)) as Cubemap;
if (GUILayout.Button("生成立方体纹理到Cubemap文件"))
{
if (renderObj == null || cubemap == null)
{
EditorUtility.DisplayDialog("提醒", "在生成立方体纹理前,需要先关联需要生成立方体纹理的对象和保存纹理的Cubemap", "确认");
return;
}
// 动态的生成立方体纹理
GameObject tempObj = new GameObject("临时对象");
tempObj.transform.position = renderObj.transform.position;
Camera camera = tempObj.AddComponent<Camera>();
// 使用 RenderToCubemap 方法生成6张2D纹理贴图,用于立方体纹理
camera.RenderToCubemap(cubemap);
DestroyImmediate(tempObj);
}

GUILayout.Label("保存立方体纹理数据的RenderTexture文件");
renderTexture = (EditorGUILayout.ObjectField(renderTexture, typeof(RenderTexture), true)) as RenderTexture;
if (GUILayout.Button("生成立方体纹理到RenderTexture"))
{
if (renderObj == null || renderTexture == null)
{
EditorUtility.DisplayDialog("提醒", "在生成立方体纹理前,需要先关联需要生成立方体纹理的对象和保存纹理的RenderTexture", "确认");
return;
}
// 动态的生成立方体纹理
GameObject tempObj = new GameObject("临时对象");
tempObj.transform.position = renderObj.transform.position;
Camera camera = tempObj.AddComponent<Camera>();
// 使用 RenderToCubemap 方法生成6张2D纹理贴图,用于立方体纹理
camera.RenderToCubemap(renderTexture);
DestroyImmediate(tempObj);
}
}
}

显示效果:

image

值得一提的是,生成到 Render Texture 上的运行时间要比生成到 Cubemap 上的时间要短得多,
以下为测试代码(使用 C# Stopwatch​ 测试,生成位置相同的 512*512 质量的 Cube 贴图):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
private void OnGUI()
{
GUILayout.Label("立方体纹理生成所在位置的对象");
renderObj = (EditorGUILayout.ObjectField(renderObj, typeof(GameObject), true)) as GameObject;

GUILayout.Label("保存立方体纹理数据的Cubemap文件");
cubemap = (EditorGUILayout.ObjectField(cubemap, typeof(Cubemap), true)) as Cubemap;
if (GUILayout.Button("生成立方体纹理到Cubemap文件"))
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
if (renderObj == null || cubemap == null)
{
EditorUtility.DisplayDialog("提醒", "在生成立方体纹理前,需要先关联需要生成立方体纹理的对象和保存纹理的Cubemap", "确认");
return;
}
// 动态的生成立方体纹理
GameObject tempObj = new GameObject("临时对象");
tempObj.transform.position = renderObj.transform.position;
Camera camera = tempObj.AddComponent<Camera>();
// 使用 RenderToCubemap 方法生成6张2D纹理贴图,用于立方体纹理
camera.RenderToCubemap(cubemap);
DestroyImmediate(tempObj);
stopwatch.Stop();
UnityEngine.Debug.Log($"Cubemap 生成时间为:{stopwatch.ElapsedMilliseconds} ms");
}

GUILayout.Label("保存立方体纹理数据的RenderTexture文件");
renderTexture = (EditorGUILayout.ObjectField(renderTexture, typeof(RenderTexture), true)) as RenderTexture;
if (GUILayout.Button("生成立方体纹理到RenderTexture"))
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
if (renderObj == null || renderTexture == null)
{
EditorUtility.DisplayDialog("提醒", "在生成立方体纹理前,需要先关联需要生成立方体纹理的对象和保存纹理的RenderTexture", "确认");
return;
}
// 动态的生成立方体纹理
GameObject tempObj = new GameObject("临时对象");
tempObj.transform.position = renderObj.transform.position;
Camera camera = tempObj.AddComponent<Camera>();
// 使用 RenderToCubemap 方法生成6张2D纹理贴图,用于立方体纹理
camera.RenderToCubemap(renderTexture);
DestroyImmediate(tempObj);
stopwatch.Stop();
UnityEngine.Debug.Log($"RenderTexture 生成时间为:{stopwatch.ElapsedMilliseconds} ms");
}
}

运行时间对比:

image

在运行时动态生成立方体纹理

Camera​ 中的 RenderToCubemap​ 也可以在运行时实时动态生成立方体纹理,但是要注意对性能的影响,
如果每帧都需要渲染立方体贴图的所有六个面,生成操作开销将会很大

  1. 在 LateUpdate​ 中使用
  2. 降低立方体纹理贴图的分辨率
  3. 分帧渲染,RenderToCubemap()​ 有重载(使用第二个参数),可以一个面一个面的渲染
  4. 降低更新频率,不要每帧执行
  5. 改用 RenderTexture​ 来接收立方体纹理数据

等等

以下面的代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnityEngine;

public class RealTimeCubeRender : MonoBehaviour
{
private Camera _camera;
public RenderTexture _renderTexture;

void Start()
{
_camera = gameObject.AddComponent<Camera>();
_camera.enabled = false;
}

private void LateUpdate()
{
// 每帧为外部关联的 RenderTexture 生成立方体纹理数据,以实现实时反射效果
_camera.RenderToCubemap(_renderTexture);
}
}

将该脚本依附某个 GameObject​ 上,再关联一个 RenderTexture​,这个对象就可以实时的在 GameObject​ 位置上生成立方体纹理
假设,GameObject​ 使用了基于此 RenderTexture​ 的立方体纹理实现反射的 Shader,
那么这个 GameObject​ 就可以实时的反射周围的对象了,而不需要再手动重新生成立方体纹理

反射效果相关内容详见:US3S8L4——反射效果

image