前言

  • 有段时间没有更新博客了,不知道应该写些什么,太简单感觉没有记录的必要,太难自己都没能理解,不知道如何下手。回归初心,记录自己想记录的东西。
  • 需要实现一个白板绘画的功能,可以使用LineRenderer或者GL,但是都被我舍弃了,我想同时实现笔刷功能,以上两种方法都不合适,于是我选择了用材质渲染到RenderTexture上,用来记录绘画的痕迹。
  • 之前已经在ue4中,实现了一个类似的功能,现在准备在unity上画在一个白板上,如果想在3D物体上涂鸦,就参考之前的博客:UE4快速实现涂鸦功能

思路

有之前的demo作为参考,我们基本上已经确定了实现白板绘画的可能性。我们需要做的就是利用
Graphics.Blit函数,将笔刷纹理、颜色绘制到一张RenderTexture保存下来,并重复利用,就能完整保存下来自己的绘画痕迹。

笔刷Shader

除了上面的Graphics.Blit函数,最核心的就是这个shader了,里面就是将之前的Texture与最新的笔刷已经纹理再混合成一张新的图片。注释写得比较随意,看看就好。

Shader "Unlit/PaintBrush"
{
    Properties
    {
        //之前的Texture
        _MainTex ("Texture", 2D) = "white" {}
        //笔刷纹理
        _BrushTex("Brush Texture",2D)= "white" {}
        //笔刷颜色
        _Color("Color",Color)=(1,1,1,1)
        //最新绘制笔刷的位置
        _UV("UV",Vector)=(0,0,0,0)
        //笔刷的大小
        _Size("Size",Range(1,1000))=1
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" }
        LOD 100
        //开启深度测试 关闭剔除...
        ZTest Always Cull Off ZWrite Off Fog{ Mode Off }
        //半透明混合
        Blend SrcAlpha OneMinusSrcAlpha
        //Blend One DstColor
        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;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _BrushTex;
            fixed4 _UV;
            float _Size;
            fixed4 _Color;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                //将笔刷的中心移动到整个纹理的中心
                float size = _Size;
                float2 uv = i.uv + (0.5f/size);
                //计算动态的绘画的位置
                uv = uv - _UV.xy;
                //放大uv->缩小纹理
                uv *= size;
                fixed4 col = tex2D(_BrushTex,uv);
                //去掉原来的颜色
                //我这里基本上都是取rng图片做的笔刷
                col.rgb = 1;
                //*上笔刷的颜色
                col *= _Color;
                return col;
            }
            ENDCG
        }
    }
}

功能实现

我们在一个白板上去画线,比在模型上用射线取模型uv的值应该更好理解了,我们只需要获取鼠标的位置计算与屏幕宽高的占比就是对应了图片的uv值。

    //画点
    private void Paint(Vector2 point)
    {
        if (point.x < 0 || point.x > _screenWidth || point.y < 0 || point.y > _screenHeight)
            return;

        Vector2 uv = new Vector2(point.x / (float)_screenWidth,
            point.y / (float)_screenHeight);
        _paintBrushMat.SetVector("_UV", uv);
        Graphics.Blit(_renderTex, _renderTex, _paintBrushMat);
    }

注意事项

  • 如果你在update获取的鼠标移动过快,两个点的距离太大会导致绘画不连续,这里就需要插值绘制,我这里的做法不太严谨,有需要可以自己重新写插值算法。
 //插点
    private void LerpPaint(Vector2 point)
    {
        Paint(point);

        if (_lastPoint == Vector2.zero)
        {
            _lastPoint = point;
            return;
        }

        float dis = Vector2.Distance(point, _lastPoint);
        if (dis > _brushLerpSize)
        {
            Vector2 dir = (point - _lastPoint).normalized;
            int num = (int)(dis / _brushLerpSize);
            for (int i = 0; i < num; i++)
            {
                Vector2 newPoint = _lastPoint + dir * (i + 1) * _brushLerpSize;
                Paint(newPoint);
            }
        }
        _lastPoint = point;
    }
  • 因为我们使用到了RenderTexture,unity好像会将RenderTexture缓存下来以便下次的快速调用,但是这就有一个新的问题,每次我们重新运行的时候,RenderTexture可能还会保留上次的内容,这时候,我们就可以在最开始的时候,将RenderTexture的内容全部清除掉。
Shader "Unlit/ClearBrush"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        ZTest Always Cull Off ZWrite Off Fog{ Mode Off }
        Blend One DstColor
        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;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                col = 0;
                return col;
            }
            ENDCG
        }
    }
}

完整代码

//-----------------------------------------------------------------------
// <copyright file="Test.cs" company="Codingworks Game Development">
//     Copyright (c) codingworks. All rights reserved.
// </copyright>
// <author> codingworks </author>
// <email> coding2233@163.com </email>
// <time> #CREATETIME# </time>
//-----------------------------------------------------------------------

using UnityEngine;
using UnityEngine.UI;

public class Paint : MonoBehaviour
{
    private Vector2 _lastPoint;

    [SerializeField] private Material _clearBrushMat;

    [SerializeField] private Material _paintBrushMat;

    private RenderTexture _renderTex;

    private int ScreenWidth, ScreenHeight;

    [SerializeField] private RawImage _rawImage;

    private float _paintLerpSize;


    // Use this for initialization
    private void Start()
    {
        ScreenWidth = Screen.width;
        ScreenHeight = Screen.height;

        var brushSize = _paintBrushMat.GetFloat("_Size");
        float brushTexWidth = _paintBrushMat.GetTexture("_BrushTex").width;
        _paintLerpSize = brushTexWidth / brushSize;

        _renderTex = RenderTexture.GetTemporary(ScreenWidth, ScreenHeight, 24);

        Graphics.Blit(null, _renderTex, _clearBrushMat);

        _rawImage.texture = _renderTex;
    }

    // Update is called once per frame
    private void Update()
    {
        if (_renderTex && _paintBrushMat)
        {
            if (Input.GetMouseButton(0))
                LerpPaint(Input.mousePosition);

            if (Input.GetMouseButtonUp(0))
                _lastPoint = Vector2.zero;
        }
    }


    private void LerpPaint(Vector2 point)
    {
        Paint(point);

        if (_lastPoint == Vector2.zero)
        {
            _lastPoint = point;
            return;
        }

        var dis = Vector2.Distance(point, _lastPoint);
        if (dis > _paintLerpSize)
        {
            var dir = (point - _lastPoint).normalized;
            var num = (int) (dis / _paintLerpSize);
            for (var i = 0; i < num; i++)
            {
                var newPoint = _lastPoint + dir * (i + 1) * _paintLerpSize;
                Paint(newPoint);
            }
        }
        _lastPoint = point;
    }

    /// <summary>
    ///     绘画
    /// </summary>
    /// <param name="point">鼠标的位置</param>
    private void Paint(Vector2 point)
    {
        if (point.x < 0 || point.x > ScreenWidth || point.y < 0 || point.y > ScreenHeight)
            return;

        var uv = new Vector2(point.x / ScreenWidth,
            point.y / ScreenHeight);
        _paintBrushMat.SetVector("_UV", uv);
        Graphics.Blit(_renderTex, _renderTex, _paintBrushMat);
    }
}

截图展示

这里写图片描述

未完成

  1. 橡皮擦还没做,最好参照笔刷shader,再单独写一个橡皮擦的shader
  2. 颜色如果能做成一个面板,能随意选择颜色
  3. 本来就只是一个demo,就不要求太多…

总结

  1. 作为一个程序员,绘画和写字就不要吐槽了
  2. 整篇博客下来,自己都有一种不知所云的感觉,思维太飘了
  3. 有兴趣的同学,直接看整个工程吧

源码链接

https://github.com/coding2233/UnityPaint

Logo

致力于链接即构和开发者,提供实时互动和元宇宙领域的前沿洞察、技术分享和丰富的开发者活动,共建实时互动世界。

更多推荐