在Unity中绘制等宽线条

您所在的位置:网站首页 英语购物ppt内容 在Unity中绘制等宽线条

在Unity中绘制等宽线条

#在Unity中绘制等宽线条 | 来源: 网络整理| 查看: 265

Home » Posts在Unity中绘制等宽线条在Unity世界空间中画一条等宽且抗锯齿的直线.December 2, 2021 · zznewclear13 | 编辑Equal Width Line Cover

Equal Width Lines

在Unity中绘制等宽线条动机抗锯齿和宽度的思考具体的绘制线条的操作绘制线条的流程DrawEqualWidthLine.csEqualWidthLineShader.shader结语动机#

直接动机是想要在unity中制作一个描边效果。对于卡通渲染的描边效果,已经有很多很多的案例了,但是我觉得这些案例不一定能完全满足我的需求,于是想要从画直线开始研究。

从普通绘画的角度来看,很重要的一点就是描边的宽度基本上是一致的:考虑一下远景的物体,画家在绘画的时候使用和近景相同粗细的笔(用一样或者稍弱的力),绘制一个较不精细的物体,而不是使用很细的笔去绘制一个精细的物体,结论就是远处的描边可能稍细一些,但最好是相同粗细,其颜色可能会变浅。

从另一个角度来看,描边往往需要能够控制其宽度,对于较细的线,则会有较明显的锯齿(事实上只要角度不太好的描边,就会有很明显的边缘锯齿),那么控制粗细和进行一定程度的抗锯齿也是一个研究的方向。

从第三个角度,碰巧看到了Freya Holmér制作的Shapes插件,能够绘制高质量的线条画,看上去渲染的效果很好。但是价格过于高昂,于是想要研究一个能够做到差不多效果的工具。

抗锯齿和宽度的思考#

在Unity中画直线有蛮多办法,Debug.DrawLine或者Gizmos.DrawLine都能绘制等宽的直线,不过只能固定一个像素宽,而且因为只有一个像素宽,所以会有明显的锯齿。使用LineRenderer可以绘制任意宽度的直线,写特定的shader通过透明度混合能够防止锯齿的出现(不过线段两端不太好做抗锯齿),但是不能保证在不同角度下直线的宽度相等。那么答案就很明显了,通过线段的顶点的数据,生成两个三角面(一个Quad),在GPU中计算出三角面每个顶点的屏幕空间的位置,确保线段的宽度一致。也由于有宽度的存在,给后续的透明度混合抗锯齿留下了操作的空间。

具体的绘制线条的操作#

首先看一张图(完了三角形顶点顺序画反了,已经积重难返了,代码表现对就当做对的吧) Equal Width Line Diagram

我们将使用Graphics.DrawProcedural这个方法来绘制我们的直线,每一段直线由两个三角形组成(这里要注意在unity中三角形顶点顺序是顺时针的),也就是说要绘制N条直线的话,要传入N+1个节点位置数据,而实际绘制时会绘制6N个顶点。这样传入Shader的是一个表示线段节点位置的长度是N+1的Vector3数据_VerticesBuffer,和一个表示顶点序号的从0到6N-1的uint数据,也就是在绘制时图形API自动传输的SV_VERTEXID。

这也就导致了我们的顶点着色器输入数据和普通的Shader有所区别,只有一个uint数据:

struct Attributes { uint vertexID : SV_VERTEXID; };

在顶点着色器获取到SV_VERTEXID之后,我们可以以此来计算出线段的序号lineID和每个顶点在绘制线段时的序号vertexID。在绘制时我们又要知道当前线段的两个节点的位置,在传入_VerticesBuffer之后,我们需要对每个顶点确定其对应的线段节点的序号,即lineID + 0或者lineID + 1,我们可以将这个可以通过vertexID确定的0或者1储存到一个数组vertexIndexes中,方便使用vertexID读取。

为了让线段取得较好的抗锯齿效果,我们把线段整体外扩了_OutlineWidth个像素宽(实际线段宽度是这个的两倍)。此时对于每一个顶点,我们需要知道外扩之后的屏幕空间的顶点位置.由于我们需要使用从LineStart到LineEnd的方向offset(对于1, 2, 4这几个顶点来说,offset是从LineEnd到LineStart的方向)和与其相垂直的方向来做外扩,我们需要把每个顶点的两个方向值储存到数组vertexOffsets中。可以看到顶点永远是往远离较远节点的方向移动的,因此vertexOffset的x值都是-1。而我们选取offset逆时针旋转90°的方向作为相垂直的方向,因此vertexOffset的y值会有正负之分。

最后是uv的数值,我们还需要给每个顶点传入uv的数值,就比较简单了,储存到数组vertexUVs中。

此外还要注意一点,在片元着色器中,我们需要知道LineStart和LineEnd对应的uv位置来做线段的圆形端点,因此我们还需要把这个长方体的高宽比ratio传给片元着色器。

这样,我们片元着色器的输入数据就是positionCS,uv和ratio了:

struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float ratio : TEXCOORD1; }; 绘制线条的流程#C#脚本获取要绘制线条的节点数据,传入_VerticesBuffer,调用Graphics.DrawProcedural方法进行绘制。Shader的顶点着色器根据SV_VERTEXID计算出每个顶点对应的线段序号lineID和顶点序号vertexID。通过线段序号、顶点序号和vertexIndexes从_VerticesBuffer中找到较近的线段节点的世界坐标,和较远的线段节点的世界坐标。将两个世界坐标转换到屏幕空间,得到屏幕空间两点对应的向量和与之相垂直的向量,进行归一化。将每个顶点根据vertexOffsets和描边宽度进行外扩。将外扩后的裁剪空间的坐标,从vertexUVs中获得的uv和高宽比ratio传给片元着色器。片元着色器根据ratio和uv,绘制出一个两头圆形的线段。DrawEqualWidthLine.cs#

C#脚本比较简单了,没什么特别需要注意的地方。

using UnityEngine; public class DrawEqualWidthLine : MonoBehaviour { public Vector3[] vertices; private int vertexCount; public Material equalWidthMaterial; ComputeBuffer verticesBuffer; private void EnsureBuffer(ref ComputeBuffer buffer, int count, int stride) { if (buffer == null) { buffer = new ComputeBuffer(count, stride, ComputeBufferType.Structured); } else if (buffer.count != count || buffer.stride != stride) { buffer.Release(); buffer = new ComputeBuffer(count, stride, ComputeBufferType.Structured); } } private void Awake() { vertexCount = vertices.Length; EnsureBuffer(ref verticesBuffer, vertexCount, 3 * 4); } private void Update() { if(vertexCount != vertices.Length) { vertexCount = vertices.Length; EnsureBuffer(ref verticesBuffer, vertexCount, 3 * 4); } Bounds bounds = new Bounds(Vector3.zero, Vector3.one * 100.0f); verticesBuffer.SetData(vertices); MaterialPropertyBlock mpb = new MaterialPropertyBlock(); mpb.SetBuffer("_VerticesBuffer", verticesBuffer); Graphics.DrawProcedural(equalWidthMaterial, bounds, MeshTopology.Triangles, (vertexCount - 1) * 6, properties: mpb); } void OnDestroy() { verticesBuffer.Dispose(); } } EqualWidthLineShader.shader#

基本上需要注意的都写在shader的注释里面了,使用_OutlineColor来控制线段的颜色,_OutlineWidth来控制线段的宽度,_Sharpness来控制锐利程度以达到抗锯齿的效果。

特别要注意的就是屏幕比例的问题,要确保在屏幕中两个偏移方向是互相垂直的,这样才能获得正确的新的裁剪空间的坐标,也才能获得正确的线段节点的uv值。还有要注意仿射变换和透视变换的区别,参考维基百科上的说明,决定了uv在屏幕空间中是不是线性的。

当然三个数组可以合并成一个,不过也没那个必要就是了。

Shader "zznewclear13/EqualWidthLineShader" { Properties { _OutlineColor ("Outline Color", color) = (1.0, 1.0, 1.0, 1.0) _OutlineWidth("Outline Width", float) = 10.0 _Sharpness("Sharpness", range(0, 0.99)) = 0.5 } HLSLINCLUDE #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" StructuredBuffer _VerticesBuffer; float4 _OutlineColor; float _OutlineWidth; float _Sharpness; static int vertexIndexes[] = { 0, 1, 1, 0, 1, 0 }; static float2 vertexOffsets[] = { float2(-1, -1), float2(-1, 1), float2(-1, -1), float2(-1, -1), float2(-1, -1), float2(-1, 1), }; static float2 vertexUVs[] = { float2(0, 0), float2(1, 0), float2(1, 1), float2(0, 0), float2(1, 1), float2(0, 1), }; struct Attributes { uint vertexID : SV_VERTEXID; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float ratio : TEXCOORD1; }; Varyings Vert(Attributes input) { Varyings output = (Varyings)0; int vertexID = input.vertexID % 6; int lineID = input.vertexID / 6; //获取较近的线段节点的世界坐标,和较远的线段节点的世界坐标 float3 vertexPos = _VerticesBuffer[lineID + vertexIndexes[vertexID]]; float3 anotherVert = _VerticesBuffer[lineID + 1 - vertexIndexes[vertexID]]; //转换到裁剪空间 float4 positionCS = mul(UNITY_MATRIX_VP, float4(vertexPos, 1.0)); float4 anotherCS = mul(UNITY_MATRIX_VP, float4(anotherVert, 1.0)); //Unity相机是看向Z轴负方向的,所以裁剪空间w分量小于0 //这里就不需要用从远的节点减去近的节点了 float2 offset = positionCS.xy / positionCS.w - anotherCS.xy / anotherCS.w; //得到长方形宽度 float lengthOffset = length(offset); //要考虑到屏幕的比例 float2 normalizedOffset = normalize(offset * (_ScreenParams.wz - 1.0)); float2 pointOffset = vertexOffsets[vertexID]; float2 pointOffsetX = float2(normalizedOffset.x, normalizedOffset.y) * pointOffset.x; float2 pointOffsetY = float2(-normalizedOffset.y, normalizedOffset.x) * pointOffset.y; //考虑到屏幕的比例,根据上面的两个Offset,得到新的裁剪空间的坐标 float4 newClipPos = float4(-positionCS.w * (pointOffsetX + pointOffsetY) * (_ScreenParams.zw - 1.0) * _OutlineWidth, 0, 0) + positionCS; //算的不一定对,不过也大差不差了。。 float lengthRadius = _OutlineWidth * length(pointOffsetX * (_ScreenParams.zw - 1.0)); output.positionCS = newClipPos; //如果不乘上深度的话,会有透视变形的问题 //https://en.wikipedia.org/wiki/Texture_mapping#Affine_texture_mapping output.uv = vertexUVs[vertexID] * (-newClipPos.w); output.ratio = 2.0 * lengthRadius / (2.0 * lengthRadius + lengthOffset); return output; } float GetDist(float2 uv, float ratio) { uv.x = 0.5 - abs(0.5 - uv.x); float2 coord = float2(uv.x * rcp(ratio), uv.y); float2 center = float2(0.5, 0.5); float distToCenter = length(coord - center); float distToLine = abs(uv.y - 0.5); return coord.x


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3