版权声明:Davidwang原创文章,严禁用于任何商业途径,授权后方可转载。
在前述章节中,我们实现的阴影都是实时阴影,阴影会根据虚拟物体的形状、移动、灯光而产生变化,实时阴影在带来更好适应性的同时也会消耗大量计算资源,特别在移动设备上,这会挤占其他功能的正常运行资源,造成应用卡顿。在VR中,我们可以将光照效果烘焙进场景中以达到提高性能的目的,然而由于AR场景就是真实环境,无法预先烘焙场景(在复杂AR场景中,包括物体自身阴影,也可以采用灯光烘焙方法提高性能),但如果AR中的虚拟物体是刚体,不发生形变,且不能脱离AR平面时,我们可以采用预先制作阴影的方法实现阴影效果。
(一)预先制作阴影
所谓预先制作阴影,就是在虚拟物体下预先放置一个平面,平面渲染纹理是与虚拟物体匹配的阴影纹理,以此来模拟阴影。因为这个阴影是使用图像的方法来实现的,所以最大的优势是不浪费计算资源,其次是阴影的可以根据需要自由处理成硬阴影、软阴影、超软阴影、斑点阴影等类型,自主性强。缺点是这个阴影一旦设定后在运行时不能依据环境变化而产生变化,不能对环境进行适配。
为防止Unity自带阴影产生干扰,我们先关闭场景中Directional Light阴影产生,如下图所示。
然后制作一张与虚拟物体相匹配的阴影效果图,通常这张图应该是带Alpha通道的png格式纹理图,如下图所示。
新建一个材质,命名为ARShadow,材质使用Unlit->Transparent着色器以实现透明效果,纹理使用刚才的阴影纹理,如下图所示。
在Hierarchy窗口中新建一个空对象,命名为ARPlane,并将虚拟物体模型作为其子物体放置在其下。在ARPlane下建一个Quad对象,将其X轴旋转90度以使其平铺,将上文制作的ARShadow材质赋给它,并调整Quad对象与模型的相对关系,如下图所示。
通过预先制作阴影的方法,我们避免了在运行时实时计算阴影,并且可以预先设置阴影的类型,在真实场景中的效果如下图所示,可以看到,这个阴影效果在模拟一般普通环境下的阴影时非常用效。
(二)一种精确放置物体的方法
在之前的操作中,我们加载虚拟物体的做法通常是先识别显示平面,然后通过手势在平面上点击加载虚拟物体,这种方式的问题是过程不直接(先识别显示平面,然后通过点击加载),另外放置位置不精准。本节我们讲述一种规避这两个问题的加载虚拟物体的方法:直接在可放置虚拟物体的地方显示一个放置指示图标,点击屏幕任何地方都会在指示图标所在位置放置虚拟物体。
虽然现在我们不再需要显示已检测识别的平面,但我们还是平面检测功能(为了真实感,虚拟物体需要放置在平面上而不是悬空浮在空中),因此,我们依然需要在场景中的AR Session Origin对象上挂载AR Plane Manager组件,但由于不需要渲染显示已检测识别的平面,所以其Plane Prefab要置空,如下图所示。
新建一个C#脚本,命名为AppController,编写如下代码:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
[RequireComponent(typeof(ARRaycastManager))]
public class AppControler : MonoBehaviour
{
public GameObject mObjectPrefab;
public GameObject mPlacementIndicator;
private ARRaycastManager mRaycastManager;
private Pose placementPose;
private GameObject placementIndicatorObject;
private bool placementPoseIsValid = false;
void Start()
{
mRaycastManager = GetComponent<ARRaycastManager>();
ARSessionOrigin mSession = GetComponent<ARSessionOrigin>();
placementIndicatorObject = Instantiate(mPlacementIndicator, mSession.trackablesParent);
}
void Update()
{
UpdatePlacementPose();
UpdatePlacementIndicator();
if (placementPoseIsValid && Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began)
{
Instantiate(mObjectPrefab, placementPose.position, Quaternion.identity);
}
}
private void UpdatePlacementIndicator()
{
if (placementPoseIsValid)
{
placementIndicatorObject.SetActive(true);
placementIndicatorObject.transform.SetPositionAndRotation(placementPose.position, placementPose.rotation);
}
else
{
placementIndicatorObject.SetActive(false);
}
}
private void UpdatePlacementPose()
{
var screenCenter = Camera.current.ViewportToScreenPoint(new Vector3(0.5f, 0.5f));
var hits = new List<ARRaycastHit>();
mRaycastManager.Raycast(screenCenter, hits, TrackableType.PlaneWithinPolygon | TrackableType.PlaneWithinBounds);
placementPoseIsValid = hits.Count > 0;
if (placementPoseIsValid)
{
placementPose = hits[0].pose;
var cameraForward = Camera.current.transform.forward;
var cameraBearing = new Vector3(cameraForward.x, 0, cameraForward.z).normalized;
placementPose.rotation = Quaternion.LookRotation(cameraBearing);
}
}
}
该脚本首先实例化一个指示图标,依然使用射线检测的方式检测与识别平面的相交情况,只是这个射线一直从屏幕的正中间位置发出,不再需要用手去点击,在检测到与平面相交后显示并实时更新指示图标姿态,当放置位置有效时(指示图标显示)点击屏幕后会在指示图标所在位置实例化虚拟物体。
将该脚本挂载在场景中的AR Session Origin对象上,并为其赋上指示图标与虚拟物体Prefab,编译运行,效果如下图所示。