本文轉(zhuǎn)自Unity Connect博主 dreamfairy
先上成平圖
測(cè)試效果圖舔亭, 圖中的褲子上投影了一個(gè)眼睛
那么投影的原理是什么呢检疫。厘熟。嘲恍。 那么請(qǐng)看下面這張
這張圖左下角就是投影器看到的景象息堂,投影貼圖“眼睛” 充滿了整個(gè)投影器的視野嚷狞,那么原理就呼之而出了。
在正常渲染褲子的頂點(diǎn)時(shí)荣堰,順便變換到投影器的屏幕空間床未,然后再渲染褲子的片段處理函數(shù)中將位于投影器屏幕空間的像素都換成眼睛即可。
渲染褲子的Shader
Shader "Unlit/ProjectorShader"
{
? ? Properties
? ? {
? ? ? ? _MainTex ("Texture", 2D) = "white" {}
? ? }
? ? SubShader
? ? {
? ? ? ? Tags { "RenderType"="Opaque" }
? ? ? ? Pass
? ? ? ? {
? ? ? ? ? ? CGPROGRAM
? ? ? ? ? ? #pragma vertex vert
? ? ? ? ? ? #pragma fragment frag
? ? ? ? ? ? #include "UnityCG.cginc"
? ? ? ? ? ? struct appdata
? ? ? ? ? ? {
? ? ? ? ? ? ? ? float4 vertex : POSITION;
? ? ? ? ? ? ? ? float2 uv : TEXCOORD0;
? ? ? ? ? ? };
? ? ? ? ? ? struct v2f
? ? ? ? ? ? {
? ? ? ? ? ? ? ? float2 uv : TEXCOORD0;
? ? ? ? ? ? ? ? float4 projectorUV : TEXCOORD1;
? ? ? ? ? ? ? ? float4 vertex : SV_POSITION;
? ? ? ? ? ? };
? ? ? ? ? ? float4x4 _ProjectorP;
? ? ? ? ? ? float4x4 _ProjectorV;
? ? ? ? ? ? float4x4 _ProjectorVP;
? ? ? ? ? ? sampler2D _ProjectorTex;
? ? ? ? ? ? sampler2D _ProjectorFallOut;
? ? ? ? ? ? sampler2D _MainTex;
? ? ? ? ? ? float4 _MainTex_ST;
? ? ? ? ? ? v2f vert (appdata v)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? v2f o;
? ? ? ? ? ? ? ? o.vertex = UnityObjectToClipPos(v.vertex);
? ? ? ? ? ? ? ? o.uv = TRANSFORM_TEX(v.uv, _MainTex);
? ? ? ? ? ? ? ? float4x4 propMVP = mul(_ProjectorVP, unity_ObjectToWorld);
? ? ? ? ? ? ? ? float4 projProjPos = mul(propMVP, v.vertex);
? ? ? ? ? ? ? ? projProjPos = ComputeScreenPos(projProjPos);
? ? ? ? ? ? ? ? o.projectorUV = projProjPos;
? ? ? ? ? ? ? ? return o;
? ? ? ? ? ? }
? ? ? ? ? ? fixed4 frag (v2f i) : SV_Target
? ? ? ? ? ? {
? ? ? ? ? ? ? ? // sample the texture
? ? ? ? ? ? ? ? fixed4 col = tex2D(_MainTex, i.uv);
? ? ? ? ? ? ? ? fixed4 projectorCol = tex2Dproj(_ProjectorTex, i.projectorUV);
? ? ? ? ? ? ? ? // tex2Dproj = xyz/ w
? ? ? ? ? ? ? ? fixed4 projectorFallOutCol = tex2Dproj(_ProjectorFallOut, i.projectorUV);
? ? ? ? ? ? ? ? projectorCol *= projectorFallOutCol;
? ? ? ? ? ? ? ? col.rgb += projectorCol.rgb;
? ? ? ? ? ? ? ? return col;
? ? ? ? ? ? }
? ? ? ? ? ? ENDCG
? ? ? ? }
? ? }
}
自定義投影器的CS腳本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
public class CustomProjector : MonoBehaviour {
? ? public Camera ProjectorCam;
? ? public Texture2D Tex;
? ? public Texture2D FallOut;
? ? public Material Mat;
? ? private void Start()
? ? {
? ? }
? ? private void Update()
? ? {
? ? ? ? Matrix4x4 P = GL.GetGPUProjectionMatrix(ProjectorCam.projectionMatrix, false);
? ? ? ? Matrix4x4 V = ProjectorCam.worldToCameraMatrix;
? ? ? ? Mat.SetMatrix("_ProjectorV", V);
? ? ? ? Mat.SetMatrix("_ProjectorP", P);
? ? ? ? Mat.SetMatrix("_ProjectorVP", P * V);
? ? ? ? Mat.SetTexture("_ProjectorTex", Tex);
? ? ? ? Mat.SetTexture("_ProjectorFallOut", FallOut);
? ? }
}
范例中的代碼借用了Unity的相機(jī)振坚,實(shí)際上并不需要相機(jī)薇搁,僅僅是借用了相機(jī)的投影矩陣和世界空間矩陣而已。
很多項(xiàng)目組在制作移動(dòng)端游戲時(shí)渡八,都使用Projector來(lái)制作主角的投影啃洋,雖然比起ShadowMap是優(yōu)化了許多,但是實(shí)際上只要和Projector碰撞到物件其DC 都會(huì)翻倍屎鳍, 對(duì)于我來(lái)說(shuō)宏娄,這還是不可接受的。
而使用上面范例的代碼逮壁,可以讓DC不翻倍孵坚,但是并不通用, 因?yàn)槭躊rojector影響的物體都需要定制Shader.
Unity自帶Projector會(huì)翻倍的原因主要也是通用性窥淆,跨平臺(tái)卖宠,使用方便, 因此它的原理是
1.找到所有和Projector有碰撞的MeshRenderer
2.使用Projector的材質(zhì)球忧饭,將MeshRenderer的頂點(diǎn)再渲染一遍扛伍,并貼圖
也因此被投影的物體無(wú)法觸發(fā)動(dòng)態(tài)合批
那么問題來(lái),有沒有一種方案眷昆,既可以保證通用性蜒秤,不需要定制被投影目標(biāo)的Shader,又可以使DC不翻倍呢? 答案是:有的亚斋, 但是有代價(jià)
代價(jià)1:需要使用深度圖
代價(jià)2:DC不會(huì)翻倍作媚,但是總共的DC為,被投影物體數(shù)量 + 1. 既物體自帶的DC + 1 * (Projector數(shù)量) 其實(shí)代價(jià)2根本不算個(gè)事
說(shuō)搞就搞
1.開啟相機(jī)的深度渲染
Camera.main.depthTextureMode |= DepthTextureMode.Depth;
2.創(chuàng)建一個(gè)表示投影器范圍的網(wǎng)格帅刊,我搞了個(gè)Cube Mesh
3.創(chuàng)建Cube Mesh對(duì)應(yīng)的相關(guān)矩陣纸泡,因?yàn)槭荂ube 因此創(chuàng)建的投影為正交投影, 當(dāng)然赖瞒,如果也可以使用透視投影女揭。
? ? ? ? BoxCollider collider = this.GetComponent<BoxCollider>();
? ? ? ? this.m_size = collider.size.x / 2;
? ? ? ? this.m_nearClip = -collider.size.x / 2;
? ? ? ? this.m_farClip = collider.size.x / 2;
? ? ? ? this.m_aspect = 1;
? ? ? ? Matrix4x4 projector = default(Matrix4x4);
? ? ? ? projector = Matrix4x4.Ortho(-m_aspect * m_size, m_aspect * m_size, -m_size, m_size, m_nearClip,? ? ? ? ? ? ? ? m_farClip);
? ? ? ? m_worldToProjector = projector * this.transform.worldToLocalMatrix;
? ? ? ? MeshRenderer mr = this.GetComponent<MeshRenderer>();
? ? ? ? mr.sharedMaterial.SetMatrix("_WorldToProjector", m_worldToProjector);
好了蚤假,準(zhǔn)備工作做完了。開始渲染吧吧兔。
首先是將投影器覆蓋的區(qū)域磷仰,采樣出當(dāng)前屏幕空間的深度,類似這樣的效果
要實(shí)現(xiàn)這樣的效果境蔼,就是講頂點(diǎn)變換到投影平面灶平,并將坐標(biāo)變換到UV值域下
大概這樣
vert part
o.screenPos = ComputeScreenPos(o.vertex);
fragment part
fixed4 screenPos = i.screenPos;
screenPos.xy = screenPos.xy / screenPos.w;
float depth = tex2D(_CameraDepthTexture, screenPos).r;
好了,現(xiàn)在我們有了深度箍土,下一部就是講當(dāng)前像素的深度還原回該深度對(duì)應(yīng)的世界坐標(biāo)了
只需要兩部矩陣變換
1.從屏幕空間變換到相機(jī)空間 unity_CameraInvProjection
2.從相機(jī)空間變換到世界空間 unity_MatrixInvV
有了世界坐標(biāo)后逢享,就可以將該坐標(biāo)變換到Projector的控件,就是準(zhǔn)備工作中的 _WorldToProjector
變換到Projector空間后吴藻,還記得范例上的投影器的全部視野就是需要投射的貼圖范圍嗎瞒爬?因此這里要做UV值域的變換
//變換到自定義投影器投影空間
fixed4 projectorPos = mul(_WorldToProjector, worldSpacePos);
projectorPos /= projectorPos.w;
fixed2 projUV = projectorPos.xy * 0.5 + 0.5;? //變換到uv坐標(biāo)系
fixed4 col = tex2D(_ProjectorTex, projUV);? //采樣投影貼圖
fixed4 mask = tex2D(_ProjectorTexMask, projUV); //采樣遮罩貼圖
col.rgb =? lerp(fixed3(1, 1, 1), col.rgb, (1 - mask.r)); //融合
大功告成! 你可能會(huì)好奇沟堡,為什么多了一個(gè)遮罩貼圖侧但? 雖然你講投影器視野內(nèi)的像素部分都貼了投影貼圖,但是是野外的像素怎么辦弦叶?這個(gè)時(shí)候就需要遮罩圖抹掉俊犯,因此遮罩圖的紋理設(shè)置要設(shè)置為Clamp,保證邊緣像素為拉伸且外側(cè)的Alpha為0
項(xiàng)目完整源碼:https://github.com/dreamfairy/Unity-CubeProjector
原文鏈接:https://connect.unity.com/p/unity-projector-tou-ying-qi-yuan-li-yi-ji-you-hua?app=true
歡迎戳上方原文鏈接,下載Unity官方技術(shù)社區(qū)app伤哺,發(fā)現(xiàn)更多資源干貨~