项目源码:后期发布
索引
- 图层
- 渲染绘画区域
- 图层Shader
- 编辑器
- 编辑模式
- 新建图层
- 设置当前图层
- 上、下移动图层
- 删除图层
- 图层快照
图层
在PS
中,图层
的概念贯穿始终(了解PS图层),他可以称作PS
最基础也是最强大的特性之一。
那么,在TextureShop
中,我们的第一个任务也即是设计并完成图层
的功能。
渲染绘画区域
接上篇,为了实现渲染绘画区域
的逻辑,我们添加一个控制变量:
/// <summary>
/// 图层
/// </summary>
public sealed class TextureLayer
{
//图层是否为脏的,将触发重新生成渲染源
private bool _isTextureDirty = false;
}
然后,在帧轮询方法OnUpdate
中,检测并重新生成渲染源:
/// <summary>
/// 图层
/// </summary>
public sealed class TextureLayer
{
/// <summary>
/// 图层更新
/// </summary>
public void OnUpdate()
{
//当图层改变时(被标记为脏的)
if (_isTextureDirty)
{
_isTextureDirty = false;
for (int x = 0; x < _texture.width; x++)
{
for (int y = 0; y < _texture.height; y++)
{
//通过偏移值(锚点),从绘画板中提取颜色,填充到_texture(渲染源)中
Color bColor = _plate.GetColor(x + Offset.x, y + Offset.y);
_texture.SetPixel(x, y, bColor);
}
}
_texture.Apply();
}
}
}
_isTextureDirty
变量的控制至关重要,它使得即便在同一帧多次改变了图层数据,也仅仅只会进行一次生成渲染源
,而不是图层每改变一次便触发一次生成渲染源
,那将带来极大的性能开销。
图层Shader
再者是图层持有的Shader TextureLayer.shader
,在TextureLayer
类的构造方法中,他已将渲染源
设置给了Shader:
_material.SetTexture("_PaintTex", _texture);
那么在TextureLayer.shader
中将是我们自行编写的渲染逻辑,只不过此时我们没有额外的处理,直接原封不动的输出了渲染源
中的颜色:
Shader "Hidden/TextureShop/TextureLayer"
{
Properties
{
[PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
[HideInInspector] _PaintTex("图层纹理", 2D) = "white" {}
}
SubShader
{
Tags
{
"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"
"PreviewType" = "Plane"
"CanUseSpriteAtlas" = "True"
}
Cull Off
Lighting Off
ZWrite Off
ZTest[unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#include "UnityCG.cginc"
#include "UnityUI.cginc"
struct VertData
{
float4 vertex : POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct FragData
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
UNITY_VERTEX_OUTPUT_STEREO
};
sampler2D _PaintTex;
FragData vert(VertData IN)
{
FragData OUT;
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.worldPosition = IN.vertex;
OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);
OUT.texcoord = IN.texcoord;
OUT.color = IN.color;
return OUT;
}
fixed4 frag(FragData IN) : SV_Target
{
//原封不动输出颜色
half4 color = tex2D(_PaintTex, IN.texcoord);
return color;
}
ENDCG
}
}
}
编辑器
我们需要定义一个最顶层的控制器类,也将作为我们整个程序的入口,TextureShop
的编辑器:
/// <summary>
/// TextureShop编辑器
/// </summary>
[DisallowMultipleComponent]
public sealed class TextureShopEditor : MonoBehaviour
{
}
回顾图层的尺寸等参数,所有的图层应当拥有一样的尺寸大小
,所以将他们加入到编辑器中,以公开到外部设置:
/// <summary>
/// TextureShop编辑器
/// </summary>
[DisallowMultipleComponent]
public sealed class TextureShopEditor : MonoBehaviour
{
/// <summary>
/// 绘画区域宽度
/// </summary>
public int PaintAreaWidth = 512;
/// <summary>
/// 绘画区域高度
/// </summary>
public int PaintAreaHeight = 512;
/// <summary>
/// 绘画板宽度
/// </summary>
public int PlateWidth = 2048;
/// <summary>
/// 绘画板高度
/// </summary>
public int PlateHeight = 2048;
}
编辑模式
编辑模式
目前暂定为如下几种:
编辑模式 | 描述 |
---|---|
无 | 不可进行任何编辑操作 |
移动 | 选中选区时,移动选区内容,否则移动绘画板内容 |
选取 | 以矩形方式选择选区 |
套索 | 以套索方式选择选区 |
魔术棒 | 以魔术棒方式选择选区 |
修剪 | 进行高自由度修剪选区 |
仿制图章 | 按住Alt锚定一片区域,然后在其他地方可复制该区域 |
画笔 | 使用鼠标进行绘画 |
橡皮擦 | 使用鼠标进行擦除 |
/// <summary>
/// TextureShop编辑器
/// </summary>
[DisallowMultipleComponent]
public sealed class TextureShopEditor : MonoBehaviour
{
/// <summary>
/// 编辑模式
/// </summary>
[SerializeField] private EditMode _mode = EditMode.None;
}
/// <summary>
/// 编辑模式
/// </summary>
public enum EditMode
{
/// <summary>
/// 无
/// </summary>
None,
/// <summary>
/// 移动
/// </summary>
Move,
/// <summary>
/// 选取
/// </summary>
Choose,
/// <summary>
/// 套索
/// </summary>
Noose,
/// <summary>
/// 魔术棒
/// </summary>
MagicWand,
/// <summary>
/// 修剪
/// </summary>
Trim,
/// <summary>
/// 仿制图章
/// </summary>
CloneStamp,
/// <summary>
/// 画笔
/// </summary>
Paint,
/// <summary>
/// 擦除
/// </summary>
Erase
}
新建图层
新建图层
时,传入尺寸、图像数据等参数(如果传入了图像,将该图像颜色数据载入新建的图层中,否则新建一个空图层):
/// <summary>
/// TextureShop编辑器
/// </summary>
[DisallowMultipleComponent]
public sealed class TextureShopEditor : MonoBehaviour
{
//图层渲染器放置的根节点
private RectTransform _textureLayerRoot;
/// <summary>
/// 所有的图层
/// </summary>
public List<TextureLayer> TextureLayers { get; private set; } = new List<TextureLayer>();
/// <summary>
/// 新建一个图层
/// </summary>
/// <param name="layerName">图层名称</param>
/// <param name="texture">图层图像</param>
/// <param name="offset">偏移值</param>
/// <returns>图层</returns>
public TextureLayer NewTextureLayer(string layerName, Texture2D texture, Vector2Int offset = default)
{
layerName = string.IsNullOrEmpty(layerName) ? "新建图层" : layerName;
TextureLayer textureLayer = new TextureLayer(layerName, PaintAreaWidth, PaintAreaHeight, PlateWidth, PlateHeight, _textureLayerRoot, texture, offset);
TextureLayers.Add(textureLayer);
return textureLayer;
}
}
设置当前图层
只有当前图层
不为空,才能进一步编辑当前图层:
/// <summary>
/// TextureShop编辑器
/// </summary>
[DisallowMultipleComponent]
public sealed class TextureShopEditor : MonoBehaviour
{
/// <summary>
/// 当前选中的图层
/// </summary>
public TextureLayer CurrentLayer { get; private set; }
/// <summary>
/// 设置当前激活的图层
/// </summary>
/// <param name="layer">图层</param>
public void SetCurrentLayer(TextureLayer layer)
{
if (CurrentLayer == layer)
return;
CurrentLayer = layer;
}
}
上、下移动图层
图层越靠下(在图层列表中越靠后),其显示层级越高,也即是能在视觉上挡住之前的图层:
/// <summary>
/// TextureShop编辑器
/// </summary>
[DisallowMultipleComponent]
public sealed class TextureShopEditor : MonoBehaviour
{
/// <summary>
/// 向上移动当前图层(显示层级降低)
/// </summary>
public void MoveLayerUpwards()
{
if (CurrentLayer != null)
{
int index = TextureLayers.IndexOf(CurrentLayer);
if (index > 0)
{
index -= 1;
TextureLayers.Remove(CurrentLayer);
TextureLayers.Insert(index, CurrentLayer);
//移动图层渲染器实体,改变其在父级中的位置
CurrentLayer.Entity.transform.SetSiblingIndex(index);
}
}
}
/// <summary>
/// 向下移动当前图层(显示层级升高)
/// </summary>
public void MoveLayerDownwards()
{
if (CurrentLayer != null)
{
int index = TextureLayers.IndexOf(CurrentLayer);
if (index < TextureLayers.Count - 1)
{
index += 1;
TextureLayers.Remove(CurrentLayer);
TextureLayers.Insert(index, CurrentLayer);
CurrentLayer.Entity.transform.SetSiblingIndex(index);
}
}
}
}
删除图层
/// <summary>
/// TextureShop编辑器
/// </summary>
[DisallowMultipleComponent]
public sealed class TextureShopEditor : MonoBehaviour
{
/// <summary>
/// 删除一个图层
/// </summary>
/// <param name="layer">图层</param>
public void DeleteTextureLayer(TextureLayer layer)
{
if (layer == null)
return;
if (CurrentLayer == layer)
{
SetCurrentLayer(null);
}
layer.Dispose();
TextureLayers.Remove(layer);
}
}
图层快照
在进行某些操作时,如果用户未点击确认
,而是点击了取消
,则之前的操作将被还原,如下:
为了实现这个功能,我们引入图层快照
(当然,更高级的应当引入命令模式,但这不在本系列的学习范畴内):
/// <summary>
/// 图层快照
/// </summary>
public sealed class TextureLayerSnapshot
{
internal Plate _plate;
internal Plate _region;
internal Vector2Int _offset;
/// <summary>
/// 图层快照
/// </summary>
public TextureLayerSnapshot(int paintAreaWidth, int paintAreaHeight, int plateWidth, int plateHeight)
{
_plate = new Plate(plateWidth, plateHeight, true);
_region = new Plate(paintAreaWidth, paintAreaHeight, false);
_offset = new Vector2Int((_plate.Width - _region.Width) / 2, (_plate.Height - _region.Height) / 2);
}
/// <summary>
/// 释放图层快照资源
/// </summary>
public void Dispose()
{
if (_plate != null)
{
_plate.Dispose();
_plate = null;
}
if (_region != null)
{
_region.Dispose();
_region = null;
}
}
}
任何图层,都可以为其创建快照
,并在之后的某一时刻还原到快照
,快照能够记录一个图层在某一时刻的完整状态:
/// <summary>
/// TextureShop编辑器
/// </summary>
[DisallowMultipleComponent]
public sealed class TextureShopEditor : MonoBehaviour
{
/// <summary>
/// 当前的图层快照
/// </summary>
public TextureLayerSnapshot CurrentSnapshot { get; private set; }
/// <summary>
/// 为当前图层创建快照
/// </summary>
public void CreateLayerSnapshot()
{
if (CurrentLayer != null)
{
CurrentLayer.OutputToSnapshot(CurrentSnapshot);
}
}
/// <summary>
/// 将当前图层还原到快照
/// </summary>
public void RestoreLayerSnapshot()
{
if (CurrentLayer != null)
{
CurrentLayer.InputOfSnapshot(CurrentSnapshot);
}
}
}
至于图层如何创建快照
,如何还原到快照
,将由他自己定夺:
/// <summary>
/// 图层
/// </summary>
public sealed class TextureLayer
{
/// <summary>
/// 输出到快照
/// </summary>
/// <param name="snapshot">快照</param>
internal void OutputToSnapshot(TextureLayerSnapshot snapshot)
{
_plate.CopyTo(snapshot._plate);
_region.CopyTo(snapshot._region);
snapshot._offset = Offset;
}
/// <summary>
/// 通过快照输入
/// </summary>
/// <param name="snapshot">快照</param>
internal void InputOfSnapshot(TextureLayerSnapshot snapshot)
{
snapshot._plate.CopyTo(_plate);
snapshot._region.CopyTo(_region);
Offset = snapshot._offset;
//图层为脏的,触发重新生成渲染源
_isTextureDirty = true;
}
}
到此,图层的功能就实现得差不多了。