使用Unity进行增强现实中的光照和阴影的渲染

我们曾经为大家介绍过Unity中光照和阴影有关的内容,比如Unity实时阴影实现图解。近几年,增强现实应用开发者越来越多,相应的开发技巧被众多开发者所关注。本文将指导您对Unity和Vuforia
SDK增强现实的应用进行光照和阴影渲染。这背后的理论同样适用于其它的SDK和游戏引擎。

光照与阴影的重要性

想要使用Unity创建优质的游戏场景,光照和阴影的设置是非常重要的,因为它们被用来制造场景的景深,从而让画面更加自然与真实。缺少阴影会使得画面缺乏真实感,如果没有阴影,您无法区分出人物是站在平面上还是漂浮在空中,如下图所示。

从上图中可以看出有无阴影的明显区别,在AR增强现实应用开发中也是如此。

渲染光照和阴影存在的问题

如果想在Unity中渲染光照或者阴影,就必须有网格。但是,添加网格会遮挡相机视图。如下图所示,灰色的平板挡住了桌面。

如果把灰色平板换成透明材质不就可以解决这个问题么?其实不然,如果使用透明材质替代上图的灰色平面,使用Unity的标准着色器就无法在上面投射阴影(虽然可能有些自定义着色器可以投射)。如下图所示,使用透明材质可以看到桌面,但没有阴影。

解决思路:分别渲染光照和阴影

最初的想法是利用叠加(Addition)或者复合(Multiplication)效果,可以分别渲染光照和阴影,然后使用后处理效果将阴影和相机视图结合起来。

我们可以将场景内容分为三层:背景、光照和阴影、3D对象,然后将它们合并到一起。

_ _

背景层:只包含相机渲染的图像

_ _

光照和阴影层

_ _

3D对象层

_ _

三层叠加的最终效果

上图由于桌面太黑了,因此可能看不清阴影。您可以查看下面的视频了解更多详情。

详细步骤

如果您还不清楚Unity结合Vuforia开发AR应用的基本步骤,可以先看看之前的文章《5分钟使用Unity制作AR应用》

总得来讲,我们需要添加三个相机分别用来渲染背景、光照和阴影以及增强现实对象,这三个相机的视角与Vuforia的Augmented Reality
Camera相机完全一样,本例中使用CopyCameraData.cs来实现上述功能。另外,由于无法将对象的阴影渲染到另外一层,因此需要将其进行拷贝,我们需要编辑副本的“Mesh
Renderer”,将“Cast Shadows”属性设置为“Shadow Only”。最后,本例中使用了Color
FX插件实现后处理效果。您可以在文末查看教程的视频演示,并点击【阅读原文】下载教程相关素材。

使用Unity实现本例中AR环境下的阴影与光照渲染效果的详细步骤如下:

1)新建场景,删除新场景默认的主相机和平行光。

2)将Vuforia中的预制件ARCamera拖拽至场景中,在检视面板中加入App License Key(Vuforia 的激活码),
并勾选配置文件的Datasets(数据集)中的Load MyTargets Database和Active。

3)将Vuforia 中的预制件ImageTarget拖拽至场景中,设置好Database中的目标识别图。

4)新建一个Plane,将其Position的Y值设为-0.01(略低于识别目标图)。

5)新建一个立方体和球体,作为光照遮挡物。

6)新建一个点光源,设置好光照范围和阴影类型。

7)将ARCamera的中的World Center
Mode由FIRST_TARGET改为SPECIFIC_TARGET。并将目标图像(ImageTarget)指定给World Center。

8)在ARCamera下再创建两个Camera分别命名为Light Camera和ARObject Camera。

9)打开Tags & Layers检视面板,分别添加Background Layer、Light Layer 和 ARObject Layer三个层。

10)设置ARCamera下方的三个相机。将Light Camera的Culling Mask设置为Light Layer和Default;ARObject
Camera的Culling Mask设置为ARObject Layer和Default;Camera 的Clear Flags设置为Solid
Color, Culling Mask设置为Background Layer,
并且其子节点BackgroundPlane的Layer设置为Background Layer。

11)分别为Light Camera和ARObject Camera 添加 CopyCameraData脚本并将TargetCamera指定为Camera。

12)调整一下相机的视角,创建一个空对象命名为_ARObjects并将Plane、Cube和Sphere拖拽至其下方。然后复制_ARObjects对象并命名为_Light
Layer Objects,并将Cube和Sphere的Cast Shadows 设置为Shadows Only。将_Light Layer
Objects的层级设置为Light Layer。

13)删除_ARObjects中的 Plane,并将其层级设置为ARObject Layer。

这样三个相机对应三层就设置好了。最后在Camera上添加后处理脚本对图像进行混合。本例中使用Colorful
FX插件进行混合以实现后处理效果,您也可以使用其它的后处理脚本。在Light
Camera上添加RendderTextureToBlend脚本并将Camera赋给脚本的Blend属性。最后将ARObject Camera的Clear
Flags设置为Depth only并调整深度值即可。调整混合模式选取最理想的效果。

您可以跟着下面的视频一起练习一下:

在教学视频中所涉及一些自定义的脚本,您可以点击【阅读原文】进行下载。

总结

希望本文可以帮您实现增强现实项目中的光照和阴影渲染。

可用于VR环境的列表视图框架

本文为大家介绍Unity Labs团队开发的可用于VR环境的列表视图框架,代码和示例场景可以从Unity Asset
Store获取,也可从Unity开源Git仓库中获取。

Unity Labs介绍

Unity Labs是专注于研究VR、AR、图形及游戏开发等相关前沿技术的团队。目前Unity Labs最主要的项目就是Unity VR编辑器以及Carte
Blanche项目。Unity Labs团队介绍请看:

Carte Blanche项目介绍

Carte Blanche项目(PCB)是Unity实验室的研发计划,目的是为非技术用户提供VR-in-VR的编程工具。Carte
Blanched的核心设计理念主要在于对象与行为的设计,它的一种典型示例:用户可以抓取虚拟的扑克牌,并将其放置在虚拟的桌子上,借助动作捕捉控制器真实地与卡牌互动。概念视频:

PCB卡牌系统介绍

PCB的卡牌系统比传统滚动列表要复杂得多。PCB系统要求卡牌必须能够动态出现或消失,且用户可以触碰到它们。此外,VR应用程序对性能的要求也极其苛刻。还要尽量避免实例化/销毁场景对象,因为这些操作的开销非常之大。最后为了可重用性,外观和感觉上的统一性起见,还需要一套可扩展的解决方案,能够使用其他类型的UI元素制造出相似的体验。

Unity Labs为列表视图开发了一套通用框架作为PCB卡牌系统的基础。代码和示例场景可以从Unity Asset
Store获取,也可从Unity开源Git仓库中获取。示例效果如下:

Model与View的解耦

本框架的一个设计目标是遵循MVC或MVVM设计模式,将数据的显示逻辑(view)与数据的状态(mode)本身解耦。对于任何一个框架而言,框架本身应自动处理列表当前状态的显示。这种实现下我们只需要考虑数据的当前状态,而不用关心如何处理视图的更新。

框架本身会负责搞定这些列表行的内存分配问题,并在列表元素离开屏幕时回收并重用在接下来要显示的元素上。

更为具体的技术实现细节请访问Unity官方中文社区阅读!

资源包

List View框架现已发布至Unity资源商店,一同开放的还有Unity BitBucket官方账号的开源Git仓库。本文的框架是Carte
Blanche项目资源包的首个模块,其他的模块将会陆续地以同样地方式对社区公布。

本框架可以归结为三个C#类:ListViewController(以两个文件存在),ListViewItem与ListViewItemData。这些类用来控制并处理鼠标与触摸的输入以及列表需要显示的数据。在处理游戏手柄,UI,手势输入或VR设备时这些类也能让开发者很方便地完成需要的特性。在PCB的例子中,列表视图的控制是通过手势追踪控制器来实现的。

更为具体的技术实现细节及代码下载请访问Unity官方中文社区!

Unity中Avatar换装实现

资源准备

1.每一套装备模型必须使用同一套骨骼,并单独将骨骼数据保存成一个Prefab。红色部分为武器挂节点(也可以把武器做成一个SkinnedMesh,不采用挂接点的形式),骨骼数据在Unity中的展示形式就是Transform。

2.将模型拆分成多个部分,将每一个部分单独保存成Prefab,武器也单独保存为一个Prefab。

每一个Prefab都含有自身的SkinnedMeshRenderer。

实现过程

1.创建骨骼GameObject,所有装备的蒙皮数据会最终合成到这个Prefab中。

2.创建装备GameObject,用于搜集其中蒙皮数据以生成新的SkinnedMeshRenderer到骨骼Prefab中。

3.public void CombineObject(GameObject skeleton, SkinnedMeshRenderer[] meshes,
bool combine = false)传入骨骼的GameObject和蒙皮数据。

4.搜集装备蒙皮数据中的有效信息。

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// Collect information from meshes



        for (int i = 0; i < meshes.Length; i ++)



        {



            SkinnedMeshRenderer smr = meshes[i];



            materials.AddRange(smr.materials); // Collect materials



            // Collect meshes



            for (int sub = 0; sub < smr.sharedMesh.subMeshCount; sub++)



            {



                CombineInstance ci = new CombineInstance();



                ci.mesh = smr.sharedMesh;



                ci.subMeshIndex = sub;



                combineInstances.Add(ci);



            }



            // Collect bones



            for (int j = 0 ; j < smr.bones.Length; j ++)



            {



                int tBase = 0;



                for (tBase = 0; tBase < transforms.Count; tBase ++)



                {



                    if (smr.bones[j].name.Equals(transforms[tBase].name))



                    {



                        bones.Add(transforms[tBase]);



                        break;



                    }



                }



            }

5.为骨骼GameObject生成新的SkinnedMeshRenderer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Create a new SkinnedMeshRenderer
SkinnedMeshRenderer oldSKinned = skeleton.GetComponent<SkinnedMeshRenderer>();
    if (oldSKinned != null) {

    GameObject.DestroyImmediate(oldSKinned);
    }



SkinnedMeshRenderer r = skeleton.AddComponent < SkinnedMeshRenderer();
r.sharedMesh = new Mesh();



r.sharedMesh.CombineMeshes(combineInstances.ToArray(), false, false);//
Combine meshes

r.bones = bones.ToArray();// Use new bones

6.挂接武器。

1
2
3
4
5
6
7
8
9
10
Transform[] transforms = Instance.GetComponentsInChildren<Transform();

foreach (Transform joint in transforms) {

if (joint.name == "weapon_hand_r")
{// find the joint (need the support of art designer)
        WeaponInstance.transform.parent = joint.gameObject.transform;
        break;
    }
    }

其中WeaponInstance为武器实例GameObject,Instance为骨骼实例GameObject。

合成后的效果

如何优化

合成之后的模型拥有4个独立材质,加上独立的对象武器,也就是会产生5个Draw Call;如果将在骨骼中的这4个材质合并成一个,那么就能将Draw
Call减少到2个。

其中实现过程如下:

优化CombineObject方法,其中Combine为bool类型,用于标识是否合并材质。

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// merge materials



if (combine)



{



    newMaterial = new Material (Shader.Find ("Mobile/Diffuse"));



    oldUV = new List<Vector2[]();



    // merge the texture



    List<Texture2D Textures = new List<Texture2D();



    for (int i = 0; i < materials.Count; i++)



    {



        Textures.Add(materials[i].GetTexture(COMBINE_DIFFUSE_TEXTURE) as
Texture2D);



    }



    newDiffuseTex = new Texture2D(COMBINE_TEXTURE_MAX, COMBINE_TEXTURE_MAX,
TextureFormat.RGBA32, true);



    Rect[] uvs = newDiffuseTex.PackTextures(Textures.ToArray(), 0);



    newMaterial.mainTexture = newDiffuseTex;



// reset uv



    Vector2[] uva, uvb;



    for (int j = 0; j < combineInstances.Count; j++)



    {



        uva = (Vector2[])(combineInstances[j].mesh.uv);



        uvb = new Vector2[uva.Length];



        for (int k = 0; k < uva.Length; k++)



        {



            uvb[k] = new Vector2((uva[k].x * uvs[j].width) + uvs[j].x,
(uva[k].y * uvs[j].height) + uvs[j].y);



        }



        oldUV.Add(combineInstances[j].mesh.uv);



        combineInstances[j].mesh.uv = uvb;



    }



    }

生成新的SkinnedMeshRenderer时略有区别:

最终效果如下:

可以看出,新的SkinnedMeshRenderer只有一个材质,Draw Call自然也就降低了。

示例工程

GitHub:https://github.com/zouchunyi/UnityAvater

感兴趣的朋友可以下载。工程中代码大家可以直接使用,但是美术资源不得用于任何商业用途。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×