一个相机单独渲染笔刷轨迹到RenderTexture
上,在通过RenderTexture
中的笔刷路径修改原图中对uv的像素点的alpha
值实现透明或者半透明
a. 在场景中新建Camera
并将ClearFlag
设置为Don't Clear
,目的是将渲染的物体连成轨迹。
b. 设置渲染层,只渲染笔刷(笔刷是一个球),笔刷根据鼠标位置移动即可。
c. 调整相机位置,使得要擦除的区域在整个视锥体内,也可以设置成正交投影。
d. 新建RenderTexture
并挂载到相机上,相机设置为非激活状态(通过代码代码Camera.Render()
)进行渲染控制。因为只记录路径,所以只创建一个R8
的RenderTexture
就可以
ps:需要关闭相机的垂直同步(MSAA),否则会将RenderTexture
翻转渲染。
设置效果如下图:
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();
}
}
}
到这一步的实现效果:
很容易看出,滑动慢的时候可以连成一条线,但是快速滑动时候就变成了分开的点了。为避免这种情况出现就是把相邻两帧的点连接起来,再进行渲染。下面就要说要优化效果相关的了。
记录上一帧鼠标的位置,跟当前帧连线,绘制好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; }
是为了防止下次绘画时,跟上一帧点关联。
至此,基本的效果已经完成,大体已经可以满足刮彩票效果的需求。
但是需求是不断改变的,如果想要擦玻璃的效果,同一个地方擦多次才能擦得干净,这就需要下面的做法了。例如文章开头的效果。
多次擦除首先想到叠加,但是渲染的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