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

  • 28
    点赞
  • 113
    收藏
    觉得还不错? 一键收藏
  • 21
    评论
Unity实现毛笔签名和橡皮擦除的方法有很多种,下面是一种简单的实现方式: 首先,需要创建一个画布,用于绘制用户的签名。可以在场景中创建一个空物体,并添加一个RenderTexture组件,将其设置为画布的贴图,这样用户绘制的签名就可以在画布上显示出来。 接下来,可以创建一个脚本用于控制用户绘制的过程,比如取名为“SignatureController”。在脚本中,需要定义一些变量用于记录用户绘制时的状态和参数,比如笔刷大小、颜色、绘制速度等。同时,需要在Update()方法中监听用户输入,当用户按下鼠标或手指时,开始绘制;当用户移动鼠标或手指时,将签名绘制在画布上;当用户松开鼠标或手指时,结束绘制。 在开始绘制时,可以利用“LineRenderer“组件创建一条连续的线条,并将其添加到画布上。在绘制过程中,可以实现一些简单的笔刷特效,比如笔画粗细随着速度变化而变化、笔刷颜色带有一定的透明度等,这样能够更好地模拟真实的签字笔画效果。在绘制结束时,可以将绘制好的签名保存到本地或上传到服务器中,完成签字过程。 除了实现签字功能,还需要实现橡皮擦功能,这可以通过控制“LineRenderer”组件的宽度和颜色来实现,将笔刷颜色设置为背景色或透明色,这样就可以实现擦除效果。 总的来说,实现Unity的毛笔签名和橡皮擦除需要有一定的编程基础和技术积累,理解基本的UI元素的用法和渲染原理也是必要的。当我们有了这些技能后,通过实践和不断的尝试,就能够写出更加精美、流畅的签字程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值