刚接触bgfx的开发者,十有八九会卡在shader编译这一步。明明写的是HLSL,在Visual Studio里也能正常调,可一进bgfx工程就报一堆莫名错误,或者干脆编译出来渲染黑屏。这篇文章我就专门聊“bgfx shader编译(Windows下HLSL为例)”这件事,从工具链搭建、shader编写规范、shaderc命令解析,到运行时加载和问题排查,完整走一遍。准备入坑bgfx图形开发、或者已经在用但想彻底搞懂shader链路的朋友,都可以参考。我不会把命令参数全列一遍,而是把“为什么这么编译”背后的逻辑讲清楚,这样你换到Linux、Android甚至Vulkan后端时,也能自己推断该用什么参数。
1. 先搞懂bgfx的shader编译流水线
1.1 一份着色器源码是怎么变成DXBC的
bgfx的shader编译和普通DirectX工程最大的区别在于,它不是直接把HLSL编译成你能丢给D3D的字节码就完事。bgfx要同时支持D3D11、D3D12、OpenGL、Vulkan、Metal这些后端,它自己的runtime不可能为每种后端维护一套完全不同的shader加载逻辑,所以它做了一个抽象层。
你在bgfx工程里写的shader,本质上是“bgfx方言”的HLSL/GLSL。源码顶部要么写着#include <bgfx_shader.sh>,要么使用了$input、$output这种以美元符号开头的预处理器指令。这些语法真正的HLSL编译器不认识,它认识的是bgfx附带的那套头文件。整个编译链路大致是这样:源文件先由shaderc工具做预处理,把bgfx_shader.sh里的宏展开,再根据目标平台生成对应的语言形态,最终翻译成目标后端需要的字节码。
以Windows + D3D11为例,最终输出的.bin文件内部负载就是DXBC字节码。但要注意,这个bin并不等同于你用fxc.exe编译出来的.cso或.dxbc文件,bgfx在bin文件外层还包了一个自己定义的文件头,包含了版本号、校验信息、shader长度等字段。加载时runtime会先检查这个文件头,确认格式和当前renderer匹配,才会把里面的DXBC负载交给D3D。
这就回答了一个新手常问的问题:能不能把我在VS里右键编译出来的HLSL字节码直接拿过来用?答案是不能,因为bgfx的runtime根本不认识那种裸格式。你得经过shaderc重新走一遍抽象层流程,输出bgfx定义的封装格式。
1.2 为什么要做这么一层抽象,而不是各平台各写一份
很多从Unity、Unreal转过来的人会有一个直觉:跨平台shader不就是用#ifdef写分支吗?写个#ifdef D3D11、#ifdef VULKAN,然后分别调不同语法不就行了。
问题在于,HLSL和GLSL的差异不只是几个函数名,而是从变量声明、语义绑定、资源绑定位一直到矩阵运算顺序都不同。靠手工#ifdef维护,shader越多,维护成本越高。bgfx的做法是提供一套公用的“词汇表”,把两边都有的概念统一命名:vec3、vec4、mat4、texture2D、gl_Position、gl_FragColor、mul。然后bgfx_shader.sh针对不同后端把这些词映射成真正HLSL或GLSL的写法和内置函数。
你可以把bgfx_shader.sh当成一本“方言词典”。shaderc在解析源码时,会自动根据--platform windows和-p vs_5_0/ps_5_0这类参数定义BGFX_SHADER_LANGUAGE_HLSL宏,于是一份源码在不同的编译参数下,会产生完全不同的实际代码。真正做到了“源码一份,多端复用”。这在工程上的价值非常大,尤其是你手里有几十个shader的时候,不必为每个平台维护一份拷贝。
1.3 平台、编译参数和runtime后端的对应关系
理解平台、profile和runtime后端的关系,是避开“编译通过却黑屏”的关键。shaderc的--platform参数不是最终决定输出格式的唯一条件,真正决定输出格式的是-p后面的profile。常用对应关系如下:
| 运行时后端 | 编译参数举例 | 输出内容 |
|---|---|---|
| Direct3D11 / Direct3D12 | --platform windows -p vs_5_0/ps_5_0 | DXBC字节码,封装在bgfx bin内 |
| OpenGL | --platform linux -p glsl | GLSL文本,以C字符串形式存在bin负载里 |
| Vulkan | --platform windows -p spirv15 | SPIR-V字节码 |
| Metal | --platform osx -p metal | Metal后端可识别的字节码 |
这里最容易踩的坑是:你编译shader时一切正常,但运行时用的是另一个renderer。比如你在Windows上默认初始化了D3D11后端,却加载了一份用-p glsl编译的bin,runtime会直接拒绝使用,表现就是物体不渲染,或者控制台出现backend mismatch的日志。
我在实际项目里的习惯是:明确调用bgfx::init时传入指定的renderer类型,比如bgfx::RendererType::Direct3D11,然后编译shader时严格按这个renderer选profile。Windows上一般用vs_5_0/ps_5_0最稳,因为D3D11和D3D12后端都接受DXBC,而且vs_5_0对硬件要求已经不高,兼容性比vs_4_0更好。
2. 环境准备:拿到能用的shaderc
2.1 两种获取shaderc的方式
编译bgfx shader离不开shaderc这个命令行工具。获取方式有两种:直接使用官方预编译的二进制,或者自己从源码构建。预编译的好处是省事,适合只想跑通流程的读者,版本一般也比较稳定。但如果你修改了bgfx源码、或者需要跟进最新主分支的特性,就必须自己编译。
自己编译需要准备三个仓库,bgfx本身依赖bx和bimg。这三个仓库要放在同级目录下,比如libs/bgfx、libs/bx、libs/bimg。整个构建工作如果用premake5,大概是这样:
cd bgfx premake5.exe --with-tools vs2022然后打开生成的Visual Studio解决方案,找到Tools文件夹下的shaderc工程,编译Release版本。输出通常在tools/bin/windows/下,文件名是shaderc.exe。
如果不想手动打开VS,bgfx仓库的scripts目录下还提供了一些现成脚本,比如build_vs2022.bat,运行后会自动把依赖拉起来并编译工具链。我第一次自己构建时就是直接用这个脚本,比手敲premake命令省心不少。
还有一种方式是cmake,新版bgfx支持直接配置:
cmake -S . -B build -DBGFX_BUILD_TOOLS=ON cmake --build build --target shaderc --config Release两条路都通,喜欢哪个用哪个。个人建议第一次跑通流程可以走premake,因为官方示例和文档基本都是这个姿势。
2.2 把运行环境一次配好:include路径和d3dcompiler
拿到shaderc.exe之后,第一件事不是急着编译自己的shader,而是确认它能找到bgfx的公共头文件。编译任何bgfx shader都需要bgfx_shader.sh,这个文件的路径决定了你-i参数要指向哪里。我见过有些版本它放在include/目录,有些工程示例里又直接引用src/目录下的文件,稳妥的办法是编译命令里把两个目录都加进include搜索路径:
shaderc.exe -f input.sc -o output.bin -i C:\libs\bgfx\include -i C:\libs\bgfx\src -i C:\libs\bgfx\examples\common ...反正-i可以重复指定,多给一个目录只会有益无害。如果源码里还include了example里的公共shader头文件,比如common.sh,那就必须加上examples/common。
Windows下还有第二个坑:shaderc生成DXBC字节码时,运行时需要动态加载d3dcompiler_47.dll。绝大多数Windows系统自带这个DLL,但某些精简版系统、或者CI容器里会缺失。表现是编译命令一跑就报类似“failed to load D3DCompiler”的错误。解决方式也简单,从安装了DirectX SDK或者带D3D的机器上找到d3dcompiler_47.dll,放到shaderc.exe同目录下即可。
验证环境是否就绪,最省事的方法是找bgfx官方examples里的某个shader源文件,用examples自带的那组编译参数试跑一次。如果examples里的shader能正常编译出bin,说明你的include路径和DLL环境都没问题,接下来才值得继续往下折腾。
3. 用HLSL写bgfx shader时必须知道的规范
3.1 bgfx_shader.sh到底替你做了什么
很多人第一次打开bgfx的shader源文件,会觉得自己看的是“长得像GLSL但是又有点HLSL影子”的奇怪代码。比如gl_Position、gl_FragColor、texture2D这些名字,在真正的HLSL里根本不存在。这些其实都是bgfx抽象层提供的“替身”。
bgfx_shader.sh内部按BGFX_SHADER_LANGUAGE_*宏划分了好几个分支。当你用--platform windows -p vs_5_0编译时,shaderc会自动定义BGFX_SHADER_LANGUAGE_HLSL,于是头文件里所有抽象符号都会被映射成HLSL的对应物。例如:
vec3映射成float3texture2D(s_tex, uv)映射成s_tex.Sample(s_tex##_sampler, uv)这类写法gl_Position映射成SV_Positiongl_FragColor映射成SV_Target0
这层翻译不是运行时发生的,而是在编译期就已经完成。所以你写shader时要遵守bgfx的“方言”,而不是想当然地直接写原生HLSL关键字。比如你在shader里写了一个return float4(1,0,0,1);当像素输出,这在纯HLSL里合法,但在bgfx抽象层里不会识别成最终输出,正确姿势是给gl_FragColor赋值。
3.2 $input/$output和varying.def.sc的联动关系
bgfx的shader有一个独立的“变量定义文件”,通常叫varying.def.sc。它的作用相当于一份跨平台协议:定义顶点shader的输入属性(a_前缀)、顶点shader输出到像素shader的varying(v_前缀),并规定语义绑定。
一个典型的varying.def.sc长这样:
vec3 a_position : POSITION; vec4 a_color0 : COLOR0; vec2 v_texcoord0 : TEXCOORD0;而shader源文件顶部会有对应的声明:
$input a_position, a_color0 $output v_texcoord0 #include <bgfx_shader.sh>$input和$output是shaderc的预处理器指令。$input a_position表示这个shader会使用顶点属性a_position,$output v_texcoord0表示顶点shader要输出一个名为v_texcoord0的varying给像素shader。shaderc会把这两行指令和varying.def.sc里的定义自动拼成合适的结构体代码,再参与后续编译。
如果varying.def.sc里没定义某个v_texcoord1,但shader里写了$output v_texcoord1,编译会直接报错。反过来,定义了却没用到一般不会报错,但会有冗余。这就是为什么在调shader时报错信息里会出现v_texcoord0、a_position这些名字,因为这些名字是跨C++顶点布局和shader代码之间的“合同”。
3.3 uniform和采样器的声明方式
bgfx的uniform声明和原生HLSL也不一样。在bgfx shader里,你会看到这样的代码:
uniform vec4 u_color; uniform mat4 u_mvp; SAMPLER2D(s_texColor, 0);uniform vec4 u_color并不是真的向D3D暴露一个cbuffer成员,而是bgfx抽象层的写法。编译成HLSL后端时,shaderc会把它们打包进一个名为uniform的cbuffer里,并且给每个变量分配register。SAMPLER2D(s_texColor, 0)的第二个参数0是bind point,也就是纹理的注册槽位。
这带来一个非常容易踩的坑:C++端createUniform传的字符串名字必须和shader里的变量名完全一致,大小写都不能错。bgfx运行时是靠名字去查找shader里的uniform,而不是靠register索引。所以shader里写u_color,C++端就要bgfx::createUniform("u_color", ...),写成U_COLOR或者ucolor都会导致uniform绑定不上,shader在驱动里用的是默认值0,最终渲染结果就是黑的或者全透明的。
采样器也是这样,SAMPLER2D(s_texColor, 0)对应C++端bgfx::createUniform("s_texColor", bgfx::UniformType::Sampler)。名字对不上,纹理采样结果就是全0。
3.4 HLSL语法在bgfx抽象层里的几个易错点
第一个易错点是矩阵乘法。HLSL的mul(a, b)和GLSL的a * b的运算顺序习惯不同。bgfx在bgfx_shader.sh里统一提供了mul宏,你在shader里应该一直用mul(u_mvp, vec4(pos, 1.0))而不是自己用*符号去乘矩阵和向量。否则同一份shader,在D3D后端正常,切到GL后端就出现矩阵错乱,物体全部变形。
第二个易错点是函数名差异。GLSL的fract对应HLSL的frac,GLSL的texture对应HLSL的Texture2D.Sample。bgfx抽象层统一采用fract、texture2D这类写法。你如果在shader里随手写了frac,用D3D后端编译没问题,但换到GL后端时bgfx_shader.sh找不到这个宏,直接报编译错误。
第三个易错点是充分理解gl_FragColor的抽象。写颜色输出时,不要试图在同一个shader里既用gl_FragColor又用HLSL的SV_Target语义。尤其在fragment shader里,老老实实用gl_FragColor = vec4(...)这一套,让抽象层去处理。这东西有点像程序员写跨平台UI代码,不要试图绕开框架层去直接操作底层窗口句柄,框架会替你处理差异,前提是你得遵循它的规则。
4. 完整实操:从写代码到运行,全程走一遍
4.1 准备一个编译用的shader工程目录
我建议从一开始就建一个固定目录结构,方便后续自动化构建。我自己的习惯是这样的:
myproject/ shaders/ varying.def.sc vs_simple.sc fs_simple.sc build_shaders.bat bin/build_shaders.bat用来批量执行编译命令,bin/专门存生成的bin文件。
现在写一个最简单的带纹理的shader。varying.def.sc:
vec3 a_position : POSITION; vec2 v_texcoord0 : TEXCOORD0;顶点shadervs_simple.sc:
$input a_position $output v_texcoord0 #include <bgfx_shader.sh> uniform mat4 u_mvp; SAMPLER2D(s_texColor, 0); void main() { gl_Position = mul(u_mvp, vec4(a_position, 1.0)); v_texcoord0 = a_position.xy * 0.5 + 0.5; }这里我把顶点坐标简单映射到UV坐标,方便预览。像素shaderfs_simple.sc:
$input v_texcoord0 #include <bgfx_shader.sh> uniform vec4 u_color; SAMPLER2D(s_texColor, 0); void main() { vec4 tex = texture2D(s_texColor, v_texcoord0); gl_FragColor = tex * u_color; }注意fragment shader顶部只需要声明$input v_texcoord0,不需要$output,因为bgfx固定用gl_FragColor作为输出变量,已经在bgfx_shader.sh里定义好了。
4.2 用shaderc编译出bin的完整命令拆解
编译顶点shader的命令:
C:\libs\bgfx\tools\bin\windows\shaderc.exe -f vs_simple.sc -o ../bin/vs_simple.bin -i C:\libs\bgfx\include -i C:\libs\bgfx\src -i . --type vertex --platform windows -p vs_5_0 --varyingdef varying.def.sc --verbose编译像素shader的命令:
C:\libs\bgfx\tools\bin\windows\shaderc.exe -f fs_simple.sc -o ../bin/fs_simple.bin -i C:\libs\bgfx\include -i C:\libs\bgfx\src -i . --type fragment --platform windows -p ps_5_0 --varyingdef varying.def.sc --verbose逐参数过一遍:
-f指定输入shader源文件。-o指定输出bin文件路径。-i指定include目录,可以重复写多个。前面提到的bgfx_shader.sh能不能找到,就靠这个参数。--type告诉shaderc这是vertex还是fragment。一个常见的低级错误是顶点shader用了--type fragment,或者fragment shader用了vs_5_0的profile。顶点shader用vs_开头,像素shader用ps_开头。--platform windows指定目标平台。这个参数会影响shaderc自动定义的平台宏。-p vs_5_0 / ps_5_0是profile。Windows D3D11/12后端推荐用vs_5_0和ps_5_0。--varyingdef指到varying.def.sc的路径。shaderc靠它来解析$input/$output指令。--verbose打开详细输出,能看到shaderc实际怎么处理宏分支,排错很好用。
把这两条命令写进build_shaders.bat,以后每次改shader,双击运行一下就能批量生成bin文件。
4.3 在C++运行时加载和使用编译好的shader
C++端的加载流程分三步。第一步读文件,第二步创建bgfx的ShaderHandle,第三步把顶点和像素shader绑定成ProgramHandle。
读文件的方式很多,核心是把二进制内容拷贝进bgfx::Memory结构。最简单的手写读取:
const bgfx::Memory* loadShader(const char* path) { FILE* f = fopen(path, "rb"); if (!f) return nullptr; fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); const bgfx::Memory* mem = bgfx::alloc((uint32_t)sz); fread(mem->data, 1, sz, f); fclose(f); return mem; }然后创建program:
bgfx::ShaderHandle vs = bgfx::createShader(loadShader("bin/vs_simple.bin")); bgfx::ShaderHandle fs = bgfx::createShader(loadShader("bin/fs_simple.bin")); if (!bgfx::isValid(vs) || !bgfx::isValid(fs)) { // 处理shader创建失败,比如renderer不匹配、bin文件损坏 } bgfx::ProgramHandle program = bgfx::createProgram(vs, fs, true);createProgram的第三个参数传true,表示program创建成功后会销毁两个shader handle,不需要再手动调bgfx::destroy。这个函数是bgfx提供的重载版本,实际项目里非常常用。
如果你不想把bin文件放在磁盘上,想直接打进可执行文件里,可以先用bgfx自带的bin2c工具把bin转成C数组,然后在代码里用bgfx::makeRef创建Memory对象:
extern const uint8_t vs_simple_bin[]; bgfx::ShaderHandle vs = bgfx::createShader( bgfx::makeRef(vs_simple_bin, sizeof(vs_simple_bin)) );这种方式对移动端和发布工程很友好,不用考虑工作目录和资源路径问题。
4.4 把shader编译嵌进自动构建流程
手动双击build脚本的方式只适合前期调试,项目一旦上了规模,shader几十个甚至上百个,就必须把编译流程嵌进自动构建里。
最简单的方案是Visual Studio的pre-build event。在工程设置里添加命令:
call "$(ProjectDir)shaders\build_shaders.bat"也可以走CMake,用add_custom_command精确控制依赖关系:
add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/bin/vs_simple.bin COMMAND ${BGFS_SHADERC} -f vs_simple.sc -o ${CMAKE_CURRENT_BINARY_DIR}/bin/vs_simple.bin -i ${BGFS_ROOT}/include -i ${BGFS_ROOT}/src -i ${CMAKE_CURRENT_SOURCE_DIR}/shaders --type vertex --platform windows -p vs_5_0 --varyingdef varying.def.sc DEPENDS vs_simple.sc varying.def.sc WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/shaders )这样shader源文件一改动,重新编译工程时shader的bin会自动更新。能对构建过程做增量管理,是工程化的关键一步,比每次手动跑命令靠谱太多。
5. 编译报错与渲染黑屏,常见问题排查记录
5.1 高频编译ErrorMessage对照表
shaderc的报错信息本身比较直白,但很多错误背后的原因首次接触时容易抓瞎。我把实际中碰到的高频报错整理成一张表,方便对照排查:
| 报错特征 | 常见原因 | 解决方案 |
|---|---|---|
| fatal error: can't open file 'bgfx_shader.sh' | -i参数没包含bgfx头文件目录 | 把bgfx/include和bgfx/src加进include搜索路径 |
| unknown type 'vec4' / 'float4x4' | 源码文件忘了include bgfx_shader.sh,或者宏分支没生效 | 检查源码开头有没有#include <bgfx_shader.sh>,确认--platform和-p参数匹配 |
| varying variable 'v_texcoord0' is not defined | varying.def.sc里没有定义这个v_变量 | 在varying.def.sc里补全vec2 v_texcoord0 : TEXCOORD0; |
| Invalid profile 'vs_5_0' for fragment | 像素shader用了顶点shader的profile | fragment shader要用ps_5_0,vertex再用vs_5_0 |
| Failed to load d3dcompiler_47.dll | 系统缺少D3D编译器DLL | 手动把d3dcompiler_47.dll放到shaderc.exe同目录 |
| DXBC compile failed | 源码在翻译成HLSL后语法不合法 | 加--preprocess看预处理后的代码,定位具体错误行 |
5.2 编译通过但画面不对,问题多半出在varying和uniform
编译通过只代表语法没问题,不代表渲染结果正确。我调了这么久bgfx,遇到最多的一类问题是“shader编译成功、Program创建成功、物体就是画不出来”。
第一种典型情况是C++顶点布局和varying.def.sc不一致。比如你C++端顶点数据只有位置和颜色,但varying.def.sc里声明了a_texcoord0,shader里也用了$input a_texcoord0,那么顶点数据会被错误解释,三角形可能直接消失或者变成奇怪形状。排查方法是用RenderDoc截一帧,看vertex shader输入的顶点位置数据是否合理。如果数据位置全在零点附近,基本就是布局错位。
第二种典型情况是uniform名字不一致。shader里叫u_color,C++端创建uniform时打成u_Color,运行时不会报错,但uniform全部上了默认0。尤其当你的颜色是黑色或者alpha为0时,整个物体直接不可见。排查时可以给uniform设一个明显的非零初值,如果画面出现表现变化,说明这个名字对齐了。
第三种典型情况是矩阵变换问题。在Windows D3D后端下,mul(u_mvp, vec4(pos,1.0))如果写反了,渲染结果通常不是黑屏,而是物体位置错乱。这个问题和DX与GL的坐标系习惯有关,严重依赖你C++端MVP矩阵的生成方式。建议在shader里把所有矩阵乘法统一写成bgfx的mul宏,不要在部分shader里用*符号。
5.3 用--preprocess和RenderDoc一层层定位问题
遇到shader编译报错,第一步不是盯着错误信息猜,而是加--preprocess参数,让shaderc输出预处理后的展开代码:
shaderc.exe -f vs_simple.sc -o preprocessed.hlsl -i ... --type vertex --platform windows -p vs_5_0 --varyingdef varying.def.sc --preprocess打开preprocessed.hlsl,你会看到bgfx_shader.sh的宏分支到底把你写的源码翻译成了什么。如果某个变量名变成了未定义符号,或者某个函数展开后明显不对,问题一眼就出来了。这个技巧在排查“为什么D3D能过GL不能过”这种跨平台问题时特别有用。
如果编译、加载全都没问题,但渲染结果不对,下一步就是上RenderDoc。截帧后重点看三件事:Program是不是有效、VS输出的顶点的位置是否符合预期、绑定的纹理和uniform是不是有内容。一般来说,RenderDoc里如果某个绘制调用的VS/FS绑定失败,会直接高亮报错,比盲猜Shader问题要快得多。
另外bgfx允许注册一个回调接口,把驱动层的日志接出来。启动时设置bgfx::setPlatformData时,可以顺手挂一个回调,把traceV里收到的信息打到自己的日志系统。驱动编译shader失败、资源绑定出问题,往往在你自己的控制台还一片安静的时候,驱动已经吐了一堆错误。养成看驱动日志的习惯,排查效率能提高一个档次。
6. 从Windows走向全平台的一点经验
6.1 换到其他平台时只要改两个参数?
理解了前面的编译流水线,你会发现一份源码要输出到其他平台,其实改动量很小。编译命令里主要改--platform和-p,shader源码本身尽量保持平台无关。比如:
| 目标平台 | 编译参数 |
|---|---|
| Android OpenGL ES | --platform android -p essl |
| 桌面Linux OpenGL | --platform linux -p glsl |
| Windows Vulkan | --platform windows -p spirv15 |
| macOS/iOS Metal | --platform osx -p metal |
这里有个前提,就是你的shader代码没有自己偷偷写原生平台API。如果你在shader里用#ifdef BGFX_SHADER_LANGUAGE_HLSL分支出了一大段原生HLSL代码,换到GL后端时这段分支不会走,但你能不能保证GL分支的代码同样正确?所以我的建议是:除非遇到极其特殊的平台能力差异,否则尽量别写平台分支,让bgfx的抽象层去处理。
6.2 跨平台shader代码的素养
真正跨平台的shader,写起来有章法可循。变量命名保持一致,uniform统一用u_前缀,采样器用s_前缀,varying用v_前缀,顶点属性用a_前缀。这不仅是bgfx社区的惯例,也方便你跨shader文件复用代码。
矩阵运算统一走mul宏。颜色输出固定用gl_FragColor。纹理采样统一用texture2D。这些“统一”会让你在换平台时几乎不用改代码。我见过最省心的项目,CI脚本里对每个平台跑一遍shaderc命令,输出的bin文件直接按平台路径上传,源码里完全没有#ifdef,跨平台切换就是换一条编译命令的事。
说实话,我用bgfx这几年,最耗时的shader问题往往不是“编译不过”,而是“编译过了渲染不对”。所以给刚上手的朋友一个忠告:先把varying.def.sc、顶点布局、uniform命名这三样焊死在规范里,再谈性能优化。有了这套底子,Windows和Android之间切换shader基本就是改一条命令行参数的事。希望这篇随笔能帮你少踩几个坑。