VR日記その29 -実物体にお絵かきをするAR
アプリを開発しようと思ってたんですけど力尽きたのでやる気を出すためにも一旦記事にします。
作ったもの
youtubeにアップロードしたらショート動画扱いされてしまったので試しにニコ動に上げてみました。youtubeがいいという方はこちら。↓
やっていることは大きく3つ。この記事では2つ目について書きます。
・タップしたところの画像をテクスチャとして使うよ
・現実世界にお絵かきするよ
・写真を撮るよ
若干正確性に欠けるのですが、一旦書き起こします。
現実世界にお絵かきするよ
環境
環境は下記のとおりです。
Unity 2021.1.11f1
Niantic Lightship ARDK 1.3.1
参考にしたサイト
お絵描きの仕方は下記を参考にしました。
現実世界との接点の位置情報の取得は公式ドキュメントを参考にしました。
指でたどったところに線を書く
基本的に、紹介したサイトの内容をスマホ用にアレンジしています。
(最終系をnote用に改変したのでもしかしたらうまく動かないかもしれません。最終系も最後にのせるのでそちらを参考にしてください。)
void Update()
{
if (Input.touchCount > 0&& !EventSystem.current.IsPointerOverGameObject(Input.GetTouch(0).fingerId))
{
Touch touch = PlatformAgnosticInput.GetTouch(0);
if (touch.phase == TouchPhase.Began)
{
CurrentLine = Instantiate(Line, new Vector3(0, 0, 0), Quaternion.identity);
lineRenderer = CurrentLine.GetComponent<LineRenderer>();
}
else if (touch.phase == TouchPhase.Moved)
{
TouchPosition(touch);
MakeLineStroke();
}
else if (touch.phase == TouchPhase.Ended)
{
CurrentLine = null;
}
}
}
private void MakeLineStroke()
{
int NextPositionIndex = lineRenderer.positionCount;
lineRenderer.positionCount = NextPositionIndex + 1;
lineRenderer.SetPosition(NextPositionIndex, hitPosition- (_camera.transform.position)*0.3f);
}
最初のif文では、ボタンのタッチと画面のタッチを振り分けています。
if (Input.touchCount > 0&& !EventSystem.current.IsPointerOverGameObject(Input.GetTouch(0).fingerId))
この辺を参考にしました。
本当は別のサイトを参考にしたのですがパッと見つからず…この辺は色々なサイトに載っています。
TouchPhase~の部分でスマホのタッチ判定をしています。
TouchPhase.Began→タッチしたとき
TouchPhase.Moved→指が動いているとき
TouchPhase.Ended→指を放したとき
タッチした時にLineRendererを新しく生成します。
ドラッグ中に線の動きを作成しています。
MakeLineStroke()で指の場所に合わせてラインを伸ばしたり曲げたりしており、最後のlineRenderer.SetPosition~の行でラインを伸長させています。
伸ばす先の座標が " hitPosition- (_camera.transform.position)*0.3f " となっているのは現実世界をもとに作成したメッシュそのままだとあまりにも線とメッシュが重なってしまい、せっかく引いた線が全然見えなかったので調整しているためです。(あまり効果ない時もありますのでおまじない程度のものです。)
タッチしたところ(画面の表面)ではなく現実世界に線を引く
TouchPosition()の部分です。hitPositionを算出しています。
これは公式ドキュメントにあるTouchBegan(Touch touch) の文章をそのまま拾っています。
private void TouchPosition(Touch touch)
{
//check we have a valid frame.
var currentFrame = _session.CurrentFrame;
if (currentFrame == null)
{
return;
}
if (_camera == null) {
return;
}
//do a hit test at at that screen point
var hitTestResults =
currentFrame.HitTest
(
_camera.pixelWidth,
_camera.pixelHeight,
touch.position,
ARHitTestResultType.ExistingPlaneUsingExtent |
ARHitTestResultType.EstimatedHorizontalPlane
);
if (hitTestResults.Count == 0)
{
return;
}
//move our character to the touch hit location
// Set the cursor object to the hit test result's position
hitPosition = hitTestResults[0].WorldTransform.ToPosition();
}
ちなみに、 if (hitTestResults.Count == 0)のようにメッシュに当たらなかった場合は何も返さないので、空中に向けて線を引くことはできません。この辺は改良の余地があるかも。
現実世界の理解が出来るNiantic ARDKを使った意味をこの辺で見出したい。
スクリプト全文
最後に全文を乗せておきます。
今回紹介していない文も入っていますので参考までに。
using Niantic.ARDK.AR;
using Niantic.ARDK.Utilities;
using Niantic.ARDK.AR.ARSessionEventArgs;
using Niantic.ARDK.AR.HitTest;
using Niantic.ARDK.Utilities;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public class Line: MonoBehaviour
{
[SerializeField] GameObject Line;
[SerializeField] Camera _camera;
[SerializeField] Camera textureCamera;
[SerializeField] private RenderTexture renderTexture;
[SerializeField] private Texture2D tex;
[SerializeField] RawImage image;
[SerializeField] Shader shader;
//we need the session for the hit test.
IARSession _session;
private string status;
private GameObject CurrentLine;
private LineStatus lineStatus;
private LineRenderer lineRenderer;
private Vector3 hitPosition;
// Update is called once per frame
void Start()
{
//we will need to catch the session in for our hit test function.
ARSessionFactory.SessionInitialized += OnSessionInitialized;
}
//callback for the session starting.
private void OnSessionInitialized(AnyARSessionInitializedArgs args)
{
//only run once guard
ARSessionFactory.SessionInitialized -= OnSessionInitialized;
//save the session.
_session = args.Session;
}
void Update()
{
if (Input.touchCount > 0&& !EventSystem.current.IsPointerOverGameObject(Input.GetTouch(0).fingerId))
{
Touch touch = PlatformAgnosticInput.GetTouch(0);
if (touch.phase == TouchPhase.Began)
{
if (status == "stroke")
{
CurrentLine = Instantiate(Line, new Vector3(0, 0, 0), Quaternion.identity);
lineRenderer = CurrentLine.GetComponent<LineRenderer>();
var newMaterial = new Material(shader);
newMaterial.mainTexture = tex;
lineRenderer.material = newMaterial;
}
}
else if (touch.phase == TouchPhase.Moved)
{
TouchPosition(touch);
switch (status)
{
case "stroke":
MakeLineStroke();
break;
case "texture":
textureCamera.transform.position = touch.position;
break;
}
}
else if (touch.phase == TouchPhase.Ended)
{
switch (status)
{
case "stroke":
CurrentLine = null;
break;
case "texture":
StartCoroutine(GetTexture());
break;
}
}
}
}
private void MakeLineStroke()
{
int NextPositionIndex = lineRenderer.positionCount;
lineRenderer.positionCount = NextPositionIndex + 1;
lineRenderer.SetPosition(NextPositionIndex, hitPosition- (_camera.transform.position)*0.3f);
}
protected IEnumerator GetTexture()
{
//Texture2Dを作成
tex = new Texture2D(renderTexture.width, renderTexture.height);
yield return new WaitForEndOfFrame();
tex.ReadPixels(new Rect(textureCamera.transform.position.x, textureCamera.transform.position.y, renderTexture.width, renderTexture.height), 0, 0);
tex.Apply();
image.texture = tex;
}
public void ChangeStatus(string a)
{
status = a;
}
private void TouchPosition(Touch touch)
{
//check we have a valid frame.
var currentFrame = _session.CurrentFrame;
if (currentFrame == null)
{
return;
}
if (_camera == null) {
return;
}
//do a hit test at at that screen point
var hitTestResults =
currentFrame.HitTest
(
_camera.pixelWidth,
_camera.pixelHeight,
touch.position,
ARHitTestResultType.ExistingPlaneUsingExtent |
ARHitTestResultType.EstimatedHorizontalPlane
);
if (hitTestResults.Count == 0)
{
return;
}
//move our character to the touch hit location
// Set the cursor object to the hit test result's position
hitPosition = hitTestResults[0].WorldTransform.ToPosition();
}
}