Unity URP 如何写基础的曲面细分着色器

news2025/3/13 22:34:54

左边是默认Cube在网格模式下经过曲面细分的结果,右边是原状态。

曲面细分着色器在顶点着色器、几何着色器之后,像素着色器之前。

它的作用时根据配置信息生成额外的顶点以切割原本的面片。

关于这部分有一个详细的英文教程,感兴趣可以看一下。

https://catlikecoding.com/unity/tutorials/advanced-rendering/tessellation/

以下是完整代码

Shader "Kerzh/KerzhCgShaderTemplate"
{
    Properties
    {
	    _Color("Color", Color) = (1,1,1,1)
		_TessellationUniform ("Tessellation Uniform", Vector) = (1,1,1,1)
    }
    
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalRenderPipeline" }

        Pass
        {
        	CGPROGRAM
		    #pragma vertex vert
			#pragma hull hull
			#pragma domain domain
		    #pragma geometry geom
			#pragma fragment frag
			#pragma target 4.6
            
			#include "UnityCG.cginc"
			#include  "Assets/TA/ShaderLib/CgincInclude//CommonCgInc.cginc"
			#include  "Assets/TA/ShaderLib/CgincInclude//CustomTessellation.cginc"
            
			MeshData vert (MeshData input)
            {
                return input;
            }

			//如果不正确配置会报错
			[UNITY_domain("tri")]  //  正在处理三角形   "tri", "quad", or "isoline"
			[UNITY_outputcontrolpoints(3)]  //  每个面片输出的顶点为3个
			[UNITY_outputtopology("triangle_cw")]  //  当创建三角形时应是顺时针还是逆时针,这里应是顺时针  "point", "line", "triangle_cw", or "triangle_ccw"
			[UNITY_partitioning("integer")]  //  如何细分切割面片   "integer", "pow2", "fractional_even", or "fractional_odd"
			[UNITY_patchconstantfunc("patchConstantFunction")]  //  细分切割部分还必须提供函数处理,每个面片调用一次
			MeshData hull (InputPatch<MeshData, 3> patch, uint id : SV_OutputControlPointID)  //  每个顶点调用一次,如果是处理三角形就是调用三次
			{
				//  如果_TessellationUniform输入值为1,不产生任何变化,但当输入值为2时,具体发生了什么
				//  对于一个三角形(因为我们配置的是处理三角形),根据三条边的二等分位置添加额外的一个顶点,如果是3就是三等分位置添加两个顶点
				//  对于一个三角形(因为我们配置的是处理三角形),根据三条个顶点添加一个中心顶点
				//  根据patchConstantFunction赋值分配生成顶点的插值权重
				return patch[id];
			}

			//  barycentricCoordinates分别代表各个点的插值权重,每个面片调用一次,对细分后的三角顶点形进行处理(也就是说原顶点不经过此处理?)
			[UNITY_domain("tri")]  //  正在处理三角形
			MeshData domain(TessellationFactors factors, OutputPatch<MeshData, 3> patch, float3 barycentricCoordinates : SV_DomainLocation)  
			{
				MeshData data;  //  新的插值权重顶点
				
				#define MY_DOMAIN_PROGRAM_INTERPOLATE(fieldName) data.fieldName = \
				patch[0].fieldName * barycentricCoordinates.x + \
				patch[1].fieldName * barycentricCoordinates.y + \
				patch[2].fieldName * barycentricCoordinates.z;

				//对所有信息利用插值权重进行插值计算
				MY_DOMAIN_PROGRAM_INTERPOLATE(vertex)
				MY_DOMAIN_PROGRAM_INTERPOLATE(normalOS)
				MY_DOMAIN_PROGRAM_INTERPOLATE(tangentOS)
				MY_DOMAIN_PROGRAM_INTERPOLATE(uv1)
				MY_DOMAIN_PROGRAM_INTERPOLATE(uv2)
				MY_DOMAIN_PROGRAM_INTERPOLATE(vertexColor)
				
				return data;
			}

			//最大的顶点数,这里比如你要生成三个三角形面片,那么一个面片需要三个顶点,就是9个顶点,一般来讲这里直接填多一点即可,不过可能填太多了会导致内存占用?
            [maxvertexcount(9)]
            void geom (triangle MeshData input[3],inout TriangleStream<VOutData> triStream)
            {
            	//原样转换过去
            	VOutData output[3];
            	for(int i=0;i<3;i++)
                {
                    VOutData p0;
                    p0 = FillBaseV2FData(input[i]);
                    output[i] = p0;
                }

				triStream.RestartStrip(); //  重新开始一个新的三角形
				triStream.Append(output[0]);
                triStream.Append(output[1]);
                triStream.Append(output[2]);

				return;

				//验证准确性用  勿删
				MeshData centerMeshData;
				centerMeshData.vertex = (input[0].vertex + input[1].vertex + input[2].vertex)/3.0;
				centerMeshData.uv1 = (input[0].uv1 + input[1].uv1 + input[2].uv1)/3.0;
				centerMeshData.uv2 = (input[0].uv2 + input[1].uv2 + input[2].uv2)/3.0;
				centerMeshData.tangentOS = (input[0].tangentOS + input[1].tangentOS + input[2].tangentOS)/3.0;
				centerMeshData.normalOS = (input[0].normalOS + input[1].normalOS + input[2].normalOS)/3.0;
				centerMeshData.vertexColor = (input[0].vertexColor + input[1].vertexColor + input[2].vertexColor)/3.0;
				centerMeshData.vertex += float4((centerMeshData.normalOS * 0.35), 0);
				VOutData center = FillBaseV2FData(centerMeshData);

				//  根据这三个点分别和中心点制造三角形输出
                triStream.RestartStrip(); //  重新开始一个新的三角形
                triStream.Append(output[1]);
                triStream.Append(center);
                triStream.Append(output[0]);

                triStream.RestartStrip();  //  重新开始一个新的三角形
                triStream.Append(output[2]);
                triStream.Append(center);
                triStream.Append(output[1]);
                
                triStream.RestartStrip(); //  重新开始一个新的三角形
                triStream.Append(output[0]);
                triStream.Append(center);
                triStream.Append(output[2]);
            }

			float4 _Color;
			
			float4 frag (VOutData i) : SV_Target
			{
				return _Color;
			}
			ENDCG
        }
    }
}

依赖文件CommonCgInc.cginc:

#ifndef CommonCgInc
#define CommonCgInc

float3 FromScript_LocalPositionWS;
float3 FromScript_LocalRotationWS;
float3 FromScript_LocalScaleWS;

//输入结构
struct MeshData
{
    float4 vertex : POSITION;
    float2 uv1 : TEXCOORD0;
    float2 uv2 : TEXCOORD1;
    float4 tangentOS :TANGENT;
    float3 normalOS : NORMAL;
    float4 vertexColor : COLOR;
};

//传递结构
struct VOutData
{
    float4 pos : SV_POSITION; // 必须命名为pos ,因为 TRANSFER_VERTEX_TO_FRAGMENT 是这么命名的,为了正确地获取到Shadow
    float2 uv1 : TEXCOORD0;
    float3 tangentWS : TEXCOORD1;
    float3 bitangentWS : TEXCOORD2;
    float3 normalWS : TEXCOORD3;
    float3 posWS : TEXCOORD4;
    float3 posOS : TEXCOORD5;
    float3 normalOS : TEXCOORD6;
    float4 vertexColor : TEXCOORD7;
    float2 uv2 : TEXCOORD8;
};

//传递结构赋值
VOutData FillBaseV2FData(MeshData input)
{
    VOutData output;
    output.pos = UnityObjectToClipPos(input.vertex);
    output.uv1 = input.uv1;
    output.uv2 = input.uv2;
    output.normalWS = normalize(UnityObjectToWorldNormal(input.normalOS));
    output.posWS = mul(unity_ObjectToWorld, input.vertex);
    output.posOS = input.vertex.xyz;
    output.tangentWS = normalize(UnityObjectToWorldDir(input.tangentOS));
    output.bitangentWS = cross(output.normalWS, output.tangentWS) * input.tangentOS.w; //乘上input.tangentOS.w 是unity引擎的bug,有的模型是 1 有的模型是 -1,必须这么写
    output.normalOS = input.normalOS;
    output.vertexColor = input.vertexColor;
    return output;
}

// Hue, Saturation, Value
// Ranges:
//  Hue [0.0, 1.0]
//  Sat [0.0, 1.0]
//  Lum [0.0, HALF_MAX]
// //HSV色彩模型转为RGB色彩模型  FROM::color.hlsl
float3 HSV2RGB( float3 hsv ){
    const float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    float3 p = abs(frac(hsv.xxx + K.xyz) * 6.0 - K.www);
    return hsv.z * lerp(K.xxx, saturate(p - K.xxx), hsv.y);
}
//RGB色彩模型转为HSV色彩模型  FROM::color.hlsl
float3 RGB2HSV(float3 rgb)
{
    float4 K = float4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
    float4 p = lerp(float4(rgb.bg, K.wz), float4(rgb.gb, K.xy), step(rgb.b, rgb.g));
    float4 q = lerp(float4(p.xyw, rgb.r), float4(rgb.r, p.yzx), step(p.x, rgb.r));

    float d = q.x - min(q.w, q.y);
    float e = 1.0e-10;
    return float3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

//Gooch光照模型  ⻛格化的着⾊模型  强调冷暖色调  FROM::render4th P146
float3 GoochModel(float3 baseColor, float3 highLightColor, float3 normalWs, float3 lightDirWs, float3 viewDirWs){
    normalWs = normalize(normalWs);
    lightDirWs = normalize(lightDirWs);

    float3 coolColor = float3(0,0,0.55) + 0.25*baseColor;
    float3 warmColor = float3(0.3,0.3,0) + 0.25*baseColor;
                
    float size = clamp(100*(dot(reflect(lightDirWs, normalWs), viewDirWs) - 97), 0, 1);
    float4 halfLambert = dot(normalWs, lightDirWs) * 0.5 + 0.5;
    return  size*highLightColor + (1-size)*(halfLambert*warmColor + (1 - halfLambert)* coolColor);
}

//添加虚拟点光源  _VirtualLightFade越大,衰减越快
float3 CalculatePointVirtualLight(float3 _VirtualLightPos, float3 positionOS, float _VirtualLightFade, float3 _VirtualLightColor)
{
    float virtualLightDis = distance(_VirtualLightPos,positionOS);
    float3 virtualLight = exp(-_VirtualLightFade*virtualLightDis)*_VirtualLightColor;
    //TODO:也许补充方向衰减
    return virtualLight;
}

//切线空间计算视差uv 根据视角方向以深度反追命中点uv
float2 CalculateRealUVAfterDepth(float2 originUV, float3 viewDirTS, float depth)
{
    //计算视角方向和深度方向的cos值
    float cosTheta = dot(normalize(viewDirTS), float3(0,0,-1));  //  一般来讲unity是左手坐标系,但在切线和观察空间较为特殊是右手坐标系,不过这并不影响z轴方向的判断
    //根据深度差算出两点间距离
    float dis = depth / cosTheta;
    //算出应用深度差后对应点位
    float3 originUVPoint = float3(originUV, 0);
    float3 afrerDepthUVPoint = originUVPoint + normalize(viewDirTS) * dis;
    //返回应用深度差后对应UV
    return afrerDepthUVPoint.xy;
}
#endif

依赖文件CustomTessellation.cginc:

// Tessellation programs based on this article by Catlike Coding:
// https://catlikecoding.com/unity/tutorials/advanced-rendering/tessellation/
// 关于参数的详细说明
// https://zhuanlan.zhihu.com/p/479792793

#include  "Assets/TA/ShaderLib/CgincInclude//CommonCgInc.cginc"

//细分切割函数对于每个面片调用一次,对于每一个面片,比如三角形:每条边需要一个切割因子,内部需要一个切割因子
struct TessellationFactors {
	float edge[3] : SV_TessFactor;  //  对应三条边的切割因子
	float inside : SV_InsideTessFactor;  //  对应内部的切割因子
};

float4 _TessellationUniform;  //  细分因子,当值为1时不发生细分切割。因子可以分别设置,比如边因子是1内部因子是5,那就不会在边上生成顶点,会在中心生成五个顶点
//自定义的细分切割方法,每个面片调用一次
TessellationFactors patchConstantFunction (InputPatch<MeshData, 3> patch)
{
	TessellationFactors f;
	f.edge[0] = _TessellationUniform.x;
	f.edge[1] = _TessellationUniform.y;
	f.edge[2] = _TessellationUniform.z;
	f.inside = _TessellationUniform.w;
	return f;
}

其中,通过

#pragma hull hull
#pragma domain domain

定义hull和domain着色器,其中hull负责切割,domain用于根据权重处理细分后的顶点数据。

hull部分:

//细分切割函数对于每个面片调用一次,对于每一个面片,比如三角形:每条边需要一个切割因子,内部需要一个切割因子
struct TessellationFactors {
	float edge[3] : SV_TessFactor;  //  对应三条边的切割因子
	float inside : SV_InsideTessFactor;  //  对应内部的切割因子
};

float4 _TessellationUniform;  //  细分因子,当值为1时不发生细分切割。因子可以分别设置,比如边因子是1内部因子是5,那就不会在边上生成顶点,会在中心生成五个顶点
//自定义的细分切割方法,每个面片调用一次
TessellationFactors patchConstantFunction (InputPatch<MeshData, 3> patch)
{
	TessellationFactors f;
	f.edge[0] = _TessellationUniform.x;
	f.edge[1] = _TessellationUniform.y;
	f.edge[2] = _TessellationUniform.z;
	f.inside = _TessellationUniform.w;
	return f;
}

//如果不正确配置会报错
			[UNITY_domain("tri")]  //  正在处理三角形   "tri", "quad", or "isoline"
			[UNITY_outputcontrolpoints(3)]  //  每个面片输出的顶点为3个
			[UNITY_outputtopology("triangle_cw")]  //  当创建三角形时应是顺时针还是逆时针,这里应是顺时针  "point", "line", "triangle_cw", or "triangle_ccw"
			[UNITY_partitioning("integer")]  //  如何细分切割面片   "integer", "pow2", "fractional_even", or "fractional_odd"
			[UNITY_patchconstantfunc("patchConstantFunction")]  //  细分切割部分还必须提供函数处理,每个面片调用一次
			MeshData hull (InputPatch<MeshData, 3> patch, uint id : SV_OutputControlPointID)  //  每个顶点调用一次,如果是处理三角形就是调用三次
			{
				//  如果_TessellationUniform输入值为1,不产生任何变化,但当输入值为2时,具体发生了什么
				//  对于一个三角形(因为我们配置的是处理三角形),根据三条边的二等分位置添加额外的一个顶点,如果是3就是三等分位置添加两个顶点
				//  对于一个三角形(因为我们配置的是处理三角形),根据三条个顶点添加一个中心顶点
				//  根据patchConstantFunction赋值分配生成顶点的插值权重
				return patch[id];
			}

这里做的操作比较少,只是传入每个面片的切割因子,一个面片(三角形)的每条边需要一个切割因子,内部需要一个切割因子,一共四个切割因子。

domain部分:

			//  barycentricCoordinates分别代表各个点的插值权重,每个面片调用一次,对细分后的三角顶点形进行处理(也就是说原顶点不经过此处理?)
			[UNITY_domain("tri")]  //  正在处理三角形
			MeshData domain(TessellationFactors factors, OutputPatch<MeshData, 3> patch, float3 barycentricCoordinates : SV_DomainLocation)  
			{
				MeshData data;  //  新的插值权重顶点
				
				#define MY_DOMAIN_PROGRAM_INTERPOLATE(fieldName) data.fieldName = \
				patch[0].fieldName * barycentricCoordinates.x + \
				patch[1].fieldName * barycentricCoordinates.y + \
				patch[2].fieldName * barycentricCoordinates.z;

				//对所有信息利用插值权重进行插值计算
				MY_DOMAIN_PROGRAM_INTERPOLATE(vertex)
				MY_DOMAIN_PROGRAM_INTERPOLATE(normalOS)
				MY_DOMAIN_PROGRAM_INTERPOLATE(tangentOS)
				MY_DOMAIN_PROGRAM_INTERPOLATE(uv1)
				MY_DOMAIN_PROGRAM_INTERPOLATE(uv2)
				MY_DOMAIN_PROGRAM_INTERPOLATE(vertexColor)
				
				return data;
			}

这里则是根据传入的边和内部的切割因子的权重,对于新增加的顶点,使用这个方法中的规则填充顶点的对应信息,以完成切割。

MY_DOMAIN_PROGRAM_INTERPOLATE

是定义了一个宏用于重复处理所有的属性,还是要根据实际情况对应处理方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1520663.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

3.15号arm

汇编语言 1. 汇编语言的组成 汇编文件中由伪操作、伪指令、汇编指令以及代码注释这几部分组成 伪操作&#xff1a; ARM的汇编中伪操作以.为前缀&#xff0c;所有的伪操作不占用内存空间&#xff0c;编译汇编时告诉编译器怎么编译当前文件&#xff0c;主要用来修改汇编内…

如何本地部署SeaFile文件共享服务并实现无公网IP访问内网本地文件

文章目录 1. 前言2. SeaFile云盘设置2.1 Owncould的安装环境设置2.2 SeaFile下载安装2.3 SeaFile的配置 3. cpolar内网穿透3.1 Cpolar下载安装3.2 Cpolar的注册3.3 Cpolar云端设置3.4 Cpolar本地设置 4.公网访问测试5.结语 1. 前言 现在我们身边的只能设备越来越多&#xff0c…

C++语法、Linux命令查询网站

文章目录 1.cplusplus2.cppreference3.Linux命令查询网站 1.cplusplus 网址&#xff1a;https://legacy.cplusplus.com/ 2.cppreference 1.cppreference中文网站&#xff1a;https://zh.cppreference.com/w/首页 2.cppreference英文原站&#xff1a;https://en.cppreference…

最新潮乎盲盒系统源码,附搭建教程

搭建方法 宝塔创建网站&#xff0c;上传后端程序到根目录&#xff0c;在.env修改数据库账号密码 上传数据库&#xff0c;伪静态thinkphp 运行目录public PHP扩展安装下面的 禁用函数先禁用下面那个&#xff0c;就可以了 前端是uniapp 后台admin 禁用函数putenv、 扩展fileinfo…

python 如何使用 NLPchina 开源sql插件,提供代码

分享一段使用python&#xff0c;通过使用发送post请求的方式&#xff0c;来从es集群中获取数据。不用使用 elasticsearh&#xff0c;仅需要导入request和json包即可。 开源sql插件官方 文档 GitHub - NLPchina/elasticsearch-sql: Use SQL to query Elasticsearch 示例代码 调…

【AI】如何创建自己的自定义ChatGPT

如何创建自己的自定义ChatGPT 目录 如何创建自己的自定义ChatGPT大型语言模型(LLM)GPT模型ChatGPTOpenAI APILlamaIndexLangChain参考推荐超级课程: Docker快速入门到精通Kubernetes入门到大师通关课本文将记录如何使用OpenAI GPT-3.5模型、LlamaIndex和LangChain创建自己的…

蓝桥杯单片机快速开发笔记——AT24C02 E2PROM

一、原理分析 此处考点分析&#xff1a;可能会在引用iic驱动文件时需要自己在头文件定义SCL/SDA sbit sda P2^1; sbit scl P2^0; 工作原理&#xff1a;24C02是一种电可擦除可编程只读存储器&#xff0c;通过I2C总线与微处理器或控制器通信。它可以通过电子方式对存储的数据进…

LeetCode Python - 55.跳跃游戏

目录 题目答案运行结果 题目 给你一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个下标&#xff0c;如果可以&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 fal…

VUE 运行NPM 报错:npm ERR! code CERT_HAS_EXPIRED 解决方案

现象 由于各种原因需要调试一下VUE代码&#xff0c;用Git拉下来运行不了&#xff08;之前是可以正常运行的&#xff09;&#xff0c;报错为&#xff1a;npm ERR! code CERT_HAS_EXPIRED........... 原因 NPM 证书签名过期了 解决方法 第一步&#xff1a;CMD 命令 查看NPM代理源…

抖音获得抖音商品详情 API 返回值说明

抖音&#xff08;Douyin&#xff09;的商品详情API返回值通常会包含有关商品的详细信息。这些信息可能包括但不限于商品ID、商品名称、商品价格、商品图片、商品描述、商品销售属性等。以下是一个简化的抖音商品详情API返回值示例和说明&#xff1a; 调用链接获取详情 item_g…

【elasticsearch实战】从零开始设计全站搜索引擎

业务需求 最近需要一个全站搜索的功能&#xff0c;我们的站点的特点是数据多源&#xff0c;即有我们本地数据库&#xff0c;也包含了第三方数据源&#xff0c;我们的数据类型除了网页&#xff0c;还包括了各种类型的文档&#xff0c;例如&#xff1a;doc、pdf、excel、ppt等格…

Hive借助java反射解决User-agent编码乱码问题

一、需求背景 在截取到浏览器user-agent&#xff0c;并想保存入数据库中&#xff0c;经查询发现展示的为编码后的结果。 现需要经过url解码过程&#xff0c;将解码后的结果保存进数据库&#xff0c;那么有几种实现方式。 二、问题解决 1、百度&#xff1a;url在线解码工具 …

Mac上使用M1或M2芯片的设备安装Node.js时遇到一些问题,比如卡顿或性能问题

对于Mac上使用M1或M2芯片的设备可能会遇到在安装Node.js时遇到一些问题&#xff0c;比如卡顿或性能问题。这可能是因为某些软件包或工具在M1或M2芯片上的兼容性不佳。为了解决这个问题&#xff0c;您可以尝试以下方法&#xff1a; 1. 使用Rosetta模式 对于一些尚未适配M1或M2…

YOLOv9|加入2023Gold YOLO中的GD机制!遥遥领先!

专栏介绍&#xff1a;YOLOv9改进系列 | 包含深度学习最新创新&#xff0c;助力高效涨点&#xff01;&#xff01;&#xff01; 一、Gold YOLO摘要 在过去的几年里&#xff0c;YOLO系列模型已经成为实时目标检测领域的领先方法。许多研究通过修改体系结构、增加数据和设计新的损…

记录dockers中Ubuntu安装python3.11

参考&#xff1a; docker-ubuntu 安装python3.8,pip3_dockerfile ubuntu22 python3.8-CSDN博客

JavaScript中的事件模型(详细案例代码)

文章目录 一、事件与事件流二、事件模型原始事件模型特性 标准事件模型特性 IE事件模型 一、事件与事件流 javascript中的事件&#xff0c;可以理解就是在HTML文档或者浏览器中发生的一种交互操作&#xff0c;使得网页具备互动性&#xff0c; 常见的有加载事件、鼠标事件、自定…

FPGA静态时序分析与约束(四)、时序约束

系列文章目录 FPGA静态时序分析与约束&#xff08;一&#xff09;、理解亚稳态 FPGA静态时序分析与约束&#xff08;二&#xff09;、时序分析 FPGA静态时序分析与约束&#xff08;三&#xff09;、读懂vivado时序报告 文章目录 系列文章目录前言一、什么是时序约束&#xff1…

【CVE-2022-47549】OPTEE之使用故障注入攻击绕过Raspberry Pi3上的TA签名验证

目录 一、问题描述 二、严重性评估 三、缓解措施 四、Patches 五、解决方案 六、参考 七、OP-TEE ID 八、报告人/单位 九、更多信息 十、时间线 一、问题描述 SEAL研究人员和工程师成功地通过利用电磁故障注入的glitch攻击&#xff0c;在树莓派3设备上攻克了签名检查…

佛教圣地——普陀山

洛 迦 山 今天早上集合时间是5:20&#xff0c;我们不到4点都醒了&#xff0c;昨天晚上我并没有睡着。清早起来之后&#xff0c;身体很是匮乏。但我们还需要去托运行李&#xff0c;所以只能匆忙起来&#xff0c;等忙活完了&#xff0c;已经5点多了&#xff0c;我们下楼走到小区门…

SCUI Admin:快速构建企业级中后台前端的利器 让前端开发更快乐。

欢迎加入我们的前端组件学习交流群&#xff0c;可添加群主微信&#xff0c;审核通过后入群。 随着Web技术的不断发展&#xff0c;中后台前端解决方案在各类企业级应用中扮演着越来越重要的角色。SCUI Admin正是一款基于Vue3和elementPlus的WebUI前端框架&#xff0c;旨在帮助开…