Chinar 堅持將簡單的生活方式,帶給世人! (擁有更好的閱讀體驗 —— 高分辨率用戶請根據需求調整網頁縮放比例) |
助力快速實現一個簡單的鏡面反射效果 為新手節省寶貴的時間,避免采坑! |
Chinar 教程效果:
1
Create Mirror —— 創建鏡子
本教程,無需自己找鏡子Shader,只需2個腳本即可在Unity中創建一個簡單的模擬鏡面反射效果
1. 在場景中創建一個 Plane —— 用來作為鏡子
2. 同時創建一個材質球 /Material —— 給到 Plane 上
3. 修改新創建的 Material 的 Shader 為 Unlit/Texture
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 材質的屬性為:
具體流程其實很簡單,如下
兩個腳本,都需要掛載到 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 上
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 —— 用來作為參照對象
然后點擊運行后,即可看到鏡子效果已經完成
5
Indistinct —— 顯示效果不清晰
如果發現,鏡子的顯示效果並不清晰
這是因為我們創建的 Render Texture 時使用的是默認的分辨率 256*256
修改成較高的分辨率即可,這里我修改為:1024*1024 (可視情況自己設定)
注意:分辨率設置越高,是越耗性能的
至此:鏡子的制作教程結束
6
Project —— 項目文件
項目文件為 unitypackage 文件包:
下載導入 Unity 即可使用
點擊下載 —— 項目資源
支持
May Be —— 搞開發,總有一天要做的事!
Chinar 提供一站式教程,閉眼式創建! 為新手節省寶貴時間,避免采坑! |
先點擊領取 —— 阿里全產品優惠券 (享受最低優惠)
1 —— 雲服務器超全購買流程 (新手必備!)
2 —— 阿里ECS雲服務器自定義配置 - 購買教程(新手必備!)
3—— Windows 服務器配置、運行、建站一條龍 !
4 —— Linux 服務器配置、運行、建站一條龍 !

技術交流群:806091680 ! Chinar 歡迎你的加入
本博客為非營利性個人原創,除部分有明確署名的作品外,所刊登的所有作品的著作權均為本人所擁有,本人保留所有法定權利。違者必究
對於需要復制、轉載、鏈接和傳播博客文章或內容的,請及時和本博主進行聯系,留言,Email: ichinar@icloud.com
對於經本博主明確授權和許可使用文章及內容的,使用時請注明文章或內容出處並注明網址