【UnityShader】投影テクスチャマッピング #109
前回の成果
オブジェクトの影を受け取った。
今回やること
投影テクスチャマッピングを実装します。
事前準備
Scene上にGameObjectを配置し、今回制作したスクリプトをアタッチします。
スクリーンとなるオブジェクトには、Materialをアタッチします。
ソースコード
スクリプト側
using UnityEngine; public class TextureProjector : MonoBehaviour { [SerializeField, Range(0.0001f, 180)] private float _fieldOfView = 60; [SerializeField, Range(0.2f, 5.0f)] private float _aspect = 1.0f; [SerializeField, Range(0.0001f, 1000.0f)] private float _nearClipPlane = 0.01f; [SerializeField, Range(0.0001f, 1000.0f)] private float _farClipPlane = 100.0f; [SerializeField] private ProjectionType projectionType; [SerializeField] private float _orthographicSize = 1.0f; [SerializeField] private Texture2D _texture; [SerializeField] private Material _material; private enum ProjectionType { PERSPECTIVE, ORTHOGRAPHIC, } private int _matrixVpId; private int _textureId; private int _posId; private void Awake() { SetPropertyId(); } /// <summary> /// PropertyIdの設定 /// </summary> private void SetPropertyId() { _matrixVpId = Shader.PropertyToID("_ProjectorMatrixVP"); _textureId = Shader.PropertyToID("_ProjectorTexture"); _posId = Shader.PropertyToID("_ProjectorPos"); } private void Update() { SetMaterialParam(); } /// <summary> /// マテリアルのパラメータ設定 /// </summary> private void SetMaterialParam() { if (_texture == null) { Debug.LogError("Not Setting Material."); return; } var viewMatrix = Matrix4x4.Scale(new Vector3(1, 1, -1)) * transform.worldToLocalMatrix; var projectionMatrix = GetProjectionMatrix(); _material.SetMatrix(_matrixVpId, projectionMatrix * viewMatrix); _material.SetTexture(_textureId, _texture); // プロジェクターの位置を渡す // _ObjectSpaceLightPosのような感じでwに0が入っていたらOrthographicの前方方向とみなす _material.SetVector(_posId, GetProjectorPos()); } /// <summary> /// Projection行列の取得 /// </summary> /// <returns></returns> private Matrix4x4 GetProjectionMatrix() { Matrix4x4 projectionMatrix; switch (projectionType) { case ProjectionType.PERSPECTIVE : projectionMatrix = Matrix4x4.Perspective(_fieldOfView, _aspect, _nearClipPlane, _farClipPlane); break; case ProjectionType.ORTHOGRAPHIC : var orthographicWidth = _orthographicSize * _aspect; projectionMatrix = Matrix4x4.Ortho(-orthographicWidth, orthographicWidth, -_orthographicSize, _orthographicSize, _nearClipPlane, _farClipPlane); break; default : Debug.LogError("Not Expected CameraType"); return Matrix4x4.identity; } return GL.GetGPUProjectionMatrix(projectionMatrix, true); } /// <summary> /// プロジェクターの座標取得 /// </summary> /// <returns></returns> private Vector4 GetProjectorPos() { Vector4 projectorPos; switch (projectionType) { case ProjectionType.PERSPECTIVE : projectorPos = transform.position; projectorPos.w = 1; break; case ProjectionType.ORTHOGRAPHIC : projectorPos = transform.forward; projectorPos.w = 0; break; default : Debug.LogError("Not Expected CameraType"); return Vector4.zero; } return projectorPos; } private void OnDrawGizmos() { var gizmosMatrix = Gizmos.matrix; Gizmos.matrix = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one); switch (projectionType) { case ProjectionType.ORTHOGRAPHIC : var orthographicWidth = _orthographicSize * _aspect; var length = _farClipPlane - _nearClipPlane; var start = _nearClipPlane + length / 2; Gizmos.DrawWireCube(Vector3.forward * start, new Vector3(orthographicWidth * 2, _orthographicSize * 2, length)); break; case ProjectionType.PERSPECTIVE : Gizmos.DrawFrustum(Vector3.zero, _fieldOfView, _farClipPlane, _nearClipPlane, _aspect); break; default : Debug.LogError("Not Expected CameraType"); break; } Gizmos.matrix = gizmosMatrix; } }
シェーダー側
Shader "Unlit/TextureProjecor" { SubShader { Tags { "RenderType"="Opaque" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float4 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float4 projectorSpacePos : TEXCOORD0; float3 worldPos : TEXCOORD1; float3 worldNormal : TEXCOORD2; }; sampler2D _ProjectorTexture; float4x4 _ProjectorMatrixVP; float4 _ProjectorPos; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.projectorSpacePos = ComputeScreenPos(mul(mul(_ProjectorMatrixVP, unity_ObjectToWorld), v.vertex)); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { // Projection座標に変換 // tex2DProj(_ProjectorTexture, i.projectorSpacePos)と同義 i.projectorSpacePos.xyz /= i.projectorSpacePos.w; float4 projectorTex = tex2D(_ProjectorTexture, i.projectorSpacePos.xy); // カメラの範囲外(0 ~ 1)は描画しない fixed3 isOut = step((i.projectorSpacePos - 0.5) * sign(i.projectorSpacePos), 0.5); float alpha = isOut.x * isOut.y * isOut.z; // プロジェクターから見て、裏側の面には描画しない // Perspective : _ProjectorPos.xyz - i.worldPos // Orthographic : -_ProjectorPos.xyz alpha *= step(-dot(lerp(-_ProjectorPos.xyz, _ProjectorPos.xyz - i.worldPos, _ProjectorPos.w), i.worldNormal), 0); return projectorTex * alpha; } ENDCG } } }
投影テクスチャマッピング
テクスチャをプロジェクターで写したかのように描画する技術のことです。
射影テクスチャマッピングとも呼ばれます。
ProjectionType
カメラがシーンを投影する際に、どのような方法で投影するかを決めるものになります。
投影の手法は2つ存在します。
透視投影(perspective)
近くのオブジェクトを大きく、遠くのオブジェクトを小さく描画する手法になります。
主に3Dゲームで用いられる手法になります。
正投影(orthographic)
オブジェクトの距離では大きさを変えない投影手法になります。
主に2Dゲームで用いられる手法になります。
View行列の作成
var viewMatrix = Matrix4x4.Scale(new Vector3(1, 1, -1)) * transform.worldToLocalMatrix;
今回はこのスクリプトをアタッチしたオブジェクトをカメラのように扱います。
transform.worldToLocalMatrix
は、ワールド座標からローカル座標へと変換した行列を取得するものになっているので、これを元にView行列を取得します。
ScaleのZ座標
Matrix4x4.Scale(new Vector3(1, 1, -1)) * transform.worldToLocalMatrix
transform.worldToLocalMatrix
のZ座標を反転させています。
これはC#は左手座標系、シェーダーは右手座標系なので、その差を吸収するためにZ座標を反転させています。
似たメソッドにCamera.worldToCameraMatrix
があり、こちらは予め右手座標系にしてくれています。
transform.worldToLocalMatrix
Camera.worldToCameraMatrix
Projection行列の作成
private Matrix4x4 GetProjectionMatrix() { Matrix4x4 projectionMatrix; switch (projectionType) { case ProjectionType.PERSPECTIVE : projectionMatrix = Matrix4x4.Perspective(_fieldOfView, _aspect, _nearClipPlane, _farClipPlane); break; case ProjectionType.ORTHOGRAPHIC : var orthographicWidth = _orthographicSize * _aspect; projectionMatrix = Matrix4x4.Ortho(-orthographicWidth, orthographicWidth, -_orthographicSize, _orthographicSize, _nearClipPlane, _farClipPlane); break; default : Debug.LogError("Not Expected CameraType"); return Matrix4x4.identity; } projectionMatrix = GL.GetGPUProjectionMatrix(projectionMatrix, true); return projectionMatrix; }
PERSPECTIVE
Matrix4x4.Perspective(_fieldOfView, _aspect, _nearClipPlane, _farClipPlane);
でProjection行列を計算しています。
Matrix4x4.Perspective
はPerspectiveなProjection行列を生成する関数になっています。
定義
/// <summary> /// PerspectiveなProjection行列を生成 /// </summary> /// <param name="fov">垂直な視野角(度)</param> /// <param name="aspect">アスペクト比</param> /// <param name="zNear">Near Clip</param> /// <param name="zFar">Far Clip</param> /// <returns></returns> public static Matrix4x4 Perspective(float fov, float aspect, float zNear, float zFar);
ORTHOGRAPHIC
Matrix4x4.Ortho(-orthographicWidth, orthographicWidth, -_orthographicSize, _orthographicSize, _nearClipPlane, _farClipPlane);
でProjection行列を計算しています。
Matrix4x4.Ortho
はOrthographicなProjection行列を生成する関数になっています。
定義
/// <summary> /// OrthographicなProjection行列を生成 /// </summary> /// <param name="left">左のx座標</param> /// <param name="right">右のx座標</param> /// <param name="bottom">下のy座標</param> /// <param name="top">上のy座標</param> /// <param name="zNear">Near Clip</param> /// <param name="zFar">Far Clip</param> /// <returns></returns> public static Matrix4x4 Ortho(float left, float right, float bottom, float top, float zNear, float zFar);
プラットフォームの違いを吸収
projectionMatrix = GL.GetGPUProjectionMatrix(projectionMatrix, true);
取得したProjection行列をProjection変換に使用する際に、
View空間にあるxyzを-1~1に収めるような座標に変換する必要があります。
これを正規化デバイス座標と呼びます。
この際にwで除算する必要があるのですが、現在取得したProjection行列はC#側で生成したので正規化した際にShader側とz値を収める範囲が異なります。
その差を吸収してくれるのがGL.GetGPUProjectionMatrix
となります。
定義
/// <summary> /// Projection行列のプラットフォームの違いを吸収する /// </summary> /// <param name="proj">Projection行列</param> /// <param name="renderIntoTexture">Projection行列をRenderTextureで使用するか</param> /// <returns></returns> public static Matrix4x4 GetGPUProjectionMatrix(Matrix4x4 proj, bool renderIntoTexture);
プロジェクターの座標を取得
private Vector4 GetProjectorPos() { Vector4 projectorPos; switch (projectionType) { case ProjectionType.PERSPECTIVE : projectorPos = transform.position; projectorPos.w = 1; break; case ProjectionType.ORTHOGRAPHIC : projectorPos = transform.forward; projectorPos.w = 0; break; default : Debug.LogError("Not Expected CameraType"); return Vector4.zero; } return projectorPos; }
シェーダーの定義済の値に_ObjectSpaceLightPos
があり、
wの値が0の場合は指向性ライト、1の場合はその他のライトとなっています。
それに習い、wの値をPerspectiveかOrthographicによって変化させています。
範囲外の描画をしない
カメラ外
fixed3 isOut = step((i.projectorSpacePos - 0.5) * sign(i.projectorSpacePos), 0.5);
i.projectorSpacePos
はクリップスペース空間に変換されているので、0~1の範囲となっています。
そこでsign
とstep
を組み合わせて0~1の範囲外は描画しないようにしています。
グラフ
赤 : (x-0.5) * sign(x) 青 : step((x-0.5) * sign(x), 0.5)
sign(x)
xが正なら+1.0、0.0なら0.0、負なら-1.0を返す関数になります。
裏麺
step(-dot(lerp(-_ProjectorPos.xyz, _ProjectorPos.xyz - i.worldPos, _ProjectorPos.w), i.worldNormal), 0);
lerp
を使用しているのは、スクリプト側から_ProjectorPosを渡した際のwで投影法を切り替えるためになります。
その後、視線ベクトルと法線ベクトルの内積で裏面かを判断しています。
結果
Perspective
Orthographic
今回は以上となります。
ここまでご視聴ありがとうございました。