草庐IT

关于Shader KeyWord的整理

Mr.QSheng 2024-04-07 原文

关于Shader KeyWord的整理

关于Shader KeyWord的整理。源自于挺久之前做的Demo,今天翻出来整理一下。


文章目录


前言

关于Shader KeyWord的整理。源自于挺久之前做的Demo,今天翻出来整理一下。


一、KeyWord

在shader 编写中,我们常常会遇到这样的问题。某些显示功能我们想通过代码实现手动控制打开关闭。由此 变体应运而生。Unity shader 的变体定义有
multi_compile
shader_feature
shader_feature_local unity 2019之后才有
以上都可以实现功能的开关功能。但是他们的作用域是不一样的。 multi_compile作用于全局也可作用于局部,可以使用Shader.EnableKeyword(“XXXX”),将作用于全局,使用meshRender.material.EnableKeyword (“XXXX”)则作用于局部。shader_feature也同理,但在打包上差别就很大,以下会说明。
在定义上multi_compile A 将只定义一个变体A,而且默认打开,无法关闭。所以我们需要添加下划线,代表关闭状态
multi_compile _ A。
而定义 shader_feature A 将会默认定义变体_ 以及变体A。
Local keywords:
shader_feature和multi_compile的主要缺点是,定义的所有关键字都限制了Unity的全局关键字数量(256个全局关键字,加上64个本地关键字)。为了避免这个问题,我们可以使用不同的着色器变体指令:shader_feature_local和multi_compile_local。

shader_feature_local: 与 shader_feature类似, 但是仅限本shader使用
multi_compile_local: 与multi_compile类似, 但是限本shader使用

二、KeyWord查看

我们可以点击这里切换Debug模式,查看材质球上缓存的变体。以及其他数据。


选择shader文件,点击keyword,可以显示shader定义的变体。

点击Compile And Show Code的箭头可以查看变体组合


在frame debugger上,我们也能看到当前生效的变体。

三、KeyWordDemo

我们先直接上我们的demo代码。首先定义一个shader 代码如下:

Shader "Unlit/NewUnlitShader"
{

   SubShader {
		Pass {
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#pragma multi_compile MY_multi_1 
		#pragma multi_compile MY_multi_2
		#include "UnityCG.cginc"
		struct vertOut {
			 float4 pos : POSITION;
            };
		vertOut vert(appdata_base v)
		{
			vertOut o;
			o.pos = UnityObjectToClipPos(v.vertex);
			return o;
		}
		float4 frag(vertOut i):COLOR
		{
			float4 c = float4(0, 0, 0, 0);
			#if defined (MY_multi_1)
			c = float4(0, 1, 0, 0);//输出绿色
			#endif
			#if defined (MY_multi_2)
			c = float4(0, 0, 1, 0);//输出蓝色
			#endif
			return c;
		}
		ENDCG
		}	
	} 
}

代码如上,我们定义两个multi_compile全局变体 MY_multi_1, MY_multi_2。MY_multi_2的优先级高于MY_multi_1。当MY_multi_2 成立时,我们输出蓝色,当MY_multi_1 成立,并且MY_multi_2不成立时,我们输出绿色。当MY_multi_1, MY_multi_2都不生效时,我们直接输出黑色。
我们添加CS脚本,控制变体开关。

public class TestKeyWorld : MonoBehaviour
{
    public bool multi_1;
    public MeshRenderer meshRender;

    public void OnChangeJJJJ()
    {
        multi_1 = !multi_1;
       if (multi_1) {
            Shader.EnableKeyword("MY_multi_1");
            Shader.DisableKeyword("MY_multi_2");
            //meshRender.material.EnableKeyword ("MY_multi_1");
            //meshRender.material.DisableKeyword ("MY_multi_2");
        } else {
            Shader.EnableKeyword("MY_multi_2");
            Shader.DisableKeyword("MY_multi_1");
            //meshRender.material.EnableKeyword ("MY_multi_2");
            //meshRender.material.DisableKeyword ("MY_multi_1");
        }
    }
}

我们定义函数OnChangeJJJJ来控制变体的开关。
场景上我们定义两个面片A,B。 以及按钮。按钮的点击事件绑定我们的函数OnChangeJJJJ()
场景如下:

1.multi_compile

我们可以看到面片已经显示蓝色了。这是因为我们变体定义#pragma multi_compile MY_multi_1,不含下划线。那他的默认值就是开启状态。并且函数OnChangeJJJJ()控制不了变体的开关。
这时候我们修改变体定义,添加关闭选项”_“
#pragma multi_compile _ MY_multi_1
//#pragma multi_compile _ MY_multi_2 (注释该变体)

可以看到他显示黑色,即MY_multi_1并没有开启。我们点击按钮就可以控制变体的开启与关闭了。

我们可以看到时两个面板一起变绿,因为我们用的是multi_compile全局变体。变体控制我们用的是 Shader.EnableKeyword(“MY_multi_1”);
如果我们想使用multi_compile控制单个材质球变体开关,我们可以使用meshRender.material.EnableKeyword (“MY_multi_1”)。我们修改代码shader

#pragma multi_compile _ MY_multi_1
#pragma multi_compile _ MY_multi_2

修改函数OnChangeJJJJ

 public void OnChangeJJJJ()
    {
        multi_1 = !multi_1;
       if (multi_1) {
            //Shader.EnableKeyword("MY_multi_1");
            //Shader.DisableKeyword("MY_multi_2");
            meshRender.material.EnableKeyword("MY_multi_1");
            meshRender.material.DisableKeyword("MY_multi_2");
        } else {
            //Shader.EnableKeyword("MY_multi_2");
            //Shader.DisableKeyword("MY_multi_1");
            meshRender.material.EnableKeyword("MY_multi_2");
            meshRender.material.DisableKeyword("MY_multi_1");
        }
    }

再次运行游戏。我们可以看到右边的面板还是绿色,实际上他应该时黑色,应该时材质球的缓存导致的,当我们打包出来后或者重启Unity就会变成黑色的。左边面板功能正常。

我们打包试exe一下

可以看到功能正常。我们这里用的是multi_compile,打包时候会生成所有变体,无论当前没有用到。但是就是因为这东西会生成所有变体组合,当变体定义数量多时,变体组合成指数增长,内存会爆炸的。所以我们需要适当使用。

2.shader_feature

我们尝试使用 shader_feature 实现以上效果。修改shader 文件

#pragma shader_feature MY_multi_1
#pragma shader_feature MY_multi_2

修改cs函数

 public void OnChangeJJJJ()
    {
        multi_1 = !multi_1;
       if (multi_1) {
            //Shader.EnableKeyword("MY_multi_1");
            //Shader.DisableKeyword("MY_multi_2");
            meshRender.material.EnableKeyword("MY_multi_1");
            meshRender.material.DisableKeyword("MY_multi_2");
        } else {
            //Shader.EnableKeyword("MY_multi_2");
            //Shader.DisableKeyword("MY_multi_1");
            meshRender.material.EnableKeyword("MY_multi_2");
            meshRender.material.DisableKeyword("MY_multi_1");
        }
}

变体定义 #pragma shader_feature MY_multi_1 不需要携带_下划线。他会默认定义。
运行由此,我们发现没什么异常。

我们打个包看看

我们可以看到他默认显示黑色,并且按钮没有反应。因为shader_feature 变体打包时候只会打进已编译的变体。shader_feature 的默认值是”_”,默认是不开启的。
为解决以上问题ShaderVariants 变体收集器,应运而生。


Demo中选择后

放到这里预加载

我们再打一次包试试

功能正常了。
我们已经明白了使用变体收集器ShaderVariants配合使用shader_feature可以很好的控制变体组合生成,排除不需要的变体。

四、变体收集器自动生成

变体生成规则如下:
shader_feature A
shader_feature B
变体Group如下
A,B,AB
直接贴代码。

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using System.IO;
using System.Reflection;
using System;
using UnityEngine.Rendering;
using System.Linq;

public class ShaderCollection : EditorWindow
{
    static Dictionary<string, List<ShaderVariantCollection.ShaderVariant>> ShaderVariantDict = new Dictionary<string, List<ShaderVariantCollection.ShaderVariant>>();
    public static List<string> GetAllRuntimeDirects()
    {
        //搜索所有资源
        List<string> directories = new List<string>();
        directories.Add("Assets");
        return directories;
    }
    private ShaderVariantCollection svc;
    readonly public static string ALL_SHADER_VARAINT_PATH = "Assets/AllShaders.shadervariants";

    static List<string> allShaderNameList = new List<string>();

    [MenuItem("ShaderTool/AutoShaderVariants")]
    public static void GenShaderVariant()
    {
        ShaderVariantDict = new Dictionary<string, List<ShaderVariantCollection.ShaderVariant>>();
        //先搜集所有keyword到工具类SVC
        toolSVC = new ShaderVariantCollection();
        var shaders = AssetDatabase.FindAssets("t:Shader", new string[] { "Assets", "Packages" }).ToList();
        foreach (var shader in shaders)
        {
            ShaderVariantCollection.ShaderVariant sv = new ShaderVariantCollection.ShaderVariant();
            var shaderPath = AssetDatabase.GUIDToAssetPath(shader);
            sv.shader = AssetDatabase.LoadAssetAtPath<Shader>(shaderPath);
            toolSVC.Add(sv);
            //
            allShaderNameList.Add(shaderPath);
        }

        var toolsSVCpath = "Assets/Tools.shadervariants";
        //防空

        File.WriteAllText(toolsSVCpath, "");
        AssetDatabase.DeleteAsset(toolsSVCpath);
        AssetDatabase.CreateAsset(toolSVC, toolsSVCpath);

        //搜索所有Mat
        var paths = GetAllRuntimeDirects().ToArray();
        var assets = AssetDatabase.FindAssets("t:Prefab", paths).ToList();
        var assets2 = AssetDatabase.FindAssets("t:Material", paths);
        assets.AddRange(assets2);
        List<string> allMats = new List<string>();

        //GUID to assetPath
        for (int i = 0; i < assets.Count; i++)
        {
            var p = AssetDatabase.GUIDToAssetPath(assets[i]);
            //获取依赖中的mat
            var dependenciesPath = AssetDatabase.GetDependencies(p, true);
            var mats = dependenciesPath.ToList().FindAll((dp) => dp.EndsWith(".mat"));
            allMats.AddRange(mats);
        }

        //处理所有的 material
        allMats = allMats.Distinct().ToList();

        float count = 1;
        foreach (var mat in allMats)
        {
            var obj = AssetDatabase.LoadMainAssetAtPath(mat);
            if (obj is Material)
            {
                var _mat = obj as Material;
                EditorUtility.DisplayProgressBar("处理mat", string.Format("处理:{0} - {1}", Path.GetFileName(mat), _mat.shader.name), count / allMats.Count);
                AddToDict(_mat);
            }

            count++;
        }

        EditorUtility.ClearProgressBar();
        //所有的svc
        ShaderVariantCollection svc = new ShaderVariantCollection();
        foreach (var item in ShaderVariantDict)
        {
            foreach (var _sv in item.Value)
            {
                svc.Add(_sv);
            }
        }

        AssetDatabase.DeleteAsset(ALL_SHADER_VARAINT_PATH);
        AssetDatabase.CreateAsset(svc, ALL_SHADER_VARAINT_PATH);
        AssetDatabase.Refresh();

    }
    public class ShaderData
    {
        public int[] PassTypes = new int[] { };
        public string[][] KeyWords = new string[][] { };
        public string[] ReMainingKeyWords = new string[] { };
    }

    //shader数据的缓存
    static Dictionary<string, ShaderData> ShaderDataDict = new Dictionary<string, ShaderData>();

   

    //添加Material计算
    static List<string> passShaderList = new List<string>();

    /// <summary>
    /// 添加到Dictionary
    /// </summary>
    /// <param name="curMat"></param>
    static void AddToDict(Material curMat)
    {
        if (!curMat || !curMat.shader) return;

        var path = AssetDatabase.GetAssetPath(curMat.shader);
        if (!allShaderNameList.Contains(path))
        {
            Debug.LogError("不存在shader:" + curMat.shader.name);
            Debug.Log(path);
            return;
        }

        ShaderData sd = null;
        ShaderDataDict.TryGetValue(curMat.shader.name, out sd);
        if (sd == null)
        {
            //一次性取出所有的 passtypes 和  keywords
            sd = GetShaderKeywords(curMat.shader);
            ShaderDataDict[curMat.shader.name] = sd;
        }

        var kwCount = sd.PassTypes.Length;
        if (kwCount > 2000)
        {
            if (!passShaderList.Contains(curMat.shader.name))
            {
                Debug.LogFormat("Shader【{0}】,变体数量:{1},不建议继续分析,后续也会跳过!", curMat.shader.name, kwCount);
                passShaderList.Add(curMat.shader.name);
            }
            else
            {
                Debug.LogFormat("mat:{0} , shader:{1} ,keywordCount:{2}", curMat.name, curMat.shader.name, kwCount);
            }

            return;
        }

      
        List<ShaderVariantCollection.ShaderVariant> svlist = null;
        if (!ShaderVariantDict.TryGetValue(curMat.shader.name, out svlist))
        {
            svlist = new List<ShaderVariantCollection.ShaderVariant>();
            ShaderVariantDict[curMat.shader.name] = svlist;
        }

        //求所有mat的kw
        for (int i = 0; i < sd.PassTypes.Length; i++)
        {
            //
            var pt = (PassType)sd.PassTypes[i];
            ShaderVariantCollection.ShaderVariant? sv = null;
            try
            {
                string[] key_worlds = sd.KeyWords[i];

                //变体交集 大于0 ,添加到 svcList
                sv = new ShaderVariantCollection.ShaderVariant(curMat.shader, pt, key_worlds);
                SetShaderVariantKeyWorld(svlist, sv);
            }
            catch (Exception e)
            {
                Debug.LogErrorFormat("{0}-当前shader不存在变体(可以无视):{1}-{2}", curMat.name, pt, curMat.shaderKeywords.ToString());
                continue;
            }

 
        }
    }

    static void SetShaderVariantKeyWorld(List<ShaderVariantCollection.ShaderVariant> svlist, ShaderVariantCollection.ShaderVariant? sv)
    {
        //判断sv 是否存在,不存在则添加
        if (sv != null)
        {
            bool isContain = false;
            var _sv = (ShaderVariantCollection.ShaderVariant)sv;
            foreach (var val in svlist)
            {
                if (val.passType == _sv.passType && System.Linq.Enumerable.SequenceEqual(val.keywords, _sv.keywords))
                {
                    isContain = true;
                    break;
                }
            }

            if (!isContain)
            {
                svlist.Add(_sv);
            }
        }
    }


    static MethodInfo GetShaderVariantEntries = null;

    static ShaderVariantCollection toolSVC = null;

    //获取shader的 keywords
    public static ShaderData GetShaderKeywords(Shader shader)
    {
        ShaderData sd = new ShaderData();
        GetShaderVariantEntriesFiltered(shader, new string[] { }, out sd.PassTypes, out sd.KeyWords, out sd.ReMainingKeyWords);
        return sd;
    }

    /// <summary>
    /// 获取keyword
    /// </summary>
    /// <param name="shader"></param>
    /// <param name="filterKeywords"></param>
    /// <param name="passTypes"></param>
    /// <param name="keywordLists"></param>
    /// <param name="remainingKeywords"></param>
    static void GetShaderVariantEntriesFiltered(Shader shader, string[] filterKeywords, out int[] passTypes, out string[][] keywordLists, out string[] remainingKeywords)
    {
        //2019.3接口
        //            internal static void GetShaderVariantEntriesFiltered(
        //                Shader                  shader,                     0
        //                int                     maxEntries,                 1
        //                string[]                filterKeywords,             2
        //                ShaderVariantCollection excludeCollection,          3
        //                out int[]               passTypes,                  4
        //                out string[]            keywordLists,               5
        //                out string[]            remainingKeywords)          6
        if (GetShaderVariantEntries == null)
        {
            GetShaderVariantEntries = typeof(ShaderUtil).GetMethod("GetShaderVariantEntriesFiltered", BindingFlags.NonPublic | BindingFlags.Static);
        }

        passTypes = new int[] { };
        keywordLists = new string[][] { };
        remainingKeywords = new string[] { };
        if (toolSVC != null)
        {
            var _passtypes = new int[] { };
            var _keywords = new string[] { };
            var _remainingKeywords = new string[] { };
            object[] args = new object[] { shader, 256, filterKeywords, toolSVC, _passtypes, _keywords, _remainingKeywords };
            GetShaderVariantEntries.Invoke(null, args);

            var passtypes = args[4] as int[];
            passTypes = passtypes;
            //key word
            keywordLists = new string[passtypes.Length][];
            var kws = args[5] as string[];
            for (int i = 0; i < passtypes.Length; i++)
            {
                keywordLists[i] = kws[i].Split(' ');
            }

            //Remaning key word
            var rnkws = args[6] as string[];
            remainingKeywords = rnkws;
        }
    }
}


点击这里自动收集变体组合。
我们还想在打包时候输出shader信息
通过实现接口IPreprocessShaders完成
直接贴代码

using System.Collections;
using System.Collections.Generic;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;

public class MyCustomBuildProcessor : IPreprocessShaders
{
    ShaderKeyword m_Blue;

    public MyCustomBuildProcessor()
    {
        m_Blue = new ShaderKeyword("_BLUE");
    }

    public int callbackOrder { get { return 0; } }

    public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data)
    {
        System.Text.StringBuilder sb = new System.Text.StringBuilder();
        sb.AppendFormat("shader={3}, passType={0}, passName={1}, shaderType={2}\n",
            snippet.passType, snippet.passName, snippet.shaderType, shader.name);

        for (int i = 0; i < data.Count; ++i)
        {
            var pdata = data[i];
            sb.AppendFormat("{0}.{1},{2}: ", i, pdata.graphicsTier, pdata.shaderCompilerPlatform);
            var ks = pdata.shaderKeywordSet.GetShaderKeywords();
            foreach (var k in ks)
            {
                sb.AppendFormat("{0}, ", k.ToString());
            }
            sb.Append("\n");
        }
        Debug.Log(sb.ToString());
    }
}

总结

以上就是今天要讲的内容,如有错误欢迎指出。

有关关于Shader KeyWord的整理的更多相关文章

  1. ruby-on-rails - 关于 Ruby 的一般问题 - 2

    我在我的rails应用程序中安装了来自github.com的acts_as_versioned插件,但有一段代码我不完全理解,我希望有人能帮我解决这个问题class_eval我知道block内的方法(或任何它是什么)被定义为类内的实例方法,但我在插件的任何地方都找不到定义为常量的CLASS_METHODS,而且我也不确定是什么here,并且有问题的代码从lib/acts_as_versioned.rb的第199行开始。如果有人愿意告诉我这里的内幕,我将不胜感激。谢谢-C 最佳答案 这是一个异端。http://en.wikipedia

  2. ruby - 我怎样才能更好地了解/了解更多关于 Ruby 的知识? - 2

    按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭9年前。我最近开始学习Ruby,这是我的第一门编程语言。我对语法感到满意,并且我已经完成了许多只教授相同基础知识的教程。我已经写了一些小程序(包括我自己的数组排序方法,在有人告诉我谷歌“冒泡排序”之前我认为它非常聪明),但我觉得我需要尝试更大更难的东西来理解更多关于Ruby.关于如何执行此操作的任何想法?

  3. ruby - 关于 Ruby 中 Dir[] 和 File.join() 的混淆 - 2

    我在Ruby中遇到了一个关于Dir[]和File.join()的简单程序,blobs_dir='/path/to/dir'Dir[File.join(blobs_dir,"**","*")].eachdo|file|FileUtils.rm_rf(file)ifFile.symlink?(file)我有两个困惑:首先,File.join(@blobs_dir,"**","*")中的第二个和第三个参数是什么意思?其次,Dir[]在Ruby中有什么用?我只知道它等价于Dir.glob(),但是,我对Dir.glob()确实不是很清楚。 最佳答案

  4. elasticsearch源码关于TransportSearchAction【阶段三】 - 2

    1.回顾.TransportServicepublicclassTransportServiceextendsAbstractLifecycleComponentTransportService:方法:1publicfinalTextendsTransportResponse>voidsendRequest(finalTransport.Connectionconnection,finalStringaction,finalTransportRequestrequest,finalTransportRequestOptionsoptions,TransportResponseHandlerT>

  5. 关于Qt程序打包后运行库依赖的常见问题分析及解决方法 - 2

    目录一.大致如下常见问题:(1)找不到程序所依赖的Qt库version`Qt_5'notfound(requiredby(2)CouldnotLoadtheQtplatformplugin"xcb"in""eventhoughitwasfound(3)打包到在不同的linux系统下,或者打包到高版本的相同系统下,运行程序时,直接提示段错误即segmentationfault,或者Illegalinstruction(coredumped)非法指令(4)ldd应用程序或者库,查看运行所依赖的库时,直接报段错误二.问题逐个分析,得出解决方法:(1)找不到程序所依赖的Qt库version`Qt_5'

  6. H2数据库配置及相关使用方式一站式介绍(极为详细并整理官方文档) - 2

    目录H2数据库入门以及实际开发时的使用1.H2数据库的初识1.1H2数据库介绍1.2为什么要使用嵌入式数据库?1.3嵌入式数据库对比1.3.1性能对比1.4技术选型思考2.H2数据库实战2.1H2数据库下载搭建以及部署2.1.1H2数据库的下载2.1.2数据库启动2.1.2.1windows系统可以在bin目录下执行h2.bat2.1.2.2同理可以通过cmd直接使用命令进行启动:2.1.2.3启动后控制台页面:2.1.3spring整合H2数据库2.1.3.1引入依赖文件2.1.4数据库通过file模式实际保存数据的位置2.2H2数据库操作2.2.1Mysql兼容模式2.2.2Mysql模式

  7. ruby - 关于 Ruby/ChefSpec 编码风格的反馈 - 2

    我是Ruby的新手,但过去两周我一直在对Chef测试进行大量研究。该测试使用ChefSpec和Fauxhai,但它看起来不是很“像ruby”,我希望社区能给我一些编码风格的建议。有没有更好的方法来编写这样的嵌套循环?Recipe/foo/recipes/default.rbpackage"foo"doaction:installendRecipe/foo/spec/default_spec.rbrequire'chefspec'describe'foo::default'doplatforms={"debian"=>['6.0.5'],"ubuntu"=>['12.04','10.04

  8. ruby - 关于 ruby​​ 类变量的困惑 - 2

    假设一个使用类变量的简单ruby​​程序,classHolder@@var=99defHolder.var=(val)@@var=valenddefvar@@varendend@@var="toplevelvariable"a=Holder.newputsa.var我猜结果应该是99,但输出不是99。我想知道为什么。由于类变量的范围是类,我假设@@var="toplevelvariable"行不会影响类中的变量。 最佳答案 @@var是Holder的类变量。而顶层的@@var不是Holder的同名类变量@@var,是你在创建类Obj

  9. 一文解决关于VLAN所有的疑惑 - 2

    一文解决关于VLAN所有的疑惑VLAN基本概念为什么需要VLAN?怎么在交换机上划分VLAN,VLAN的工作原理有了子网,已经隔离了广播,还需要VLAN干啥?只进行子网划分,不进行VLAN划分VLAN划分与子网划分附加VLAN信息的方法VLAN划分交换机的端口类型(Access和Trunk)一、访问链接二、汇聚链接汇聚链接VLAN间通信为什么要进行VLAN间通信?路由器实现VLAN间通信路由器和交换机的连接方式通信细节三层交换机实现VLAN间通信加速VLAN间通信三层交换机与路由器三层交换机路由器路由器和交换机配合构建LAN的实例使用VLAN设计局域网的特点VLAN增加网络的灵活性不使用VLA

  10. ruby - 关于 CoffeeScript 变量范围的困惑 - 2

    我正在尝试了解CoffeeScript变量的范围。根据文档:ThisbehavioriseffectivelyidenticaltoRuby'sscopeforlocalvariables.但是,我发现它的工作方式不同。在CoffeeScript中a=1changeValue=->a=3changeValue()console.log"a:#{a}"#Thisdisplays3在ruby中a=1deffa=3endputsa#Thisdisplays1有人能解释一下吗? 最佳答案 Ruby的局部变量(以[a-z_]开头)arerea

随机推荐