Unity URP实现漫画板效果
可以分成两个部分,一块是画框,一块是绘制框内的内容(以下实现都默认所有顶点在同一平面上)。
画框
创建透明unlit材质,计算边框区域并且着色。创建一个脚本(CreateMesh.cs
下称CreateMesh)用于创建和控制四边形网格,CreateMesh可以控制的参数有四个顶点的位置,边框粗细,以及边框效果的材质。
边框效果 Graph Shader:
点击查看CalBounding代码
float3 v01 = v1 - v0;
float3 v03 = v3 - v0;
float3 v21 = v1 - v2;
float3 v23 = v3 - v2;
float3 v0p = w_pos - v0;
float3 v2p = w_pos - v2;
float v01p = dot(normalize(v01), v0p);
float v03p = dot(normalize(v03), v0p);
float v21p = dot(normalize(v21), v2p);
float v23p = dot(normalize(v23), v2p);
float length_01 = length(v0p - normalize(v01) * v01p);
float length_03 = length(v0p - normalize(v03) * v03p);
float length_21 = length(v2p - normalize(v21) * v21p);
float length_23 = length(v2p - normalize(v23) * v23p);
distance = min(min(length_01, length_03), min(length_21, length_23));
if(length_01 < m_thickness || length_03 < m_thickness || length_21 < m_thickness || length_23 < m_thickness)
{
value = 1;
}
else
{
value = 0;
}
实现后就有了一个能随意控制的边框了。
内容物
两类判断条件,1.是否在边框(正面)之前,2.是否在边框内。也就是说只有在画框后面并且不能透过画框看到的部分才会被剔除掉。这部分用另一个材质实现。
CreateMesh.cs代码
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class CreateMesh : MonoBehaviour
{
[SerializeField]
public Vector3[] BoundVertex; //注意为局部坐标
Vector4[] BoundVertexVP = new Vector4[4];
Vector4[] BoundVertexWS = new Vector4[4];
[SerializeField, Range(0.0f, 1.0f)]
public float _m_thickness = 0.05f;
public Shader shader;
public Material maskMaterial;
private MeshFilter meshFilter;
private Mesh mesh;
private MeshRenderer meshRenderer;
// Start is called before the first frame update
void Start()
{
mesh = new Mesh();
meshFilter = GetComponent<MeshFilter>();
meshFilter.mesh = mesh;
mesh.vertices = GetVertex();
mesh.triangles = GetIndex();
mesh.uv = GetUV();
meshRenderer = GetComponent<MeshRenderer>();
meshRenderer.material = new Material(shader);
UpdataMat();
}
private void Update()
{
mesh.vertices = GetVertex();
mesh.triangles = GetIndex();
UpdataMat();
}
void UpdataMat()
{
meshRenderer.material.SetVector("_v0", BoundVertex[0]);
meshRenderer.material.SetVector("_v1", BoundVertex[1]);
meshRenderer.material.SetVector("_v2", BoundVertex[2]);
meshRenderer.material.SetVector("_v3", BoundVertex[3]);
meshRenderer.material.SetFloat("_m_thickness", _m_thickness);
UpdataSpacePosition();
}
void UpdataSpacePosition()
{
for (int i = 0; i < BoundVertex.Length; i++)
{
BoundVertexWS[i] = transform.TransformPoint(BoundVertex[i]);
BoundVertexVP[i] = Camera.main.WorldToViewportPoint(BoundVertexWS[i]);
}
//Debug.Log(BoundVertexVP[0]);
maskMaterial.SetVectorArray("_drawingBoardVertex_WS", BoundVertexWS);
maskMaterial.SetVectorArray("_drawingBoardVertex_VP", BoundVertexVP);
}
Vector3[] GetVertex()
{
return BoundVertex;
}
int[] GetIndex()
{
return new int[6] { 0, 1, 2, 2, 3, 0 };
}
Vector2[] GetUV()
{
return new Vector2[]{
new Vector2(0, 0), // 左下
new Vector2(0, 1), // 左上
new Vector2(1, 1), // 右上
new Vector2(1, 0) // 右下
};
}
private void OnDrawGizmos()
{
for (int i = 0; i < 4; i++)
{
Gizmos.color = Color.red;
Gizmos.DrawSphere(transform.TransformPoint(BoundVertex[i]), 0.02f);
}
}
}
VisibleMask材质代码
Shader "Custom/VisibleMask"
{
Properties {
}
SubShader {
Tags { "RenderType"="Opaque"}
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
ENDHLSL
Pass {
Cull Off
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
struct Attributes {
float4 positionOS : POSITION;
};
struct Varyings {
float4 positionCS : POSITION;
float3 positionWS : TEXCOORD;
};
float4 _drawingBoardVertex_VP[4];
float4 _drawingBoardVertex_WS[4];
float float2cross(float2 A, float2 B){
return A.x * B.y - A.y * B.x;
}
bool IsVertexInFront(float3 posWS){
float3 e1 = _drawingBoardVertex_WS[1] - _drawingBoardVertex_WS[0];
float3 e2 = _drawingBoardVertex_WS[2] - _drawingBoardVertex_WS[0];
float3 normalWS = normalize(cross(e1, e2));
if(dot(posWS - _drawingBoardVertex_WS[0], normalWS) >= 0) return true;
return false;
}
bool CWTriangle(float2 v0, float2 v1, float2 v2){
float z = (v1.x - v0.x) * (v2.y - v0.y) - (v1.y - v0.y) * (v2.x - v0.x);
if (z > 0) return false;
return true;
}
bool IsVertexInTriangle(float2 v0, float2 v1, float2 v2, float2 pos){
if(!CWTriangle(v0, v1, v2)) return false;
float c1 = float2cross(pos - v0, v1 - v0);
float c2 = float2cross(pos - v1, v2 - v1);
float c3 = float2cross(pos - v2, v0 - v2);
if(c1 * c2 > 0 && c2 * c3 > 0) return true;
return false;
}
bool IsVertexInQuad(float2 posCS){
if(IsVertexInTriangle(_drawingBoardVertex_VP[0].xy, _drawingBoardVertex_VP[1].xy, _drawingBoardVertex_VP[2].xy, posCS) ||
IsVertexInTriangle(_drawingBoardVertex_VP[0].xy, _drawingBoardVertex_VP[2].xy, _drawingBoardVertex_VP[3].xy, posCS)){
return true;
}
return false;
}
Varyings vert(Attributes IN) {
Varyings OUT;
VertexPositionInputs positionInputs = GetVertexPositionInputs(IN.positionOS.xyz);
OUT.positionCS = positionInputs.positionCS;
OUT.positionWS = positionInputs.positionWS;
return OUT;
}
float4 frag(Varyings IN) : SV_Target {
if(!IsVertexInFront(IN.positionWS) && !IsVertexInQuad(float2(IN.positionCS.x / _ScreenParams.x, IN.positionCS.y / _ScreenParams.y))){
discard;
}
return float4(1., 1. ,1., 1.);
}
ENDHLSL
}
}
}
通用内容物
目前只想到两个办法,一种是修改VisibleMask材质代码效果为设置模板值,这样只用修改内容物材质的模板采样即可实现漫画板效果,缺陷是内容物需要绘制两次,且如果内容物材质有修改绘制区域的操作(如外描边)是会无效的,而且不兼容built-in Shader Graph实现的材质(built-in Shader Graph不提供模板测试的接口,URP可以的fullscreen可以设置模板测试),但胜在非常方便。
另一种是包装成方法,在内容物材质中调用,不过这样需要修改内容物材质的地方就比较多了。
总的来说,前者非常简单但限制非常多性能不够好,后者更符合直觉只是需要改的地方比较多。如果有更好的方法欢迎大佬指出。
其他
透明背景不是那么的好看,这里再实现一下背景的填充以及修复从后面观察的效果。背景填充纹理的实现方法如下。
最终效果:
12.31更新效果
简单实现了下给漫画框的边框增加了模拟画笔的粗细变化效果,以及顶点的动态抖动。
控制顶点抖动:
修改后的CreateMesh.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.Mathematics;
using UnityEngine;
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class CreateMesh : MonoBehaviour
{
[SerializeField]
public Vector3[] BoundVertex; //注意为局部坐标
Vector3[] BoundVertexCurrentPos = new Vector3[4];
Vector3[] BoundVertexTargetPos = new Vector3[4];
Vector4[] BoundVertexVP = new Vector4[4];
Vector4[] BoundVertexWS = new Vector4[4];
[SerializeField, Range(0.0f, 1.0f)]
public float _m_thickness = 0.05f;
public Material maskMaterial;
private MeshFilter meshFilter;
private Mesh mesh;
private MeshRenderer meshRenderer;
// Start is called before the first frame update
void Start()
{
mesh = new Mesh();
meshFilter = GetComponent<MeshFilter>();
meshFilter.mesh = mesh;
mesh.vertices = GetVertex();
mesh.triangles = GetIndex();
mesh.uv = GetUV();
meshRenderer = GetComponent<MeshRenderer>();
UpdataMat();
System.Array.Copy(BoundVertex, BoundVertexCurrentPos, BoundVertex.Length);
System.Array.Copy(BoundVertex, BoundVertexTargetPos, BoundVertex.Length);
}
private void Update()
{
VertexJitter();
mesh.vertices = GetJitterVertex();
UpdataMat();
}
void UpdataMat()
{
meshRenderer.materials[0].SetVector("_v0", BoundVertexCurrentPos[0]);
meshRenderer.materials[0].SetVector("_v1", BoundVertexCurrentPos[1]);
meshRenderer.materials[0].SetVector("_v2", BoundVertexCurrentPos[2]);
meshRenderer.materials[0].SetVector("_v3", BoundVertexCurrentPos[3]);
meshRenderer.materials[0].SetFloat("_m_thickness", _m_thickness);
UpdataSpacePosition();
}
void VertexJitter()
{
for(int i = 0; i < BoundVertexCurrentPos.Length; ++i)
{
if (Vector3.Equals(BoundVertexCurrentPos[i], BoundVertexTargetPos[i]))
{
BoundVertexTargetPos[i] = VertexJitterTarget(BoundVertex[i]);
}
else
{
BoundVertexCurrentPos[i] = Vector3.MoveTowards(BoundVertexCurrentPos[i], BoundVertexTargetPos[i], 0.0002f);
}
}
}
Vector3 VertexJitterTarget(Vector3 v)
{
float randomFloat = UnityEngine.Random.Range(0f, math.PI);
float areaSize = 0.02f;
return new Vector3(v.x + math.cos(randomFloat) * areaSize, v.y + math.sin(randomFloat) * areaSize, v.z);
}
void UpdataSpacePosition()
{
for (int i = 0; i < BoundVertex.Length; i++)
{
BoundVertexWS[i] = transform.TransformPoint(BoundVertexCurrentPos[i]);
BoundVertexVP[i] = Camera.main.WorldToViewportPoint(BoundVertexWS[i]);
}
//Debug.Log(BoundVertexVP[0]);
maskMaterial.SetVectorArray("_drawingBoardVertex_WS", BoundVertexWS);
maskMaterial.SetVectorArray("_drawingBoardVertex_VP", BoundVertexVP);
}
Vector3[] GetVertex()
{
return BoundVertex;
}
Vector3[] GetJitterVertex()
{
return BoundVertexCurrentPos;
}
int[] GetIndex()
{
return new int[6] { 0, 1, 2, 2, 3, 0 };
}
Vector2[] GetUV()
{
return new Vector2[]{
new Vector2(0, 0), // 左下
new Vector2(0, 1), // 左上
new Vector2(1, 1), // 右上
new Vector2(1, 0) // 右下
};
}
private void OnDrawGizmos()
{
for (int i = 0; i < 4; i++)
{
Gizmos.color = Color.red;
Gizmos.DrawSphere(transform.TransformPoint(BoundVertex[i]), 0.02f);
}
}
}
边框笔刷效果:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库