方案一:GL实现
性能问题
多次重复绘制:在 OnRenderObject 函数中,每次渲染时都会重新绘制所有的线框,这可能会影响性能。可以考虑使用 Mesh 来存储线框数据,然后使用 MeshRenderer 进行渲染,这样可以减少 GL 调用的开销。
动态创建材质:在 Start 函数中动态创建材质,这可能会导致内存管理问题。建议在编辑器中创建好材质,然后在代码中引用它们。
using UnityEngine;
public class WireframeRenderer : MonoBehaviour
{
public Color lineColor;
public Color backgroundColor;
public bool ZWrite = true;
public bool AWrite = true;
public bool blend = true;
private Vector3[] lines;
private Material lineMaterial;
private MeshRenderer meshRenderer;
void Start()
{
meshRenderer = GetComponent<MeshRenderer>();
if (meshRenderer == null)
{
meshRenderer = gameObject.AddComponent<MeshRenderer>();
}
string shaderCode = "Shader \"Lines/Background\" { Properties { _Color (\"Main Color\", Color) = (1,1,1,1) } SubShader { Pass {" + (ZWrite ? " ZWrite on " : " ZWrite off ") + (blend ? " Blend SrcAlpha OneMinusSrcAlpha" : " ") + (AWrite ? " Colormask RGBA " : " ") + "Lighting Off Offset 1, 1 Color[_Color] }}}";
meshRenderer.material = new Material(Shader.Parse(shaderCode));
lineMaterial = new Material(Shader.Parse("Shader \"Lines/Colored Blended\" { SubShader { Pass { Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Cull Front Fog { Mode Off } } } }"));
lineMaterial.hideFlags = HideFlags.HideAndDontSave;
lineMaterial.shader.hideFlags = HideFlags.HideAndDontSave;
MeshFilter filter = GetComponent<MeshFilter>();
if (filter == null || filter.mesh == null)
{
Debug.LogError("MeshFilter or Mesh is missing!");
return;
}
Mesh mesh = filter.mesh;
Vector3[] vertices = mesh.vertices;
int[] triangles = mesh.triangles;
lines = new Vector3[triangles.Length];
for (int i = 0; i < triangles.Length / 3; i++)
{
lines[i * 3] = vertices[triangles[i * 3]];
lines[i * 3 + 1] = vertices[triangles[i * 3 + 1]];
lines[i * 3 + 2] = vertices[triangles[i * 3 + 2]];
}
}
void OnRenderObject()
{
meshRenderer.material.color = backgroundColor;
lineMaterial.SetPass(0);
GL.PushMatrix();
GL.MultMatrix(transform.localToWorldMatrix);
GL.Begin(GL.LINES);
GL.Color(lineColor);
for (int i = 0; i < lines.Length / 3; i++)
{
GL.Vertex(lines[i * 3]);
GL.Vertex(lines[i * 3 + 1]);
GL.Vertex(lines[i * 3 + 1]);
GL.Vertex(lines[i * 3 + 2]);
GL.Vertex(lines[i * 3 + 2]);
GL.Vertex(lines[i * 3]);
}
GL.End();
GL.PopMatrix();
}
}
方案二:传统Shader
原理:通过在顶点着色器和片段着色器中利用重心坐标来判断像素是否位于三角形的边缘,从而确定是否绘制线框颜色。
优点:计算逻辑相对简单,对 GPU 性能要求较低,兼容性更好。
缺点:对于复杂模型,重心坐标的计算和边缘检测算法也更加复杂,性能也会有所下降
如果你只是矩形,那用这个就能实现
Shader "Custom/WireframeGeometryShader"
{
Properties
{
[HDR] _WireColor ("Wire color", Color) = (0, 0, 0, 1)
_WireSize ("Wire size", float) = 0.3
_ZMode("Z Mode", Range(-1,1)) = 1
_Cull("Cull Mode", Float) = 2.0
}
SubShader
{
Tags {
"RenderType"="Transparent" "Queue"="Transparent" }
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
Cull[_Cull]
ZWrite Off
CGPROGRAM
#include "UnityCG.cginc"
#include "Core.cginc"
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
}
方案三:几何Shader
之前有介绍过渲染管线梳理图
传统的解决方案可能依赖于几何着色器,但这不适用于所有平台(在 Android 和 iOS 手机端使用几何着色器需要谨慎考虑),而且不同大小的三角形之间很难保持一致的线条宽度
下面是效果和shader
Shader "Custom/WireframeGeometryShader"
{
Properties
{
_WireColor ("Wireframe Color", Color) = (1,1,1,1)
_WireThickness ("Wireframe Thickness", Range(0.0, 30.0)) = 1.0
}
SubShader
{
Tags {
"RenderType"="Opaque" "Queue"="Transparent+1" }
LOD 100
Pass
{
ZWrite On
Blend SrcAlpha OneMinusSrcAlpha
Offset -1, -1
CGPROGRAM
#pragma vertex vert
#pragma geometry geom
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2g
{
float4 pos : SV_POSITION;
};
struct g2f
{
float4 pos : SV_POSITION;
float3 barycentric : TEXCOORD0;
};
fixed4 _WireColor;
float _WireThickness;
v2g vert(appdata v)
{
v2g o;
o.pos = UnityObjectToClipPos(v.vertex);
return o;
}
[maxvertexcount(3)]
void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream)
{
g2f o;
for (int i = 0; i < 3; i++)
{
o.pos = IN[i].pos;
o.barycentric = float3(0, 0, 0);
o.barycentric[i] = 1;
triStream.Append(o);
}
triStream.RestartStrip();
}
fixed4 frag(g2f i) : SV_Target
{
float dist = min(min(i.barycentric.x, i.barycentric.y), i.barycentric.z);
float threshold = _WireThickness * 0.001;
float isWireframe = step(dist, threshold);
fixed4 col = _WireColor;
col.a *= isWireframe;
return col;
}
ENDCG
}
}
FallBack "Diffuse"
}
方案四:最佳方案UnityWireframeRenderer
这算是比较好的解决方案,通过顶点UV坐标在片段着色器中计算边缘距离,以实现线框渲染。通过处理屏幕空间像素与UV空间的关系,该系统可以适应各种尺寸和形状的三角面,并确保线条的一致性。
下面是效果和shader
参考链接:https://gitcode.com/gh_mirrors/un/UnityWireframeRenderer/?utm_source=artical_gitcode&index=top&type=href&&isLogin=1
这个实现方式跟插件Wireframe Shader Effect差不多
我又优化一下主要脚本
点击下载完整资源UnityWireframeRenderer
优化后的这个脚本与Wireframe Shader Effect插件的WireframeShader对比
Wireframe Shader Effect插件下载
WireframeRenderer 优化后的脚本在性能方面明显优于 WireframeShader:
- 网格和内存优化:WireframeRenderer 在网格处理和内存管理上做了更多优化,避免了每帧都重新生成网格和材质。
- 材质更新与渲染开销:WireframeRenderer 只在需要时更新材质,避免了无谓的材质创建和更新。
- 组件创建与销毁:WireframeRenderer 通过智能管理组件的创建和销毁,减少了不必要的资源浪费。
使用方法很简单
把下载下来的资源包,导入unity,在你的mesh物体上挂载脚本WireframeRenderer即可看到效果,关闭脚本恢复效果
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
public class WireframeRenderer : MonoBehaviour
{
public bool ShowBackFaces;
public Color LineColor = Color.black;
public bool Shaded;
[SerializeField, HideInInspector]
private Renderer originalRenderer;
[SerializeField, HideInInspector]
private Mesh processedMesh;
[SerializeField, HideInInspector]
private Renderer wireframeRenderer;
[SerializeField, HideInInspector]
private Material wireframeMaterialCull;
[SerializeField, HideInInspector]
private Material wireframeMaterialNoCull;
[SerializeField, HideInInspector]
private RendererType originalRendererType;
enum RendererType
{
MeshRenderer,
SkinnedMeshRenderer
}
void Awake()
{
Validate();
}
private void OnDestroy()
{
if (wireframeRenderer != null)
{
if (Application.isPlaying)
{
Destroy(wireframeRenderer.gameObject);
}
else
{
DestroyImmediate(wireframeRenderer.gameObject);
}
wireframeRenderer = null;
}
}
void Validate()
{
// 只在 wireframeRenderer 为 null 时创建和初始化
if (wireframeRenderer == null)
{
Mesh originalMesh = null;
var meshFilter = GetComponentInChildren<MeshFilter>();
if (meshFilter != null)
{
originalMesh = meshFilter.sharedMesh;
originalRendererType = RendererType.MeshRenderer;
originalRenderer = meshFilter.GetComponent<Renderer>();
}
if (originalMesh == null)
{
var skinnedMeshRenderer = GetComponentInChildren<SkinnedMeshRenderer>();
if (skinnedMeshRenderer != null)
{
originalMesh = skinnedMeshRenderer.sharedMesh;
originalRendererType = RendererType.SkinnedMeshRenderer;
originalRenderer = skinnedMeshRenderer;
}
}
if (originalMesh == null)
{
Debug.LogError("Wireframe renderer requires a MeshRenderer or a SkinnedMeshRenderer in the same gameobject or in one of its children");
enabled = false;
return;
}
// 只有在网格发生改变时才重新生成 processedMesh
if (processedMesh == null || processedMesh.vertexCount != originalMesh.vertexCount)
{
processedMesh = GetProcessedMesh(originalMesh);
}
if (processedMesh == null)
{
return;
}
// 如果没有 wireframeRenderer,则创建
if (wireframeRenderer == null)
{
CreateWireframeRenderer();
CreateMaterials();
}
}
OnValidate();
}
void CreateWireframeRenderer()
{
var wireframeGO = new GameObject("Wireframe renderer");
wireframeGO.transform.SetParent(originalRenderer.transform);
wireframeGO.transform.localPosition = Vector3.zero;
wireframeGO.transform.localRotation = Quaternion.identity;
if (originalRendererType == RendererType.MeshRenderer)
{
wireframeGO.AddComponent<MeshFilter>().mesh = processedMesh;
wireframeRenderer = wireframeGO.AddComponent<MeshRenderer>();
}
else
{
var originalSkinnedMeshRenderer = (SkinnedMeshRenderer)originalRenderer;
var wireframeSkinnedMeshRenderer = wireframeGO.AddComponent<SkinnedMeshRenderer>();
wireframeSkinnedMeshRenderer.bones = originalSkinnedMeshRenderer.bones;
wireframeSkinnedMeshRenderer.sharedMesh = processedMesh;
wireframeRenderer = wireframeSkinnedMeshRenderer;
}
}
void OnValidate()
{
if (wireframeRenderer == null) return;
UpdateWireframeRendererMaterial();
UpdateLineColor();
UpdateShaded();
}
void CreateMaterials()
{
// 只在没有材质时创建
if (wireframeMaterialNoCull == null || wireframeMaterialCull == null)
{
wireframeMaterialNoCull = CreateWireframeMaterial(false);
wireframeMaterialCull = CreateWireframeMaterial(true);
}
}
void UpdateWireframeRendererMaterial()
{
wireframeRenderer.material = ShowBackFaces ? wireframeMaterialNoCull : wireframeMaterialCull;
}
void UpdateLineColor()
{
wireframeRenderer.sharedMaterial.SetColor("_LineColor", LineColor);
}
void UpdateShaded()
{
if (originalRenderer != null)
{
if (Shaded)
{
originalRenderer.enabled = Shaded;
}
else
{
originalRenderer.enabled = !enabled;
}
}
}
Material CreateWireframeMaterial(bool cull)
{
var shaderLastName = cull ? "Cull" : "NoCull";
var shader = Shader.Find("Wireframe/" + shaderLastName);
var material = new Material(shader);
return material;
}
private void OnEnable()
{
if (wireframeRenderer != null)
{
originalRenderer.enabled = false;
wireframeRenderer.enabled = true;
OnValidate();
}
}
private void OnDisable()
{
if (wireframeRenderer != null)
{
originalRenderer.enabled = true;
wireframeRenderer.enabled = false;
}
}
Mesh GetProcessedMesh(Mesh mesh)
{
var maximumNumberOfVertices = 65534; // Unity uses 16-bit indices
var meshTriangles = mesh.triangles;
var meshVertices = mesh.vertices;
var meshNormals = mesh.normals;
var boneWeights = mesh.boneWeights;
var numberOfVerticesRequiredForTheProcessedMesh = meshTriangles.Length;
if (numberOfVerticesRequiredForTheProcessedMesh > maximumNumberOfVertices)
{
Debug.LogError("Wireframe renderer can't safely create the processed mesh it needs because the resulting number of vertices would surpass unity vertex limit!");
return null;
}
var processedMesh = new Mesh();
var processedVertices = new Vector3[numberOfVerticesRequiredForTheProcessedMesh];
var processedUVs = new Vector2[numberOfVerticesRequiredForTheProcessedMesh];
var processedTriangles = new int[meshTriangles.Length];
var processedNormals = new Vector3[numberOfVerticesRequiredForTheProcessedMesh];
var processedBoneWeigths = new BoneWeight[numberOfVerticesRequiredForTheProcessedMesh]; // The size of the array is either the same as vertexCount or empty.
for (var i = 0; i < meshTriangles.Length; i += 3)
{
processedVertices[i] = meshVertices[meshTriangles[i]];
processedVertices[i + 1] = meshVertices[meshTriangles[i + 1]];
processedVertices[i + 2] = meshVertices[meshTriangles[i + 2]];
processedUVs[i] = new Vector2(0f, 0f);
processedUVs[i + 1] = new Vector2(1f, 0f);
processedUVs[i + 2] = new Vector2(0f, 1f);
processedTriangles[i] = i;
processedTriangles[i + 1] = i + 1;
processedTriangles[i + 2] = i + 2;
processedNormals[i] = meshNormals[meshTriangles[i]];
processedNormals[i + 1] = meshNormals[meshTriangles[i + 1]];
processedNormals[i + 2] = meshNormals[meshTriangles[i + 2]];
if (processedBoneWeigths.Length > 0)
{
processedBoneWeigths[i] = boneWeights[meshTriangles[i]];
processedBoneWeigths[i + 1] = boneWeights[meshTriangles[i + 1]];
processedBoneWeigths[i + 2] = boneWeights[meshTriangles[i + 2]];
}
}
processedMesh.vertices = processedVertices;
processedMesh.uv = processedUVs;
processedMesh.triangles = processedTriangles;
processedMesh.normals = processedNormals;
processedMesh.bindposes = mesh.bindposes;
processedMesh.boneWeights = processedBoneWeigths;
return processedMesh;
}
}