Unity URP 仿原神角色渲染过程记录

news2025/2/22 17:25:53

想学一下NPR渲染,话不多说,先搞一只芙再说,边做边学

一、资源整理

终于是把东西全都集齐了

1、纹理设置 

首先要把将Diffuse和Lightmap的压缩改成"无"或"高质量"。

法线贴图的纹理类型改成"法线贴图"。

除颜色图外其它贴图全部取消勾选sRGB。

如果最终的渲染结果,出现贴图不清晰,有锯齿,有杂色,颜色深浅不对等问题,那大概率是贴图设置有问题,建议先重新认真检查一遍贴图设置。 

2、纹理分析

body和hair的diffuse贴图的a通道存放了自发光的遮罩

face的a通道则存放了腮红的遮罩

hair和body的lightmap的4个通道分别存放了高光范围,AO,高光强度,ramp分层

Body

lightmap.r:高光范围
lightmap.g:AO
lightmap.b:高光强度
lightmap.a:ramp分层

Hair

lightmap.r:高光范围
lightmap.g:AO
lightmap.b:高光强度
lightmap.a:ramp分层

 两张ShadowRamp分别控制白天和晚上的阴影颜色

NoramlMap是正常的法线贴图

MetalMap控制金属高光

FaceLightMap是一张SDF面部阴影图。

ShadowMap控制了阴影区域的遮罩。

ShadowMap.r/g
ShadowMap.a

二、渲染设置

原神的角色实际上应该是渲染了三遍,第一遍用第一套UV渲染正面,第二遍用第二套UV渲染背面,第三遍渲染描边,这样可以做到单面片正反两面的贴图不一样,崩铁应该也是使用了这套技术。

RendererFeatures

URP使用多pass需要使用RendererFeatures,先设置一下RendererFeatures。在项目大纲找到UniversalRenderer,默认路径在Assets/Settings/UniversalRenderer。

然后在检查器找到RendererFeatures,点击Add RendererFeature添加一个RenderObjects,然后在LightModeTags添加三个元素,三个元素分别为“渲染正面pass、渲染背面pass和渲染描边pass”。

元素0代表正面pass,元素1代表背面pass,元素2代表描边pass

然后在LayerMask(图层蒙版)把角色对应的图层勾上,默认在Default层,勾上Everything会所有图层都显示。

LayerMask勾上Everything

三、Shader分析

1.Shader准备

正式开始前先把计算需要用到的变量和向量等数据准备好,先来准备面板参数,面板参数如下:

Properties

        [Space(20.0)]
        [Toggle]_genshinShader("是否脸部", float) = 0.0

[Space(20.0)]
//Unity的自定义属性(Attribute),用于在Unity的Inspector面板中为后续的属性或字段添加垂直空间
//这里的20.0表示这个空间的高度为20个像素。这有助于组织Inspector面板中的属性,使其更易于阅读和管理

[Toggle]_genshinShader("是否脸部", float) = 0.0
//定义了一个名为_genshinShader的变量,但其实际类型被指定为float
//由于[Toggle]属性的存在,这个float变量实际上在Inspector面板中会被呈现为一个切换开关(Toggle)。

        [Space(20.0)]
        [NoScaleOffset]_diffuse("Diffuse", 2d) = "white"{}
        _fresnel("边缘光范围", Range(0.0, 10.0)) = 1.7
        _edgeLight("边缘光强度", Range(0.0, 10.0)) = 0.02

[NoScaleOffset]
//[NoScaleOffset]: 这是一个自定义属性,通常在Shader中用于告诉Unity不要对纹理应用缩放或偏移(Scale或Offset)的默认参数。这通常用于那些不需要或不应该有额外变换的纹理。

_diffuse("Diffuse", 2D) = "white"{}
//_diffuse: 用于存储Diffuse纹理的引用。

_fresnel("边缘光范围", Range(0.0, 10.0)) = 1.7
//_fresnel: 用于存储与Fresnel效应(菲涅尔反射)相关的参数,通常用于控制边缘光的范围。

_edgeLight("边缘光强度", Range(0.0, 10.0)) = 0.02
//_edgeLight: 用于存储与边缘光强度相关的参数。

        [Space(8.0)]
        _diffuseA( "Alpha(1透明, 2自发光)" , Range(0.0, 2.0)) = 0
        _Cutoff( "透明阈值" , Range(0.0, 1.0)) = 1.0
        [HDR]_glow( "自发光强度" , color) = (1.0, 1.0, 1.0, 1.0)
        _flicker( "发光闪烁速度" , float) = 0.8

_diffuseA( "Alpha(1透明, 2自发光)" , Range(0.0, 2.0)) = 0
//用于控制材质的透明度(当值为1时)或自发光强度(当值为2时)。

_Cutoff( "透明阈值" , Range(0.0, 1.0)) = 1.0
//用于确定哪些像素是完全不透明的(高于阈值)和哪些像素是透明的(低于阈值)。

[HDR]_glow( "自发光强度" , color) = (1.0, 1.0, 1.0, 1.0)
//[HDR] 是一个Unity的自定义属性,通常用于指示该属性支持高动态范围(HDR)颜色。这意味着该属性的颜色值可以超过常规的0到1范围。
//用于控制自发光的效果强度。

_flicker( "发光闪烁速度" , float) = 0.8
//用于控制某种发光效果的闪烁速度。

        [NoScaleOffset]_lightmap( "Lightmap/FaceLightmap" , 2d) = "white"{}
        _bright( "亮面范围" , float) = 0.99
        _grey( "灰面范围" , float) = 1.14
        _dark( "暗面范围" , float) = 0.5

_lightmap( "Lightmap/FaceLightmap" , 2d) = "white"{}
//用于全局光照或面部光照。

_bright( "亮面范围" , float) = 0.99
//用于控制材质中亮面部分的范围或强度。

_grey( "灰面范围" , float) = 1.14
//用于控制材质中灰色或中间亮度部分的范围或强度。

_dark( "暗面范围" , float) = 0.5
//用于控制材质中暗面部分的范围或强度。

        [NoScaleOffset]_bumpMap( "Normalmap" , 2d) = "bump"{}
        _bumpScale( "法线强度" , float) = 1.0

_bumpMap( "Normalmap" , 2d) = "bump"{}
//用于存储法线贴图(normal map)数据。

_bumpScale( "法线强度" , float) = 1.0
//整法线贴图产生的凹凸效果的强弱。

        [NoScaleOffset]_ramp( "Shadow_Ramp" , 2d) = "white"{}
        [Toggle]_dayAndNight("是否是白天" , float) = 0.0

_ramp( "Shadow_Ramp" , 2d) = "white"{}
//在Inspector中显示的属性标签。

[Toggle]_dayAndNight("是否是白天" , float) = 0.0
//显示为一个切换按钮

        _lightmapA0("1.0_Ramp条数" , Range(1, 5)) = 1
        _lightmapA1("0.7_Ramp条数" , Range(1, 5)) = 4
        _lightmapA2("0.5_Ramp条数" , Range(1, 5)) = 3
        _lightmapA3("0.3_Ramp条数" , Range(1, 5)) = 5
        _lightmapA4("0.0_Ramp条数" , Range(1, 5)) = 2

_lightmapA0 到 _lightmapA4
//用于控制不同光照强度下的Ramp条数。Ramp通常用于创建渐变或层次效果。

        [NoScaleOffset]_metalMap( "MetalMap" , 2d) = "white"{}
        _gloss( "高光范围" , Range(1, 256.0)) = 1
        _glossStrength( "高光强度" , Range(0.0, 1.0)) = 1
        _metalMapColor( "金属反射颜色" , color) = (1.0, 1.0, 1.0, 1.0)

_metalMap( "MetalMap" , 2d) = "white"{}
//与_ramp类似,但这个是用于金属的贴图

_gloss 到 _metalMapColor

//_gloss: 高光范围。

//_glossStrength: 高光强度。

//_metalMapColor: 金属反射的基础颜色。

        _outline( "描边粗细" , Range(0.0, 1.0)) = 0.4
        _outlineColor0( "描边颜色1" , color) = (1.0, 0.0, 0.0, 0.0)
        _outlineColor1( "描边颜色2" , color) = (0.0, 1.0, 0.0, 0.0)
        _outlineColor2( "描边颜色3" , color) = (0.0, 0.0, 1.0, 0.0)
        _outlineColor3( "描边颜色4" , color) = (1.0, 1.0, 0.0, 0.0)
        _outlineColor4( "描边颜色5" , color) = (0.5, 0.0, 1.0, 0.0)

_outline 到 _outlineColor5

//_outline: 描边的粗细。

//_outlineColor0 到 _outlineColor5: 不同的描边颜色。

2.URP框架简单总结

(一)、Bulit-in管线到URP管线的部分改变

本次渲染复刻使用的是Unity的URP管线,与Bulit-in管线存在部分的改变:

1.在URP管线中,我们将原来的CG代码改为HLSL代码

原来为 CGPROGRAM
现在为 HLSLPROGRAM;

原来为 ENDCG
现在为 ENDHLSL;

2.一些unity封装的函数名称,以及定义的宏发生了改变,例如:

原来为 UnityObjectToClipPos
现在为 TransformObjectToHClip;

原来为 UNITY_DECLARE_TEX2D(name)
现在为 TEXTURE2D(textureName); SAMPLER(samplerName);;

3.引用的头文件发生了改变,例如:

导入库
原来为 #include "UnityCG.cginc"
现在为 #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"//默认库
原来为 #include "AutoLight.cginc"
现在为 #include "Packages/com.unity.render-pipuniversal/ShaderLibrary/Lighting.hlsl"//光照库

 一个简单总结了Bulit-in到URP变化的blog :From Built-in to URP (teodutra.com)

            //导入库
            #include "Packages/com.unity.renderpipelines.universal/ShaderLibrary/Core.hlsl"  //默认库
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"  //光照库
            CBUFFER_START(UnityPerMaterial)  //常量缓冲区开头

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" 和 

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

  • 这两行代码是预处理器指令,用于包含(或“导入”)其他HLSL文件。
  • Core.hlsl 通常是URP的核心库,它包含了URP Shader中常用的基础函数和宏定义。
  • Lighting.hlsl 是URP的光照库,它提供了与光照相关的函数和宏定义。
  • 这两个库文件是URP Shader开发的基础,提供了许多便利的函数和工具。

CBUFFER_START(UnityPerMaterial)

  • CBUFFER_START 是一个宏定义,通常用于声明常量缓冲区(Constant Buffer)的开始。
  • 在URP和其他Unity渲染管道中,常量缓冲区是一种高效的方式,用于将大量数据(如材质属性)从CPU传递到GPU。
  • UnityPerMaterial 是这个常量缓冲区的名称。这表示这个常量缓冲区中的数据是每个材质特有的,而不是每个实例或每个顶点特有的。
  • 通常,在这个宏之后,你会看到一系列的变量声明,这些变量将存储在UnityPerMaterial常量缓冲区中。
  • 当你完成常量缓冲区的变量声明后,你会使用CBUFFER_END宏来结束这个常量缓冲区的定义。

 (二)、整体框架

Shader "Furina"
{
    Properties
    {
        [Space(20.0)]                                      
        [Toggle]_genshinShader("是否脸部", float) = 0.0

        [Space(20.0)]
        [NoScaleOffset]_diffuse("Diffuse", 2d) = "white"{}
        _fresnel("边缘光范围", Range(0.0, 10.0)) = 1.7
        _edgeLight("边缘光强度", Range(0.0, 10.0)) = 0.02
        [Space(8.0)]
        
        _diffuseA( "Alpha(1透明, 2自发光)" , Range(0.0, 2.0)) = 0
        _Cutoff( "透明阈值" , Range(0.0, 1.0)) = 1.0
        [HDR]_glow( "自发光强度" , color) = (1.0, 1.0, 1.0, 1.0)
        _flicker( "发光闪烁速度" , float) = 0.8
        [Space(30.0)]

        [NoScaleOffset]_lightmap( "Lightmap/FaceLightmap" , 2d) = "white"{}
        _bright( "亮面范围" , float) = 0.99
        _grey( "灰面范围" , float) = 1.14
        _dark( "暗面范围" , float) = 0.5
        [Space(30.0)]

        [NoScaleOffset]_bumpMap( "Normalmap" , 2d) = "bump"{}
        _bumpScale( "法线强度" , float) = 1.0
        [Space(30.0)]

        [NoScaleOffset]_ramp( "Shadow_Ramp" , 2d) = "white"{}
        [Toggle]_dayAndNight("是否是白天" , float) = 0.0
        [Space(8.0)]

        _lightmapA0("1.0_Ramp条数" , Range(1, 5)) = 1
        _lightmapA1("0.7_Ramp条数" , Range(1, 5)) = 4
        _lightmapA2("0.5_Ramp条数" , Range(1, 5)) = 3
        _lightmapA3("0.3_Ramp条数" , Range(1, 5)) = 5
        _lightmapA4("0.0_Ramp条数" , Range(1, 5)) = 2
        [Space(30.0)]

        [NoScaleOffset]_metalMap( "MetalMap" , 2d) = "white"{}
        _gloss( "高光范围" , Range(1, 256.0)) = 1
        _glossStrength( "高光强度" , Range(0.0, 1.0)) = 1
        _metalMapColor( "金属反射颜色" , color) = (1.0, 1.0, 1.0, 1.0)
        [Space(30.0)]
    
        _outline( "描边粗细" , Range(0.0, 1.0)) = 0.4
        _outlineColor0( "描边颜色1" , color) = (1.0, 0.0, 0.0, 0.0)
        _outlineColor1( "描边颜色2" , color) = (0.0, 1.0, 0.0, 0.0)
        _outlineColor2( "描边颜色3" , color) = (0.0, 0.0, 1.0, 0.0)
        _outlineColor3( "描边颜色4" , color) = (1.0, 1.0, 0.0, 0.0)
        _outlineColor4( "描边颜色5" , color) = (0.5, 0.0, 1.0, 0.0)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            //导入库
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"  //默认库
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"  //光照库
            CBUFFER_START(UnityPerMaterial)  //常量缓冲区开头

            //声明面板参数
            float _genshinShader;  //是否是脸部
            //diffuse
            float _fresnel;  //边缘光范围
            float _edgeLight;  //边缘光强度
            float _diffuseA;  //diffuseA
            float _Cutoff;  //透明阈值
            float4 _glow;  //自发光强度
            float _flicker;  //发光闪烁速度
            //lightmap/FaceLightmap
            float _bright;  //亮面范围
            float _grey;  //灰面范围
            float _dark;  //暗面范围
            //normal
            float _bumpScale;  //法线强度
            //ramp
            float _dayAndNight;  //是否是白天
            float _lightmapA0;  //1.0_Ramp条数
            float _lightmapA1;  //0.7_Ramp条数
            float _lightmapA2;  //0.5_Ramp条数
            float _lightmapA3;  //0.3_Ramp条数
            float _lightmapA4;  //0.0_Ramp条数
            //高光
            float _gloss;  //高光范围
            float _glossStrength;  //高光强度
            float3 _metalMapColor;  //金属折射颜色
            //描边
            float _outline;  //描边粗细
            float3 _outlineColor0;  //描边颜色1
            float3 _outlineColor1;  //描边颜色2
            float3 _outlineColor2;  //描边颜色3
            float3 _outlineColor3;  //描边颜色4
            float3 _outlineColor4;  //描边颜色5
        CBUFFER_END  //常量缓冲区结尾
            //声明贴图
            TEXTURE2D(_diffuse);  //Diffuse
            SAMPLER(sampler_diffuse);
            TEXTURE2D(_lightmap);  //Lightmap/FaceLightmap
            SAMPLER(sampler_lightmap);
            TEXTURE2D(_bumpMap);  //Normal
            SAMPLER(sampler_bumpMap);
            TEXTURE2D(_ramp);  //Shadow_Ramp
            SAMPLER(sampler_ramp);
            TEXTURE2D(_metalMap);  //MetalMap
            SAMPLER(sampler_metalMap);
        ENDHLSL           

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDHLSL
        }
    }
}

3.正面Pass

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

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

相关文章

Go语言RPC开发深度指南:net/rpc包的实战技巧和优化策略

Go语言RPC开发深度指南:net/rpc包的实战技巧和优化策略 概览理解net/rpc的核心概念RPC的基本原理net/rpc的工作模式关键特性 快速开始准备RPC服务和客户端的基础环境构建一个基础的RPC服务端构建一个基础的RPC客户端 开发一个实际的RPC服务设计服务接口实现服务客户…

pdf怎么压缩到2m以内或5m以内的方法

PDF作为一种广泛使用的文档格式,已经成为我们工作和生活中不可或缺的一部分。然而,有时候PDF文件内存会比较大,给我们的存储和传输带来了很大的不便。因此,学会压缩 PDF 文件是非常必要的。 打开"轻云处理pdf官网"&…

隐藏element的DateTimePicker组件自带的清空按钮

管理台页面使用到el-date-picker,type datetimerange 但是组件自带了清空按钮,实际上这个控件业务上代表开始时间和结束时间是一个必填选项,所有想要把清空按钮隐藏掉。 查看了文档https://element.eleme.io/#/zh-CN/component/datetime-p…

react 自定义鼠标右键点击事件

功能:鼠标右键点击节点时,出现“复制”功能,点击其他部位,隐藏“复制”;鼠标右键事件的文案,始终在鼠标点击位置的右下方;点击复制,提示复制成功 效果图: 代码&#xff1…

win11 之下载安装 allure

1. 下载 https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/2.25.0/allure-commandline-2.25.0.zip 2. 配置系统变量 path 下添加解压后的bin目录 3. 验证是否安装成功 输入 allure

git使用摘樱桃的方式,实现特定需求进行提交合并

文章目录 先checkOut到主要的分支(需求提交到这) 然后双击点别的需求分支,对提交内容选定 进行摘樱桃操作 然后双击回到主要分支,会发现那2个提交内容代码已经在主要分支的本地里,选中其 右键选择Squash Commits进行合并 标注自己的需求标题提交名更改后, 最后进行push推送到…

利用AI云防护实现高效负载均衡

在当今高度数字化的世界里,保证网站和应用的高可用性和响应速度对企业的业务连续性和用户体验至关重要。传统的负载均衡技术虽然能够分发流量,但在面对突发流量、DDoS攻击或资源动态调整时往往力不从心。本文将探讨如何借助AI云防护服务,不仅…

问题解决:局域网下多台电脑实现共享打印机

看了很多篇解决措施,都没有解决,自己鼓弄了好久,终于解决了,如下步骤所示,实测好用。 首先先保证本电脑已打开网络共享 其次关闭防火墙(有时会出现奇怪问题,最好关闭) 接着访问IP…

Chatgpt教我打游戏攻略

宝可梦朱 我在玩宝可梦朱的时候,我的同行队伍里有黏美儿,等级为65,遇到了下雨天但是没有进化,为什么呢? 黏美儿(Goomy)要进化为黏美龙(Goodra),需要满足以下…

【贪吃蛇】C语言教程

Hi~!这里是奋斗的小羊,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~ 💥💥个人主页:奋斗的小羊 💥💥所属专栏:C语言 🚀本系列文章为个人学习…

音频处理软件adobe audition使用教程

教程1笔记 基本操作 点击文件-》新建-》多轨会话: 编辑-》首选项,设置自动保存时间: 导入素材,文件-》导入素材,或者直接拖动进来文件! 导出多轨混音: 更改为需要导出的格式wav,mp3等格式&am…

Netty中Reactor线程的运行逻辑

Netty中的Reactor线程主要干三件事情: 轮询注册在Reactor上的所有Channel感兴趣的IO就绪事件。 处理Channel上的IO就绪事件。 执行Netty中的异步任务。 正是这三个部分组成了Reactor的运行框架,那么我们现在来看下这个运行框架具体是怎么运转的~~ 这…

Windows 命令行界面常用命令

Windows 命令行界面常用命令 首先我们通过WIN R,输入cmd进入命令行界面。 1. dir - 列出当前目录下的文件和子目录 用法: dir2. cd - 切换目录 用法: cd 目录路径返回上一级目录: C:\Users\YourUsername\Documents> cd …

c语言中的字符函数

1.字符分类函数 c语言中有一系列函数是专门做字符分类的&#xff0c;也就是一个字符属于什么类型的字符。这些函数的使用需要包含一个头文件是ctype.h 可能你看这些感觉很懵&#xff0c;我以islower举例 #include<ctype.h> int main() {int retislower(A);printf("…

Android OTA 升级基础知识详解+源码分析

前言&#xff1a; 本文仅仅对OTA升级的几种方式的概念和运用进行总结&#xff0c;仅在使用层面对其解释。需要更详细的内容我推荐大神做的全网最详细的讲解&#xff1a; https://blog.csdn.net/guyongqiangx/article/details/129019303?spm1001.2014.3001.5502 三种升级方式…

编码器原理和功能介绍

目录 概述 1 编码综述 2 增量式旋转编码器 2.1 功能介绍 2.2 实现原理 3 绝对式编码器 3.1 实现原理 3.2 二进制编码器 3.3 格雷码编码器 4 旋转式编码器原理 5 编码器基本参数 5.1 分辨率 5.2 精度 5.3 最大响应频率 5.4 信号输出形式 6 自制简易编码器 概述 …

matlab入门基础笔记

1、绘制简单三角函数&#xff1a; 绘制正弦曲线和余弦曲线。x[0:0.5:360]*pi/180; plot(x,sin(x),x,cos(x)); &#xff08;1&#xff09;明确x轴与y轴变量&#xff1a; 要求为绘制三角函数&#xff1a; X轴&#xff1a;角度对应的弧度数组 Y轴&#xff1a;对应sin(x)的值 求…

【数据结构C++】表达式求值(多位数)课程设计

&#x1f4da;博客主页&#xff1a;Zhui_Yi_ &#x1f50d;&#xff1a;上期回顾&#xff1a;图 ❤️感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;&#xff0c;您的三连就是我持续更新的动力❤️ &#x1f387;追当今朝天骄&#xff0c;忆顾往昔豪杰。 …

使用Ollama+OpenWebUI本地部署阿里通义千问Qwen2 AI大模型

&#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;AI大模型部署与应用专栏&#xff1a;点击&#xff01; &#x1f916;Ollama部署LLM专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年6月17日22点50分 &#x1f004;️文章质量&#xff…

函数(下) C语言

函数下 嵌套调用和链式访问1. 嵌套调用2. 链式访问 函数的声明和定义1. 单个文件2. 多个文件3. static 和 extern3.1 static 嵌套调用和链式访问 1. 嵌套调用 嵌套调用就是函数之间的互相调用&#xff0c;每个函数就像⼀个乐高零件&#xff0c;正是因为多个乐高的零件互相无缝…