Unity鏡子效果的實現(無需鏡子Shader)


Unity鏡子效果制作教程


本文提供全流程,中文翻譯。

Chinar 堅持將簡單的生活方式,帶給世人!

(擁有更好的閱讀體驗 —— 高分辨率用戶請根據需求調整網頁縮放比例)



Chinar —— 心分享、心創新!

助力快速實現一個簡單的鏡面反射效果

為新手節省寶貴的時間,避免采坑!


Chinar 教程效果:
這里寫圖片描述



全文高清圖片,點擊即可放大觀看 (很多人竟然不知道)


1

Create Mirror —— 創建鏡子


本教程,無需自己找鏡子Shader,只需2個腳本即可在Unity中創建一個簡單的模擬鏡面反射效果

1. 在場景中創建一個 Plane —— 用來作為鏡子

2. 同時創建一個材質球 /Material —— 給到 Plane

3. 修改新創建的 Material Shader Unlit/Texture

舉個栗子黑白88
這里寫圖片描述


2

Create Camera —— 創建一個新相機


1. 新建一個 Render Texture(我改名為 Plane 便於區分和理解)

2. 右鍵 層次列表/Hierarchy —— 創建一個新的 Camera

3. 將新建的 Render Texture(Plane)給新建的 Camera 組件中的 Target Texture

4. 給新建的 Camera相機,添加腳本 ChinarMirrorPlane

並將 Main Camera Plane 拖到 Inspector 面板中對應的屬性里

5. 給新建的 Camera相機,添加腳本 ChinarMirror ,並將 Plane 拖至 Inspector 面板中

注意: 一定要修改 Plane 材質的屬性為:
這里寫圖片描述
具體流程其實很簡單,如下
舉個栗子黑白88
這里寫圖片描述
兩個腳本,都需要掛載到 Camera

using UnityEngine;


/// <summary>
/// 鏡子管理腳本 —— 掛在新建的Camera上
/// </summary>
[ExecuteInEditMode]
public class ChinarMirror : MonoBehaviour
{
    public  GameObject mirrorPlane;  //鏡子
    public  Camera     mainCamera;   //主攝像機
    private Camera     mirrorCamera; //鏡像攝像機


    private void Start()
    {
        mirrorCamera = GetComponent<Camera>();
    }


    private void Update()
    {
        if (null == mirrorPlane || null == mirrorCamera || null == mainCamera) return;
        Vector3 postionInMirrorSpace    = mirrorPlane.transform.InverseTransformPoint(mainCamera.transform.position); //將主攝像機的世界坐標位置轉換為鏡子的局部坐標位置
        postionInMirrorSpace.y          = -postionInMirrorSpace.y;                                                    //一般y為鏡面的法線方向
        mirrorCamera.transform.position = mirrorPlane.transform.TransformPoint(postionInMirrorSpace);                 //轉回到世界坐標系的位置
    }
}
using UnityEngine;

/// <summary>
/// Plane管理腳本 —— 掛載新建的Camera上
/// </summary>
[ExecuteInEditMode] //編輯模式中執行
public class ChinarMirrorPlane : MonoBehaviour
{
    public  GameObject mirrorPlane; //鏡子Plane
    public  bool       estimateViewFrustum    = true;
    public  bool       setNearClipPlane       = true;   //是否設置近剪切平面
    public  float      nearClipDistanceOffset = -0.01f; //近剪切平面的距離
    private Camera     mirrorCamera;                    //鏡像攝像機
    private Vector3    vn;                              //屏幕的法線
    private float      l;                               //到屏幕左邊緣的距離
    private float      r;                               //到屏幕右邊緣的距離
    private float      b;                               //到屏幕下邊緣的距離
    private float      t;                               //到屏幕上邊緣的距離
    private float      d;                               //從鏡像攝像機到屏幕的距離
    private float      n;                               //鏡像攝像機的近剪切面的距離
    private float      f;                               //鏡像攝像機的遠剪切面的距離
    private Vector3    pa;                              //世界坐標系的左下角
    private Vector3    pb;                              //世界坐標系的右下角
    private Vector3    pc;                              //世界坐標系的左上角
    private Vector3    pe;                              //鏡像觀察角度的世界坐標位置
    private Vector3    va;                              //從鏡像攝像機到左下角
    private Vector3    vb;                              //從鏡像攝像機到右下角
    private Vector3    vc;                              //從鏡像攝像機到左上角
    private Vector3    vr;                              //屏幕的右側旋轉軸
    private Vector3    vu;                              //屏幕的上側旋轉軸
    private Matrix4x4  p  = new Matrix4x4();
    private Matrix4x4  rm = new Matrix4x4();
    private Matrix4x4  tm = new Matrix4x4();
    private Quaternion q  = new Quaternion();


    private void Start()
    {
        mirrorCamera = GetComponent<Camera>();
    }


    private void Update()
    {
        if (null == mirrorPlane || null == mirrorCamera) return;
        pa = mirrorPlane.transform.TransformPoint(new Vector3(-5.0f, 0.0f, -5.0f)); //世界坐標系的左下角
        pb = mirrorPlane.transform.TransformPoint(new Vector3(5.0f,  0.0f, -5.0f)); //世界坐標系的右下角
        pc = mirrorPlane.transform.TransformPoint(new Vector3(-5.0f, 0.0f, 5.0f));  //世界坐標系的左上角
        pe = transform.position;                                                    //鏡像觀察角度的世界坐標位置
        n  = mirrorCamera.nearClipPlane;                                            //鏡像攝像機的近剪切面的距離
        f  = mirrorCamera.farClipPlane;                                             //鏡像攝像機的遠剪切面的距離
        va = pa - pe;                                                               //從鏡像攝像機到左下角
        vb = pb - pe;                                                               //從鏡像攝像機到右下角
        vc = pc - pe;                                                               //從鏡像攝像機到左上角
        vr = pb - pa;                                                               //屏幕的右側旋轉軸
        vu = pc - pa;                                                               //屏幕的上側旋轉軸
        if (Vector3.Dot(-Vector3.Cross(va, vc), vb) < 0.0f)                         //如果看向鏡子的背面
        {
            vu = -vu;
            pa = pc;
            pb = pa + vr;
            pc = pa + vu;
            va = pa - pe;
            vb = pb - pe;
            vc = pc - pe;
        }
        vr.Normalize();
        vu.Normalize();
        vn = -Vector3.Cross(vr, vu); //兩個向量的叉乘,最后在取負,因為Unity是使用左手坐標系
        vn.Normalize();
        d = -Vector3.Dot(va, vn);
        if (setNearClipPlane)
        {
            n                          = d + nearClipDistanceOffset;
            mirrorCamera.nearClipPlane = n;
        }
        l = Vector3.Dot(vr, va) * n / d;
        r = Vector3.Dot(vr, vb) * n / d;
        b = Vector3.Dot(vu, va) * n / d;
        t = Vector3.Dot(vu, vc) * n / d;


        //投影矩陣
        p[0, 0] = 2.0f * n / (r - l);
        p[0, 1] = 0.0f;
        p[0, 2] = (r + l) / (r - l);
        p[0, 3] = 0.0f;

        p[1, 0] = 0.0f;
        p[1, 1] = 2.0f * n / (t - b);
        p[1, 2] = (t + b) / (t - b);
        p[1, 3] = 0.0f;

        p[2, 0] = 0.0f;
        p[2, 1] = 0.0f;
        p[2, 2] = (f + n) / (n - f);
        p[2, 3] = 2.0f * f * n / (n - f);

        p[3, 0] = 0.0f;
        p[3, 1] = 0.0f;
        p[3, 2] = -1.0f;
        p[3, 3] = 0.0f;

        //旋轉矩陣
        rm[0, 0] = vr.x;
        rm[0, 1] = vr.y;
        rm[0, 2] = vr.z;
        rm[0, 3] = 0.0f;

        rm[1, 0] = vu.x;
        rm[1, 1] = vu.y;
        rm[1, 2] = vu.z;
        rm[1, 3] = 0.0f;

        rm[2, 0] = vn.x;
        rm[2, 1] = vn.y;
        rm[2, 2] = vn.z;
        rm[2, 3] = 0.0f;

        rm[3, 0] = 0.0f;
        rm[3, 1] = 0.0f;
        rm[3, 2] = 0.0f;
        rm[3, 3] = 1.0f;

        tm[0, 0] = 1.0f;
        tm[0, 1] = 0.0f;
        tm[0, 2] = 0.0f;
        tm[0, 3] = -pe.x;

        tm[1, 0] = 0.0f;
        tm[1, 1] = 1.0f;
        tm[1, 2] = 0.0f;
        tm[1, 3] = -pe.y;

        tm[2, 0] = 0.0f;
        tm[2, 1] = 0.0f;
        tm[2, 2] = 1.0f;
        tm[2, 3] = -pe.z;

        tm[3, 0] = 0.0f;
        tm[3, 1] = 0.0f;
        tm[3, 2] = 0.0f;
        tm[3, 3] = 1.0f;


        mirrorCamera.projectionMatrix    = p; //矩陣組
        mirrorCamera.worldToCameraMatrix = rm * tm;
        if (!estimateViewFrustum) return;
        q.SetLookRotation((0.5f * (pb + pc) - pe), vu); //旋轉攝像機
        mirrorCamera.transform.rotation = q;            //聚焦到屏幕的中心點

        //估值 —— 三目簡寫
        mirrorCamera.fieldOfView = mirrorCamera.aspect >= 1.0 ? Mathf.Rad2Deg * Mathf.Atan(((pb - pa).magnitude + (pc - pa).magnitude) / va.magnitude) : Mathf.Rad2Deg / mirrorCamera.aspect * Mathf.Atan(((pb - pa).magnitude + (pc - pa).magnitude) / va.magnitude);
        //在攝像機角度考慮,保證視錐足夠寬
    }
}

3

Main Camera —— 主相機腳本(方便看到測試效果)


為了方便看到運行后的鏡面效果, Chinar 在這里提供了一個第三人稱的腳本

用於轉鏡頭,看不同方位

需要掛載到主相機上,並將層次列表中的 Plane 拖到 Pivot
舉個栗子黑白88

using UnityEngine;

/// <summary>
/// 主相機腳本 —— 掛載到主相機上,並 層次列表中的 Plane 拖到 pivot 
/// </summary>
public class ChinarCamera : MonoBehaviour
{
    public  Transform pivot;
    public  Vector3   pivotOffset = Vector3.zero;
    public  Transform target;
    public  float     distance       = 10.0f;
    public  float     minDistance    = 2f;
    public  float     maxDistance    = 15f;
    public  float     zoomSpeed      = 1f;
    public  float     xSpeed         = 250.0f;
    public  float     ySpeed         = 120.0f;
    public  bool      allowYTilt     = true;
    public  float     yMinLimit      = -90f;
    public  float     yMaxLimit      = 90f;
    private float     x              = 0.0f;
    private float     y              = 0.0f;
    private float     targetX        = 0f;
    private float     targetY        = 0f;
    private float     targetDistance = 0f;
    private float     xVelocity      = 1f;
    private float     yVelocity      = 1f;
    private float     zoomVelocity   = 1f;


    private void Start()
    {
        var angles     = transform.eulerAngles;
        targetX        = x = angles.x;
        targetY        = y = ClampAngle(angles.y, yMinLimit, yMaxLimit);
        targetDistance = distance;
    }


    private void LateUpdate()
    {
        if (!pivot) return;
        var scroll = Input.GetAxis("Mouse ScrollWheel");

        if (scroll > 0.0f) targetDistance -= zoomSpeed;
        else if (scroll < 0.0f)
            targetDistance += zoomSpeed;
        targetDistance     =  Mathf.Clamp(targetDistance, minDistance, maxDistance);
        if (Input.GetMouseButton(1) || (Input.GetMouseButton(0) && (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl))))
        {
            targetX += Input.GetAxis("Mouse X") * xSpeed * 0.02f;
            if (allowYTilt)
            {
                targetY -= Input.GetAxis("Mouse Y") * ySpeed * 0.02f;
                targetY =  ClampAngle(targetY, yMinLimit, yMaxLimit);
            }
        }
        x                   = Mathf.SmoothDampAngle(x,              targetX, ref xVelocity, 0.3f);
        y                   = allowYTilt ? Mathf.SmoothDampAngle(y, targetY, ref yVelocity, 0.3f) : targetY;
        Quaternion rotation = Quaternion.Euler(y, x, 0);
        distance            = Mathf.SmoothDamp(distance, targetDistance, ref zoomVelocity, 0.5f);
        Vector3 position    = rotation * new Vector3(0.0f, 0.0f, -distance) + pivot.position + pivotOffset;
        transform.rotation  = rotation;
        transform.position  = position;
    }


    private static float ClampAngle(float angle, float min, float max)
    {
        if (angle < -360) angle += 360;
        if (angle > 360) angle  -= 360;
        return Mathf.Clamp(angle, min, max);
    }
}

4

Create Cube —— 創建一個立方體


為了看鏡子的效果

在場景中創建一個 Cube —— 用來作為參照對象

然后點擊運行后,即可看到鏡子效果已經完成
舉個栗子黑白88
這里寫圖片描述
這里寫圖片描述


5

Indistinct —— 顯示效果不清晰


如果發現,鏡子的顯示效果並不清晰

這是因為我們創建的 Render Texture 時使用的是默認的分辨率 256*256

修改成較高的分辨率即可,這里我修改為:1024*1024 (可視情況自己設定)

注意:分辨率設置越高,是越耗性能的
舉個栗子黑白88
這里寫圖片描述
這里寫圖片描述

至此:鏡子的制作教程結束


6

Project —— 項目文件


項目文件為 unitypackage 文件包:

下載導入 Unity 即可使用
舉個栗子黑白88
點擊下載 —— 項目資源


支持

May Be —— 搞開發,總有一天要做的事!


擁有自己的服務器,無需再找攻略!

Chinar 提供一站式教程,閉眼式創建!

為新手節省寶貴時間,避免采坑!


先點擊領取 —— 阿里全產品優惠券 (享受最低優惠)


1 —— 雲服務器超全購買流程 (新手必備!)

2 —— 阿里ECS雲服務器自定義配置 - 購買教程(新手必備!)

3—— Windows 服務器配置、運行、建站一條龍 !

4 —— Linux 服務器配置、運行、建站一條龍 !





技術交流群:806091680 ! Chinar 歡迎你的加入


END

本博客為非營利性個人原創,除部分有明確署名的作品外,所刊登的所有作品的著作權均為本人所擁有,本人保留所有法定權利。違者必究

對於需要復制、轉載、鏈接和傳播博客文章或內容的,請及時和本博主進行聯系,留言,Email: ichinar@icloud.com

對於經本博主明確授權和許可使用文章及內容的,使用時請注明文章或內容出處並注明網址


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM