[Unity]硬表面模型描边断裂问题解决过程记录

2023-11-06 14:10

本文主要是介绍[Unity]硬表面模型描边断裂问题解决过程记录,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在Shader中使用单独一个Pass渲染轮廓线是非常常见的做法,其原理是在该Pass的顶点着色器中将模型顶点加上沿法线方向的偏移是原本的模型扩大一圈并剔除正向面,从而实现轮廓线效果。
但是使用该方法有一个要求就是模型的法线必须连续,也就是模型必须光滑表面,如果是硬表面的模型,由于转折处法线不连贯,会导致沿法线扩大的轮廓线模型断裂,如下图:
转折处轮廓线断裂
原因是转折处法线不连贯:
在这里插入图片描述
解决方案有二,但思路是一样的,就是将一个点光滑处理后的法线值存入该点的顶点色的RGB通道中,A通道可以用来控制轮廓线的粗细。需要注意的是,存入顶点色的法线必须是切线空间下的坐标,如果是模型空间下的坐标的话,一旦模型需要做动画,模型的轮廓线就会计算错误。一开始想要在3dsMax中通过脚本实现以上思路,结果发现Max脚本只能设置“控制点”的颜色,而不能分开设置同一顶点但是不同“Ploygon顶点”的颜色(不知道怎样描述),反正我是看了半天Max文档也没找到方法,如果有方法的话希望大佬能够在评论区告知,万分感谢!下面说一下我实验成功的两种解决方案。

(本人程序菜鸡,只会点基础,代码烂的一批,轻喷)
更新:才知道还有个“资产后处理(AssetPostprocessor)”这么个东西,感觉不错,可以看看这位大佬的文章:【Job/Toon Shading Workflow】自动生成硬表面模型Outline Normal
方案一:在Unity中实现以上思路,并将处理好的模型存为新的.asset文件(一开始我还以为获取模型的时候使用GetComponent<SkinnedMeshRenderer>().sharedMesh然后编辑该mesh就能就改资源文件,结果发现理解错了,这样并不行,而且Unity也不能保存Mesh为Fbx,只能存为.asset文件 )。具体实现为,Unity中新建SetNormalsInVertColor脚本,脚本内容如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;public class SetNormalsInVertColor : MonoBehaviour
{public string NewMeshPath = "Assets/";void Awake(){//获取MeshMesh mesh = new Mesh();if (GetComponent<SkinnedMeshRenderer>()){mesh = GetComponent<SkinnedMeshRenderer>().sharedMesh;}if (GetComponent<MeshFilter>()){mesh = GetComponent<MeshFilter>().sharedMesh;}Debug.Log(mesh.name);//声明一个Vector3数组,长度与mesh.normals一样,用于存放//与mesh.vertices中顶点一一对应的光滑处理后的法线值Vector3[] meshNormals = new Vector3[mesh.normals.Length];//开始一个循环,循环的次数 = mesh.normals.Length = mesh.vertices.Length = meshNormals.Lengthfor (int i = 0; i < meshNormals.Length; i++){//定义一个零值法线Vector3 Normal = new Vector3(0,0,0);//遍历mesh.vertices数组,如果遍历到的值与当前序号顶点值相同,则将其对应的法线与Normal相加for (int j = 0; j < meshNormals.Length; j++){if (mesh.vertices[j] == mesh.vertices[i]){Normal += mesh.normals[j];}}//归一化Normal并将meshNormals数列对应位置赋值为Normal,到此序号为i的顶点的对应法线光滑处理完成//此时求得的法线为模型空间下的法线Normal.Normalize();meshNormals[i] = Normal;}//构建模型空间→切线空间的转换矩阵ArrayList OtoTMatrixs = new ArrayList();for (int i = 0; i < mesh.normals.Length; i++){Vector3[] OtoTMatrix = new Vector3[3];OtoTMatrix[0] = new Vector3(mesh.tangents[i].x, mesh.tangents[i].y, mesh.tangents[i].z);OtoTMatrix[1] = Vector3.Cross(mesh.normals[i], OtoTMatrix[0]);OtoTMatrix[1] = new Vector3(OtoTMatrix[1].x * mesh.tangents[i].w, OtoTMatrix[1].y * mesh.tangents[i].w, OtoTMatrix[1].z * mesh.tangents[i].w);OtoTMatrix[2] = mesh.normals[i];OtoTMatrixs.Add(OtoTMatrix);}//将meshNormals数组中的法线值一一与矩阵相乘,求得切线空间下的法线值for (int i = 0; i < meshNormals.Length; i++){Vector3 tNormal;tNormal = Vector3.zero;tNormal.x = Vector3.Dot(((Vector3[])OtoTMatrixs[i])[0], meshNormals[i]);tNormal.y = Vector3.Dot(((Vector3[])OtoTMatrixs[i])[1], meshNormals[i]);tNormal.z = Vector3.Dot(((Vector3[])OtoTMatrixs[i])[2], meshNormals[i]);meshNormals[i] = tNormal;}//新建一个颜色数组把光滑处理后的法线值存入其中Color[] meshColors = new Color[mesh.colors.Length];for (int i = 0; i < meshColors.Length; i++){meshColors[i].r = meshNormals[i].x * 0.5f + 0.5f;meshColors[i].g = meshNormals[i].y * 0.5f + 0.5f;meshColors[i].b = meshNormals[i].z * 0.5f + 0.5f;meshColors[i].a = mesh.colors[i].a ;}//新建一个mesh,将之前mesh的所有信息copy过去Mesh newMesh = new Mesh();newMesh.vertices = mesh.vertices;newMesh.triangles = mesh.triangles;newMesh.normals = mesh.normals;newMesh.tangents = mesh.tangents;newMesh.uv = mesh.uv;newMesh.uv2 = mesh.uv2;newMesh.uv3 = mesh.uv3;newMesh.uv4 = mesh.uv4;newMesh.uv5 = mesh.uv5;newMesh.uv6 = mesh.uv6;newMesh.uv7 = mesh.uv7;newMesh.uv8 = mesh.uv8;//将新模型的颜色赋值为计算好的颜色newMesh.colors = meshColors;newMesh.colors32 = mesh.colors32;newMesh.bounds = mesh.bounds;newMesh.indexFormat = mesh.indexFormat;newMesh.bindposes = mesh.bindposes;newMesh.boneWeights = mesh.boneWeights;//将新mesh保存为.asset文件,路径可以是"Assets/Character/Shader/VertexColorTest/TestMesh2.asset"                          AssetDatabase.CreateAsset( newMesh, NewMeshPath);AssetDatabase.SaveAssets();Debug.Log("Done");}
}

然后将该脚本挂载到要处理的模型上面,play一下就行了,新模型就会出现在设置好的路径下。必须要说的是,需要处理的模型本身必须要有顶点色通道,也就是说在模型软件导出时必须要设置过顶点色,要不然导出的模型没有顶点色通道,当然即便没有顶点色通道在脚本中也可以处理,但是我懒得研究了ε=(´ο`*)))

方案二:由于在Unity中无法对fbx资源进行修改,这就使得整个资源导入的流程不够流畅。于是我决定研究一下Fbx SDK,用C++写个小程序直接对Fbx文件进行操作,操作的内容与方案一完全一样:计算法线→空间转换→写入顶点色→导出文件。需要处理的模型本身必须要有顶点色通道,也就是说在模型软件导出时必须要设置过顶点色,否则导出的模型没有顶点色通道,当然即便没有顶点色通道在脚本中也可以处理,但是我懒得研究了ε=(´ο`*)))
按照Fbx sdk的文档所示,下载配置好sdk,代码如下:

#include "pch.h"
#include <iostream>
#include <fbxsdk.h>void StoreNormalsToVertColor(FbxNode* node) 
{if (node->GetChildCount()){for (int i = 0; i < node->GetChildCount(); i++){if (node->GetChild(i)->GetMesh()){//获取meshFbxMesh* mesh = node->GetChild(i)->GetMesh();//获取layer,顶点色、法切线之类的顶点信息几乎存在layer中FbxLayer* layer0 = mesh->GetLayer(0);//依次获取layer中的顶点色层、法线层、切线层、副法线(或者叫副切线)层FbxLayerElementVertexColor* VertColor = layer0->GetVertexColors();FbxLayerElementNormal* VertNormal = layer0->GetNormals();FbxLayerElementTangent* VertTangent = layer0->GetTangents();FbxLayerElementBinormal* VertBinomral = layer0->GetBinormals();//逐顶点遍历操作for (int j = 0; j < mesh->GetPolygonVertexCount(); j++){//声明一个整型数组,用于存放与当前遍历顶点同属一个控制点的顶点序列//数组用的是FbxSdk内置的数组,是动态数组,比较好使FbxArray<int> SameControlPointsIndex;for (int k = 0; k < mesh->GetPolygonVertexCount(); k++){if (mesh->GetPolygonVertices()[k] == mesh->GetPolygonVertices()[j]){SameControlPointsIndex.Add(k);}}//声明一个Vector4数组,获取并存放上面声明的顶点序列数组中所有不同方向的法线//需要注意的是,与Unity的顶点不同,这里的顶点中有很多法线的方向是重复的//如果将重复的法线也参与计算则算出来的值是错误的,轮廓线会扭曲,说出来都是泪//所以使用AddUnique保证去掉重复的法线方向FbxArray<FbxVector4> Normals;for (int x = 0; x < SameControlPointsIndex.Size(); x++){FbxVector4 Normal = VertNormal->GetDirectArray()[SameControlPointsIndex[x]];Normals.AddUnique(Normal);}//将所有不同方向的法线加在一起并归一化获得光滑法线FbxVector4 SmoothNormal;for (int n = 0; n < Normals.Size(); n++){SmoothNormal += Normals[n];}SmoothNormal.Normalize();//分别获取当前顶点的切线、法线、副切线用于构建模型→切线空间的转换矩阵//需要注意的是:法线、切线、副切线的映射方式(也就是存储方式)是与顶点//序列一一对应,所以直接GetDirectArray()[顶点序号]就可以FbxVector4 Tangent = VertTangent->GetDirectArray()[j];FbxVector4 Normal = VertNormal->GetDirectArray()[j];FbxVector4 Bitangent = VertBinomral->GetDirectArray()[j];//将法线从模型空间转为切线空间//FbxSdk的内置矩阵类型不会使,算出来的值有问题,所以还是手动计算FbxVector4 tmpVector;tmpVector = SmoothNormal;tmpVector[0] = Tangent.DotProduct(SmoothNormal);tmpVector[1] = Bitangent.DotProduct(SmoothNormal);tmpVector[2] = Normal.DotProduct(SmoothNormal);tmpVector[3] = 0;SmoothNormal = tmpVector;//获取当前顶点的颜色信息存放于其layer中的序号//与法切副不同,顶点色数据在layer中的存储方式(映射Mapping方式)稍微复杂//首先要使用GetIndexArray()[顶点序号]获取其颜色值在DirectArray中的序号//然后使用GetDirectArray()[获得的序号]来获得该顶点的顶点色信息int VertColorIndex = VertColor->GetIndexArray()[j];//声明一个颜色值,将法线数值范围从-1~1处理为0~1后存入RGB通道中,A通道保持//不变,因为其中存放着轮廓线大小信息FbxColor Color;Color.mRed = SmoothNormal[0] * 0.5f + 0.5f;Color.mGreen = SmoothNormal[1] * 0.5f + 0.5f;Color.mBlue = SmoothNormal[2] * 0.5f + 0.5f;Color.mAlpha = VertColor->GetDirectArray()[VertColorIndex].mAlpha;//将颜色写入顶点颜色layer中VertColor->GetDirectArray().SetAt(VertColorIndex, Color);}}//递归调用,确保场景中所有mesh都得到处理StoreNormalsToVertColor(node->GetChild(i));}}
}int main(int argc, char** argv) {// lFilename是输入路径,lFilename2是输出路径const char* lFilename = "Weapon.fbx";const char* lFilename2 = "Export.fbx";//主函数中几乎都是FbxSdk文档中所写的代码,是导入导出fbx所需要的的标准流程// Initialize the SDK manager. This object handles all our memory management.FbxManager* lSdkManager = FbxManager::Create();// Create the IO settings object.FbxIOSettings *ios = FbxIOSettings::Create(lSdkManager, IOSROOT);lSdkManager->SetIOSettings(ios);// Create an importer using the SDK manager.FbxImporter* lImporter = FbxImporter::Create(lSdkManager, "");// Use the first argument as the filename for the importer.if (!lImporter->Initialize(lFilename, -1, lSdkManager->GetIOSettings())) {printf("Call to FbxImporter::Initialize() failed.\n");printf("Error returned: %s\n\n", lImporter->GetStatus().GetErrorString());exit(-1);}// Create a new scene so that it can be populated by the imported file.FbxScene* lScene = FbxScene::Create(lSdkManager, "myScene");// Import the contents of the file into the scene.lImporter->Import(lScene);// The file is imported; so get rid of the importer.lImporter->Destroy();//获取场景中根节点,然后对其调用自定义的StoreNormalsToVertColor函数	FbxNode* lRootNode = lScene->GetRootNode();if (lRootNode) {StoreNormalsToVertColor(lRootNode);}//导出Fbx文件FbxExporter* lExporter = FbxExporter::Create(lSdkManager, "");bool lExportStatus = lExporter->Initialize(lFilename2, -1, lSdkManager->GetIOSettings());if (!lExportStatus) {printf("Call to FbxExporter::Initialize() failed.\n");printf("Error returned: %s\n\n", lExporter->GetStatus().GetErrorString());return false;}lExporter->Export(lScene);lExporter->Destroy();// Destroy the SDK manager and all the other objects it was handling.lSdkManager->Destroy();return 0;
}

经过以上两种方案处理后的模型,在Unity shader中通过读取顶点颜色中的法线信息,然后将其转换到模型空间下与模型顶点坐标相加即可挤出轮廓线模型,shader中轮廓线pass代码如下:

		Pass{NAME "OUTLINE"Cull FrontCGPROGRAM#pragma vertex vert#pragma fragment frag// make fog work#pragma multi_compile_fog#include "UnityCG.cginc"struct a2v{float4 vertex : POSITION;float3 normal : NORMAL;float4 tangent : TANGENT;float4 vertexColor : COLOR0;};struct v2f{UNITY_FOG_COORDS(1)float4 vertex : SV_POSITION;};fixed4 _OutlineColor;half _OutlineWidth;v2f vert(a2v v){v2f o;//从顶点颜色中读取法线信息,并将其值范围从0~1还原为-1~1float3 vertNormal = v.vertexColor.rgb * 2 - 1;//使用法线与切线叉乘计算副切线用于构建切线→模型空间转换矩阵float3 bitangent = cross(v.normal,v.tangent.xyz) * v.tangent.w * unity_WorldTransformParams.w;//构建切线→模型空间转换矩阵float3x3 TtoO = float3x3(v.tangent.x, bitangent.x, v.normal.x,v.tangent.y, bitangent.y, v.normal.y,v.tangent.z, bitangent.z, v.normal.z);//将法线转换到模型空间下vertNormal = mul(TtoO, vertNormal);//模型坐标 + 法线 * 自定义粗细值 * 顶点颜色A通道 = 轮廓线模型					o.vertex = UnityObjectToClipPos(v.vertex + vertNormal *_OutlineWidth * v.vertexColor.a);UNITY_TRANSFER_FOG(o,o.vertex);return o;}fixed4 frag(v2f i) : SV_Target{// apply fogUNITY_APPLY_FOG(i.fogCoord, _OutlineColor);return _OutlineColor;}ENDCG}

Unity中轮廓线显示效果如下(模型颜色为顶点色):
在这里插入图片描述
一个部分光滑部分硬边的球体轮廓线效果如下:
在这里插入图片描述
在这里插入图片描述
简单做了一个骨骼动画,轮廓线也没有出现问题:
在这里插入图片描述
以上都是非常简单的测试,以后发现问题再解决,就这样

这篇关于[Unity]硬表面模型描边断裂问题解决过程记录的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/357087

相关文章

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

大模型研发全揭秘:客服工单数据标注的完整攻略

在人工智能(AI)领域,数据标注是模型训练过程中至关重要的一步。无论你是新手还是有经验的从业者,掌握数据标注的技术细节和常见问题的解决方案都能为你的AI项目增添不少价值。在电信运营商的客服系统中,工单数据是客户问题和解决方案的重要记录。通过对这些工单数据进行有效标注,不仅能够帮助提升客服自动化系统的智能化水平,还能优化客户服务流程,提高客户满意度。本文将详细介绍如何在电信运营商客服工单的背景下进行

作业提交过程之HDFSMapReduce

作业提交全过程详解 (1)作业提交 第1步:Client调用job.waitForCompletion方法,向整个集群提交MapReduce作业。 第2步:Client向RM申请一个作业id。 第3步:RM给Client返回该job资源的提交路径和作业id。 第4步:Client提交jar包、切片信息和配置文件到指定的资源提交路径。 第5步:Client提交完资源后,向RM申请运行MrAp

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

Andrej Karpathy最新采访:认知核心模型10亿参数就够了,AI会打破教育不公的僵局

夕小瑶科技说 原创  作者 | 海野 AI圈子的红人,AI大神Andrej Karpathy,曾是OpenAI联合创始人之一,特斯拉AI总监。上一次的动态是官宣创办一家名为 Eureka Labs 的人工智能+教育公司 ,宣布将长期致力于AI原生教育。 近日,Andrej Karpathy接受了No Priors(投资博客)的采访,与硅谷知名投资人 Sara Guo 和 Elad G

如何解决线上平台抽佣高 线下门店客流少的痛点!

目前,许多传统零售店铺正遭遇客源下降的难题。尽管广告推广能带来一定的客流,但其费用昂贵。鉴于此,众多零售商纷纷选择加入像美团、饿了么和抖音这样的大型在线平台,但这些平台的高佣金率导致了利润的大幅缩水。在这样的市场环境下,商家之间的合作网络逐渐成为一种有效的解决方案,通过资源和客户基础的共享,实现共同的利益增长。 以最近在上海兴起的一个跨行业合作平台为例,该平台融合了环保消费积分系统,在短

Retrieval-based-Voice-Conversion-WebUI模型构建指南

一、模型介绍 Retrieval-based-Voice-Conversion-WebUI(简称 RVC)模型是一个基于 VITS(Variational Inference with adversarial learning for end-to-end Text-to-Speech)的简单易用的语音转换框架。 具有以下特点 简单易用:RVC 模型通过简单易用的网页界面,使得用户无需深入了

购买磨轮平衡机时应该注意什么问题和技巧

在购买磨轮平衡机时,您应该注意以下几个关键点: 平衡精度 平衡精度是衡量平衡机性能的核心指标,直接影响到不平衡量的检测与校准的准确性,从而决定磨轮的振动和噪声水平。高精度的平衡机能显著减少振动和噪声,提高磨削加工的精度。 转速范围 宽广的转速范围意味着平衡机能够处理更多种类的磨轮,适应不同的工作条件和规格要求。 振动监测能力 振动监测能力是评估平衡机性能的重要因素。通过传感器实时监

【机器学习】高斯过程的基本概念和应用领域以及在python中的实例

引言 高斯过程(Gaussian Process,简称GP)是一种概率模型,用于描述一组随机变量的联合概率分布,其中任何一个有限维度的子集都具有高斯分布 文章目录 引言一、高斯过程1.1 基本定义1.1.1 随机过程1.1.2 高斯分布 1.2 高斯过程的特性1.2.1 联合高斯性1.2.2 均值函数1.2.3 协方差函数(或核函数) 1.3 核函数1.4 高斯过程回归(Gauss