Unity RenderTexture實現 刮彩票、橡皮擦、擦除效果(3D物體)

一、實現效果:

  1. 類似刮刮樂的擦除效果
  2. 支持多筆擦除(一次擦不乾淨)
    在這裏插入圖片描述

二、所用技術點:

  1. RenderTexture
  2. Shader

三、實現原理:

一個相機單獨渲染筆刷軌跡到RenderTexture上,在通過RenderTexture中的筆刷路徑修改原圖中對uv的像素點的alpha值實現透明或者半透明

1. Camera渲染到RenderTexture上:

a. 在場景中新建Camera並將ClearFlag設置爲Don't Clear,目的是將渲染的物體連成軌跡。
b. 設置渲染層,只渲染筆刷(筆刷是一個球),筆刷根據鼠標位置移動即可。
c. 調整相機位置,使得要擦除的區域在整個視錐體內,也可以設置成正交投影。
d. 新建RenderTexture並掛載到相機上,相機設置爲非激活狀態(通過代碼代碼Camera.Render())進行渲染控制。因爲只記錄路徑,所以只創建一個R8RenderTexture就可以
ps:需要關閉相機的垂直同步(MSAA),否則會將RenderTexture翻轉渲染。
設置效果如下圖:
在這裏插入圖片描述

2. 通過RenderTexture修改原圖片透明度(shader實現)

a. 通過相機的矩陣將RenderTexture變換到像素座標系
b. 修改對應uv的原像素點的alpha
shader代碼:

Shader "Learning/guacaipiao"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		// 相機渲染的RenderTexture
		_BlitTex ("BlitTexture", 2D) = "white" {}
	}
	SubShader
	{
		Tags{"Queue" = "AlphaTest" "IgnoreProjector" = "True" "RenderType" = "TransparentCutout"}
        Cull Off
        Pass
        {
            Tags{"LightMode" = "ForwardBase"}
            
            // 開啓alpah混合
            Blend SrcAlpha OneMinusSrcAlpha
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"
            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
			sampler2D _BlitTex;
			
            struct a2v
            {
                float4 vertex : POSITION;
                float4 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float2 uv : TEXCOORD2;
				float4 paintPos : TEXCOORD3;
            };

			// 相機的投影矩陣
			// C#中通過SetMatrix傳入
			// material.SetMatrix("paintCameraVP", camera.nonJitteredProjectionMatrix * camera.worldToCameraMatrix);
			float4x4 paintCameraVP;
			
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                
                // 下面三行是通過投影矩陣將頂點變換到像素座標系中([0, 1])
				float4 paintPos = mul(paintCameraVP, mul(unity_ObjectToWorld, v.vertex));
				paintPos /= paintPos.w; // 除以w分量,如果是相機正交投影可以省略
				o.paintPos.xy = paintPos.xy * 0.5 + 0.5; // 將[-1, 1] 變換到 [0, 1]
                return o;
            }

            fixed4 frag(v2f i) : SV_TARGET0
            {
                fixed4 texcolor = tex2D(_MainTex,i.uv);
                // 劃過的軌跡r值爲1,所以1 - r作爲原圖片的alpha值輸出
				float mask = tex2D(_BlitTex, i.paintPos).r;
				return fixed4(texcolor.rgb, 1 - mask);
            }
            ENDCG
		}
	}
}

C#代碼

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GuaCaiPiaoSub : MonoBehaviour {

	public Camera rtCamera;
	public Transform brush;
	RenderTexture renderTexture;
	public Material renderMaterial;

	void Start () {
		renderTexture = rtCamera.targetTexture;
		renderMaterial.SetTexture("BlitTex", renderTexture);
		renderMaterial.SetMatrix("paintCameraVP", rtCamera.nonJitteredProjectionMatrix * rtCamera.worldToCameraMatrix);
	}

	void OnMouseDrag(){
		Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
		RaycastHit hitInfo;
		if (Physics.Raycast(ray, out hitInfo)){
			brush.position = hitInfo.point;
			rtCamera.Render();
		}
	}
}

到這一步的實現效果:
在這裏插入圖片描述
很容易看出,滑動慢的時候可以連成一條線,但是快速滑動時候就變成了分開的點了。爲避免這種情況出現就是把相鄰兩幀的點連接起來,再進行渲染。下面就要說要優化效果相關的了。


四、效果優化

1. 筆刷改用LineRenderer

記錄上一幀鼠標的位置,跟當前幀連線,繪製好LineRenderer後在進行渲染,這樣就算兩幀的點間隔大,也可以繪製兩點的連線。另外可以通過調整LineRenderder寬度來調整筆刷大小。效果如下:
在這裏插入圖片描述
只需要改C#代碼,代碼如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GuaCaiPiaoSub : MonoBehaviour {

	public Camera rtCamera;
	public LineRenderer lineBrush;
	RenderTexture renderTexture;
	public Material renderMaterial;

	void Start () {
		renderTexture = rtCamera.targetTexture;
		renderMaterial.SetTexture("BlitTex", renderTexture);
		renderMaterial.SetMatrix("paintCameraVP", rtCamera.nonJitteredProjectionMatrix * rtCamera.worldToCameraMatrix);
	}

	Vector3 prePos = Vector3.one * 10000;
	Vector3[] linePosArr = new Vector3[2];
	void OnMouseDrag(){
		Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
		RaycastHit hitInfo;
		if (Physics.Raycast(ray, out hitInfo)){
			if (prePos == Vector3.one * 10000) {
				prePos = hitInfo.point;
			}
			lineBrush.positionCount = 2;
			linePosArr[0] = prePos;
			linePosArr[1] = hitInfo.point;
			lineBrush.SetPositions(linePosArr);
			lineBrush.startWidth = 1f;
			lineBrush.endWidth = 1f;
			rtCamera.Render();
			prePos = hitInfo.point;
		}
	}
	
	void OnMouseUp(){
		prePos = Vector3.one * 10000;
	}
}

其中void OnMouseUp(){ prePos = Vector3.one * 10000; }是爲了防止下次繪畫時,跟上一幀點關聯。

至此,基本的效果已經完成,大體已經可以滿足刮彩票效果的需求。
但是需求是不斷改變的,如果想要擦玻璃的效果,同一個地方擦多次才能擦得乾淨,這就需要下面的做法了。例如文章開頭的效果。

2. 多次擦除

多次擦除首先想到疊加,但是渲染的r值只有10,這樣如何做到疊加呢,這時候就需要另外兩張RenderTexture來做混合:CurrentRTPrevirousRT分別是當前幀渲染的RT和上一幀渲染的RT,求出茶之後,將差值和要渲染的RenderTexture進行混合,然後作爲最終應用到物體上。
實現混合需要使用一個接口:Graphics.Blit();,具體使用方式可以看unity的api
混合的shader代碼:

Shader "Learning/blit"
{
	Properties
	{
		_BrushStrength ("BrushStrength", int) = 1
	}
	SubShader
	{
		Cull Off ZWrite Off ZTest Always

		// 設置混合模式
		Blend One One

		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 vertex : SV_POSITION;
			};

			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = v.uv;
				return o;
			}
			
			sampler2D _CurrentRT; // 當前幀rt
			sampler2D _PrevirousRT; // 上一幀rt
			int _BrushStrength; // 筆刷強度 (需要幾次擦乾淨)

			fixed4 frag (v2f i) : SV_Target
			{
				// 計算兩幀的差值,輸出的的值跟物體的rt進行混合
				fixed4 cur = tex2D(_CurrentRT, i.uv);
				fixed4 pre = tex2D(_PrevirousRT, i.uv);
				float r = step(0.5, cur.r - pre.r); // cg的內置setp函數 大於0.5爲1,小於0.5爲0
				return fixed4(r / _BrushStrength, 0, 0, 1);
			}
			ENDCG
		}
	}
}

C#代碼

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GuaCaiPiao : MonoBehaviour {

	public Camera rtCamera;
	RenderTexture renderTexture;
	public RenderTexture currentRT;
	public RenderTexture previrousRT;
	public Material blitMaterial;
	public Material renderMaterial;

	public LineRenderer lineBrush;

	[Range(1.0f, 5.0f)]
	public float brushWidth = 1.0f;

	void Start () {
		renderTexture = rtCamera.targetTexture;
		renderMaterial.SetTexture("BlitTex", renderTexture);
		renderMaterial.SetMatrix("paintCameraVP", rtCamera.nonJitteredProjectionMatrix * rtCamera.worldToCameraMatrix);
		blitMaterial.SetTexture("_CurrentRT", currentRT);
		blitMaterial.SetTexture("_PrevirousRT", previrousRT);
	}

	void OnMouseDown(){
		// 每次按鈕要清空兩張rt
		rtCamera.clearFlags = CameraClearFlags.Color;
		rtCamera.backgroundColor = Color.black;
		rtCamera.targetTexture = previrousRT;
		rtCamera.Render();
		rtCamera.targetTexture = currentRT;
		rtCamera.Render();
		rtCamera.clearFlags = CameraClearFlags.Nothing;
	}

	Vector3 prePos = Vector3.one * 10000;
	Vector3[] linePosArr = new Vector3[2];
	void OnMouseDrag(){
		Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
		RaycastHit hitInfo;
		if (Physics.Raycast(ray, out hitInfo)){
			if (prePos == Vector3.one * 10000) {
				prePos = hitInfo.point;
			}
			lineBrush.positionCount = 2;
			linePosArr[0] = prePos;
			linePosArr[1] = hitInfo.point;
			lineBrush.SetPositions(linePosArr);
			lineBrush.startWidth = brushWidth;
			lineBrush.endWidth = brushWidth;
			rtCamera.Render();
			// 將當前幀和上一幀差值混合到 renderTexture,具體混合的實現看shader
			// 混合的計算方式爲 blitMaterial 上的 shader 中的計算
			Graphics.Blit(currentRT, renderTexture, blitMaterial);
			// 上一幀的rt替換爲當前幀渲染的rt,爲下一幀計算做準備
			Graphics.Blit(currentRT, previrousRT);
			prePos = hitInfo.point;
		}
	}

	void OnMouseUp(){
		prePos = Vector3.one * 10000;
	}

	void OnGUI(){
		if (GUI.Button(new Rect(0, 0, 80, 30), "RESET")){
			lineBrush.positionCount = 2;
			linePosArr[0] = Vector3.one * 10000;
			linePosArr[1] = Vector3.one * 10000;
			lineBrush.SetPositions(linePosArr);
			rtCamera.clearFlags = CameraClearFlags.Color;
			rtCamera.backgroundColor = Color.black;
			rtCamera.targetTexture = renderTexture;
			rtCamera.Render();
			rtCamera.targetTexture = previrousRT;
			rtCamera.Render();
			rtCamera.targetTexture = currentRT;
			rtCamera.Render();
			rtCamera.clearFlags = CameraClearFlags.Nothing;
		}
	}
}

實現效果及混合的演示:
在這裏插入圖片描述
左邊黑色是爲了觀察混合後的RenderTexture的實時演示,右邊爲具體最終效果。具體需要擦除幾次變乾淨調整blitMaterialBrushStrength屬性。

以上爲本片博客整體內容,主要應用爲 3D 物體,UI可以類比進行實現。
具體項目可以看我的 github工程
喜歡shader的朋友可以看我的GitHub中ShaderProject

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章