一、實現效果:
- 類似刮刮樂的擦除效果
- 支持多筆擦除(一次擦不乾淨)
二、所用技術點:
- RenderTexture
- Shader
三、實現原理:
一個相機單獨渲染筆刷軌跡到RenderTexture
上,在通過RenderTexture
中的筆刷路徑修改原圖中對uv的像素點的alpha
值實現透明或者半透明
1. Camera渲染到RenderTexture上:
a. 在場景中新建Camera
並將ClearFlag
設置爲Don't Clear
,目的是將渲染的物體連成軌跡。
b. 設置渲染層,只渲染筆刷(筆刷是一個球),筆刷根據鼠標位置移動即可。
c. 調整相機位置,使得要擦除的區域在整個視錐體內,也可以設置成正交投影。
d. 新建RenderTexture
並掛載到相機上,相機設置爲非激活狀態(通過代碼代碼Camera.Render()
)進行渲染控制。因爲只記錄路徑,所以只創建一個R8
的RenderTexture
就可以
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
值只有1
和0
,這樣如何做到疊加呢,這時候就需要另外兩張RenderTexture
來做混合:CurrentRT
和PrevirousRT
分別是當前幀渲染的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
的實時演示,右邊爲具體最終效果。具體需要擦除幾次變乾淨調整blitMaterial
的BrushStrength
屬性。
以上爲本片博客整體內容,主要應用爲 3D 物體,UI可以類比進行實現。
具體項目可以看我的 github工程
喜歡shader的朋友可以看我的GitHub中ShaderProject