Unity渲染頂點(diǎn)作為顏色到紋理獲取頂點(diǎn)的世界坐標(biāo)

通過(guò)渲染到浮點(diǎn)紋理實(shí)現(xiàn)三維對(duì)象拾取
Unity 瑣碎(2): Shader 顏色調(diào)試

最近項(xiàng)目有個(gè)新需求,在AR場(chǎng)景中,點(diǎn)擊場(chǎng)景的任意位置可以獲取到點(diǎn)擊位置物體表面的位置,傳統(tǒng)的都是用碰撞來(lái)做,給不同對(duì)象添加BoxCollider,然后發(fā)射線,即可。不過(guò)我們希望碰撞點(diǎn)的位置能在物體的表面,而不是包圍盒的表面,打個(gè)比方,一把椅子,它的包圍盒中有很大一塊區(qū)域是空的,因?yàn)榘鼑械挠?jì)算是將所有頂點(diǎn)都包含在盒子中,所以碰撞點(diǎn)只能落在包圍盒上,看起來(lái)離椅子還有很遠(yuǎn)的距離,如果使用mesh collider,對(duì)于一個(gè)場(chǎng)景中任意位置都支持的話,模型太多,性能消耗不起。


椅子包圍盒

后來(lái)經(jīng)群友提示,可以將頂點(diǎn)的位置(XYZ)作為顏色(RGB)渲染到RT上,再?gòu)腞T上采樣獲取RGB值,將RGB值轉(zhuǎn)換為世界坐標(biāo),這是一種非常取巧的方式,也可以說(shuō)是歪門邪道,哈哈,不過(guò)能實(shí)現(xiàn)功能就好。
下面說(shuō)下核心的思路和代碼:
1.先將頂點(diǎn)位置通過(guò)shader轉(zhuǎn)換為世界坐標(biāo),注意世界坐標(biāo)是(-∞,+∞),而顏色是[0,1],所以需要做一個(gè)映射,先歸一化,再映射到[0,1],注意dis * 0.01,作為A通道輸出,是用于獲取世界坐標(biāo)的逆運(yùn)算,乘以0.01是為了轉(zhuǎn)換[0,1]區(qū)間,我目前的項(xiàng)目中不會(huì)超過(guò)100單位,這是是個(gè)經(jīng)驗(yàn)值,可以自行嘗試得到一個(gè)較好的值。

這個(gè)shader是在Camera的OnPreRender時(shí)使用RenderWithShader方法,臨時(shí)替代原shader 獲取一張RT時(shí)使用的,所以要注意,凡是需要渲染頂點(diǎn)到顏色的材質(zhì)使用過(guò)的RenderType,都要實(shí)現(xiàn)一遍,關(guān)于RenderType可以參考筆者之前的一篇文章UnityShader RenderType。因?yàn)楣P者的場(chǎng)景中材質(zhì)shader使用到三種RenderType,分別是:

    Tags { "RenderType"="Opaque" }

    Tags { "RenderType"="TransparentCutout" }

    Tags { "RenderType"="Transparent" }

所以需要3個(gè)SubShader,Tags分別標(biāo)記為上述三個(gè)類別。

SubShader {
    Tags { "RenderType"="Opaque" }
    LOD 300

    Pass {
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #pragma target 2.0

        #include "UnityCG.cginc"

        struct appdata_t {
            float4 vertex : POSITION;
            float4 uv: TEXCOORD0;
        };

        struct v2f {
            float4 vertex : SV_POSITION;
            float2 uv: TEXCOORD0; 
            float4 worldPos : TEXCOORD1;
        };

        float4 _MainTex_TexelSize;

        v2f vert (appdata_t v)
        {
            v2f o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            //需要處理UV翻轉(zhuǎn)問(wèn)題
        #if UNITY_UV_STARTS_AT_TOP 
            if(_MainTex_TexelSize.y < 0)
                o.uv = float2(v.uv.x, 1-v.uv.y);
            else
                o.uv = v.uv;
        #else
            o.uv = v.uv;
        #endif
            o.worldPos =  mul(unity_ObjectToWorld, v.vertex);
            return o;
        }

        float4 frag (v2f i) : SV_Target
        {
            //世界坐標(biāo)映射到顏色
            float dis = length(i.worldPos.xyz);
            float3 worldPos2 = i.worldPos.xyz/dis;
            worldPos2 = worldPos2 * 0.5 + 0.5;
            return float4(worldPos2,dis * 0.01);
        }
        ENDCG
    }

SubShader {
    Tags { "RenderType"="TransparentCutout" }
    ...
    }

SubShader {
    Tags { "RenderType"="Transparent" }
    ...
    }
}

保存該shader。接下來(lái)需要在Camera的OnPreRender中使用該shader替換得到一張RT。

public Camera depthCam;
private RenderTexture depthTexture;
private Texture2D texture2D;

private void OnPreRender()
{
    if (depthCam == null) return;
    if (depthTexture)
     {
        RenderTexture.ReleaseTemporary(depthTexture);
        depthTexture = null;
     }
    depthCam.CopyFrom(Camera.main);
    depthTexture = RenderTexture.GetTemporary(Camera.main.pixelWidth, Camera.main.pixelHeight, 32, RenderTextureFormat.ARGB32);
    depthCam.backgroundColor = new Color(0, 0, 0, 0);
    depthCam.clearFlags = CameraClearFlags.SolidColor;
    depthCam.targetTexture = depthTexture;
    depthCam.RenderWithShader(shader, "RenderType");//替換shader,獲取rt

    int width = depthTexture.width;
    int height = depthTexture.height;
    texture2D = new Texture2D(width, height, TextureFormat.ARGB32, false);//屏幕中心的顏色
    RenderTexture temp = RenderTexture.active;
    RenderTexture.active = depthTexture;
    texture2D.ReadPixels(new Rect(0, 0, width, height), 0, 0);
    texture2D.Apply();
    RenderTexture.active = temp;
    Color color = texture2D.GetPixel(width / 2, height / 2);//這里采樣為中心點(diǎn)
   //逆運(yùn)算得到世界坐標(biāo)
    Vector3 w = new Vector3(color.r, color.g, color.b);
    float l = color.a * 100f;
    w.x = (w.x - 0.5f) * 2 * l;
    w.y = (w.y - 0.5f) * 2 * l;
    w.z = (w.z - 0.5f) * 2 * l;
    Debug.Log(w);
}
頂點(diǎn)作為顏色輸出得到的RenderTexture
Editor下截圖

最后輸出的就是屏幕中心頂點(diǎn)的世界坐標(biāo),當(dāng)然還可以改成鼠標(biāo)點(diǎn)擊的位置。

小結(jié)

使用該方法獲取到的世界坐標(biāo)的位置并不是非常準(zhǔn)確,大部分時(shí)候都是正確的,但是有時(shí)候會(huì)有一點(diǎn)偏差,筆者猜測(cè)是精度導(dǎo)致的問(wèn)題,目前還沒(méi)有確定是哪里導(dǎo)致的。另外對(duì)于使用了法線貼圖、視差貼圖或者其他在shader中導(dǎo)致視覺(jué)位置改變的材質(zhì),一樣會(huì)產(chǎn)生偏移,這個(gè)也是要注意的。因?yàn)楣P者項(xiàng)目的原因,有一點(diǎn)偏差,最后再通過(guò)手動(dòng)微調(diào)也是可以接受的。如果哪位大佬還有更好的方式獲取屏幕頂點(diǎn),也請(qǐng)留言告知,不勝感激。
最后給出github的地址https://github.com/eangulee/Color2Pos。
好了,準(zhǔn)備下班了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容