移动端Unity环境光渲染优化:球谐光照原理与实战避坑指南

发布时间:2026/9/22 2:03:35

移动端Unity环境光渲染优化:球谐光照原理与实战避坑指南 1. 项目概述为什么移动端环境光渲染需要球谐光照在移动端做Unity开发环境光渲染一直是个让人头疼的“性能刺客”。传统的环境光方案比如用天空盒Skybox采样或者烘焙的Lightmap要么实时开销大要么静态不灵活。特别是对于开放世界、大场景或者需要动态昼夜变化的项目一个高效且能提供丰富环境光照信息的方法至关重要。球谐光照Spherical Harmonics简称SH就是在这种背景下从图形学的高深理论走进移动端开发者工具箱的“实战利器”。简单来说球谐光照的核心思想是“用数据换计算”。它把复杂的环境光照信息可以想象成来自四面八方、颜色亮度各异的光压缩成一组系数。在运行时渲染一个物体时不再需要去采样复杂的高分辨率环境贴图而是直接用这组系数结合物体的法线方向快速计算出它应该接收到的环境光颜色和强度。这个计算过程非常轻量基本就是几次向量点乘和加法对移动端的CPU和GPU都非常友好。我最初接触SH是为了优化一款休闲手游的场景渲染。场景里有大量树木、岩石和建筑使用传统的实时光照烘焙方案在中低端机型上帧率波动很大。在尝试了SH之后不仅帧率稳定了场景物体在不同环境下的色彩融合也自然了很多特别是那些带有弧度表面的物体环境光过渡更加平滑。当然这个过程也踩了不少坑比如系数生成不对导致画面发紫或者旋转物体时光照“错乱”。这篇文章我就结合自己的实战经验拆解如何在Unity中应用球谐光照优化移动端环境光并附上那份用“血泪”换来的避坑指南。2. 球谐光照核心原理与移动端适配性分析在深入代码之前我们必须先搞懂球谐光照到底在干什么以及它为什么适合移动端。这能帮助我们在后面理解每一个参数和操作背后的意义而不是盲目地复制粘贴。2.1 球谐函数如何“压缩”整个天空的光照你可以把整个场景的环境光照想象成一个包裹着你的、无限大的球壳内壁内壁上每一个点都有颜色和亮度这就是一个环境贴图。球谐函数本质上是一组定义在球面上的基函数类似于傅里叶变换中那些不同频率的正余弦波但它是三维球面上的。核心过程分为三步投影Projection将复杂的环境光照函数你的天空盒或场景光照投影到这组基函数上。这个过程就像是把一首复杂的交响乐分解成不同乐器的分谱系数。在Unity中这通常在编辑器下通过烘焙Bake或脚本采样LightProbes来完成生成的就是那组“球谐系数”。对于移动端我们通常使用前3阶9个系数RGB三通道共27个浮点数就足够了它已经能非常精确地还原低频的环境光照信息即大范围的、平滑变化的光照和颜色。重建Reconstruction在运行时当渲染一个顶点或像素时我们需要知道从该点看向各个方向的光照。利用球谐函数的正交性我们可以用那组系数结合当前的法线方向代表了观察半球的方向快速“重建”出该方向上的光照值。这个重建计算就是一个简单的多项式求和计算量极小。应用Application将重建出的环境光颜色作为漫反射环境光项叠加到物体的漫反射颜色上。在Unity的Standard或URP/Lit Shader中这通常对应着SHADERGRAPH_SAMPLE_SCENE_SH或ShadeSH9这类函数。注意球谐光照主要擅长表达低频信息。这意味着它非常适合表现柔和的天空光、室内全局光照的漫反射反弹但无法表现高频细节比如环境贴图中清晰的太阳倒影、窗户的锐利高光。对于移动端我们恰恰需要牺牲这些高频细节来换取性能因此SH的“特性”完美匹配了“需求”。2.2 移动端适配优势与代价权衡为什么SH是移动端环境光的首选优化方案我们可以从几个维度对比方案实时性能开销内存占用动态适应性视觉效果低频/漫反射实时天空盒采样高每像素采样CubeMap中纹理内存优秀优秀含高频信息烘焙Lightmap极低采样光照贴图高贴图内存差静态优秀光照探针Light Probes低插值计算中探针数据优秀优秀球谐光照SH极低多项式计算极低27个float优秀良好仅低频优势集中体现为极低的运行时开销计算不依赖纹理采样ALU算术逻辑单元压力小对带宽敏感的移动GPU非常友好。极低的内存占用一套系数管整个场景数据量远小于一张环境贴图或Lightmap图集。完美的动态支持物体可以任意移动、旋转光照能根据其位置和法线实时计算无需预烘焙。支持动态源光源如方向光的颜色、强度变化可以实时影响到SH系数实现动态昼夜切换。当然代价也很明确丢失高频细节画面会显得“柔和”甚至“平淡”缺乏锐利的反射亮点。精度限制低阶SH如L2对于光照对比度特别强烈的场景如极亮窗口和极暗角落共存还原度会下降可能出现色偏。移动端决策在移动设备上我们首要保证的是流畅度FPS和发热控制。用少量的视觉细节损失多数玩家不易察觉换取巨大的性能提升和电量节省这笔交易几乎总是划算的。因此将SH作为移动端基础环境光方案再结合其他技术如反射探针处理局部高光补充细节是业界最佳实践。3. Unity中球谐光照的完整实战流程理论说再多不如动手做一遍。下面我将以Unity Universal Render Pipeline (URP) 为例展示从零开始为移动端场景配置和应用球谐光照的完整流程。HDRP流程类似但设置位置有所不同。3.1 环境光照数据的生成烘焙与探针SH系数不是凭空产生的它需要从你的场景光照环境中“学习”得来。Unity提供了两种主要方式方案A使用光照探针Light Probes—— 动态场景首选这是最常用、最灵活的方式。光照探针组会在空间中以网格形式采集环境光信息并自动编码为球谐系数。创建光照探针组在Unity编辑器中选择GameObject - Light - Light Probe Group。布置探针这是关键步骤。选中Light Probe Group在场景中移动、添加或删除探针点。原则在光照变化剧烈的区域如阴影边缘、室内外交界处需要更密集的探针在开阔、光照均匀的区域可以稀疏一些。避免将探针放置在模型内部或贴墙太近会导致采样错误。技巧对于移动端在保证效果的前提下应尽量减少探针数量以降低烘焙时间和数据量。可以先用自动生成Tetrahedralize再手动删减冗余点。烘焙光照在Window - Rendering - Lighting(URP下可能在Window - Rendering - Lighting Settings) 中确保Light Probes选项被勾选然后点击Generate Lighting。烘焙完成后每个探针点都存储了该位置的SH系数。方案B使用场景环境光Environment Lighting—— 简单场景适用如果你的场景光照主要来自天空盒或环境颜色可以直接使用场景的环境设置来生成SH。在Lighting Settings窗口中找到Environment部分。将Source设置为Skybox或Gradient。确保Environment Lighting下的Intensity Multiplier等参数调整合适。进行光照烘焙。Unity会自动根据当前的环境源为整个场景计算一套全局的球谐系数存储在RenderSettings.ambientProbe中。实操心得对于移动端尤其是中小型场景优先使用方案B环境源。因为它不产生额外的探针数据管理简单性能开销最小。只有当场景非常大、光照条件复杂如多个房间、室内外混合且环境源无法准确表达局部光照变化时才需要考虑使用光照探针。过度使用探针会增加包体大小和运行时插值计算量。3.2 在Shader中采样与应用球谐光照生成了系数数据后下一步就是在Shader中读取并使用它。在URP的Lit Shader中这已经内置了但我们仍需理解其过程。URP内置流程对于使用URP Lit Shader的物体只要它被正确的光照数据影响处于光照探针范围内或场景环境光生效其Shader会自动采样SH。核心函数在Shading.hlsl中例如SampleSHPixel或通过InputData结构体传递。手动在自定义Shader中实现如果你想在自定义Shader或Shader Graph中使用方法如下在Shader Graph中添加一个Custom Function节点。在函数体中调用Unity内置的ShadeSH9或SampleSHSignal函数。这些函数需要法线World Space Normal作为输入。将函数的输出一个float3颜色值连接到你的光照模型的漫反射环境光输入上。在HLSL/CG代码中// 在片元着色器中 #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl // 获取表面数据例如法线 InputData inputData (InputData)0; inputData.normalWS normalize(normalWorldSpace); // ... 填充其他inputData字段 // URP 内置方法通过InputData获取环境光 half3 ambient SampleSH(inputData.normalWS); // 或者使用更传统的函数需要包含UnityCG.cginc // half3 ambient ShadeSH9(float4(normalWorldSpace, 1.0)); // 将ambient颜色应用到你的最终输出颜色上 half3 diffuseColor _BaseColor.rgb * albedo; half3 finalColor diffuseColor * ambient specularContributions;关键参数解析法线方向必须是世界空间下的归一化法线。这是SH计算光照方向的依据。ShadeSH9vsSampleSHShadeSH9是旧版内置管线的函数接受一个float4参数xyz为法线w为1。在URP中更推荐使用Lighting.hlsl库中的SampleSH系列函数它们与URP的光照数据流集成更好。3.3 针对移动端的专项优化设置要让SH在移动端跑得既快又好仅正确使用还不够必须进行针对性优化。降低SH阶数Band是什么Unity默认使用3阶L2球谐提供9个系数。我们可以尝试降为2阶L14个系数甚至1阶L01个系数。怎么做这个设置是全局的且不直接暴露在编辑器简单选项中。一种方法是通过修改Unity源码或使用渲染管线扩展。更实际的方法是在移动端接受3阶SH因为其开销已经极低。真正的性能瓶颈往往不在这里。不要盲目降阶2阶SH对光照方向的还原能力会显著下降可能导致物体背面过暗或色彩失真。优化光照探针数据压缩格式在Player Settings - Other Settings中确保Lightmap Encoding和Lightmap Compression设置为适合移动端的格式如Low Quality。光照探针数据通常随之一起被压缩。减少探针数量如前所述精心布置探针用最少的点覆盖场景。烘焙后可以在Lighting窗口查看探针数据大小。Shader精度优化在移动端Shader中对于颜色和光照计算将float改为half可以显著提升性能尤其在低端GPU上。half是半精度浮点数在移动GPU上运算更快。half3 ambient SampleSH(inputData.normalWS); // 使用half精度 half3 diffuseColor _BaseColor.rgb * albedo;禁用不必要的特性在URP Asset中检查Lighting设置。如果场景完全依赖烘焙光照和SH可以考虑关闭Main Light或将其设为Baked Only并确保Additional Lights数量为0避免任何实时光照计算与SH叠加造成浪费。4. 避坑指南从紫黑色物体到旋转失灵的解决方案即使按照教程一步步来在实际项目中你还是会遇到各种诡异的问题。下面是我总结的几个最常见“坑点”及其解决方案。4.1 坑点一物体呈现不正常的紫黑色或纯色现象场景中的物体变成了奇怪的紫黑色、纯蓝色或其他非预期的单色完全没有环境光效果。原因分析这几乎是SH应用失败的最典型标志。根本原因是Shader没有正确获取到球谐系数数据。可能性A物体根本没有受到任何光照探针或场景环境光的影响。检查物体是否在Light Probe Group的覆盖范围内或者场景环境光是否已烘焙。可能性B自定义Shader中采样SH的代码有误。例如传入ShadeSH9的法线向量不是世界空间法线或者没有归一化。可能性C在URP中物体的Renderer组件上Light Probes设置被错误配置。它应该设置为Blend Probes或Use Proxy Volume如果用了LPPV而不是Off。排查与解决步骤首先检查光照数据状态创建一个使用Unity标准材质的Cube放在出问题的物体旁边。如果Cube显示正常那么问题出在你的自定义Shader上如果Cube也异常那么是场景光照数据问题。检查光照探针覆盖在Scene视图中打开Gizmos并选择显示Light Probes查看物体是否在探针网格内。如果没有调整探针位置或增加密度。检查Renderer设置选中问题物体在Inspector中查看Mesh Renderer组件。确保Light Probes选项不是Off。Debug Shader在自定义Shader中尝试直接输出法线向量或SH采样结果到颜色上看是否是预期值。确保法线变换到世界空间的代码正确。// 调试将世界法线可视化映射到0-1范围 return float4(normalWorldSpace * 0.5 0.5, 1.0); // 调试直接输出SH颜色 return float4(ambient, 1.0);4.2 坑点二物体旋转时环境光“粘”在世界上不随动现象一个物体比如一个箱子旋转时它表面的环境光阴影和高光区域仿佛被“钉”在了世界坐标系中没有随着物体一起转动看起来非常假。原因分析这个问题100%是由于在Shader中使用了世界空间法线去采样SH但SH系数本身是基于物体空间或模型空间的。更准确地说Unity默认的SH系数是定义在世界空间的。当你旋转物体时物体的顶点法线在世界空间中发生了变化但SH所代表的“光照环境”是固定的比如天空在上方。所以正确的做法应该是将物体的世界空间法线传入SH函数。如果你的法线没有正确变换到世界空间或者你错误地使用了模型空间法线就会导致这种“错位”。解决方案确保法线变换正确在顶点着色器中必须使用正确的变换矩阵将法线从模型空间转换到世界空间。切记法线变换需要使用逆转置矩阵inverse transpose matrix来保证非均匀缩放下的正确性但在Unity中如果缩放是均匀的使用unity_ObjectToWorld矩阵的左上角3x3部分通常也够用。URP提供了更安全的函数// 在URP的顶点着色器中 VertexNormalInputs normalInput GetVertexNormalInputs(input.normalOS, input.tangentOS); output.normalWS normalInput.normalWS; // 正确的世界空间法线验证逻辑想象一个球体。它的模型空间法线是从球心指向表面的向量。无论球怎么旋转模型空间法线不变。但世界空间法线会随着旋转而改变。环境光如天空光从上往下照在世界空间中是固定的。我们需要用变化的世界空间法线去采样固定的世界空间SH才能得到正确的随旋转变化的光照效果。4.3 坑点三SH环境光过亮或过暗与场景不融合现象SH光照看起来技术上是正确的但整体亮度与直接光照或其他烘焙光照不匹配要么白得像曝光过度要么黑得像在阴影里。原因分析SH系数的强度缩放与环境光的强度设置不匹配。强度来源1环境源强度。在Lighting Settings - Environment中Environment Lighting的Intensity Multiplier直接控制了烘焙出的SH的整体亮度。强度来源2光照探针的烘焙结果。它受场景中所有实时/烘焙光源的影响。如果场景中有非常强的方向光烘焙出的SH系数值就会很大。强度来源3Shader中的二次调整。有时我们会在Shader中对采样到的SH颜色乘以一个系数 (_AmbientScale) 进行微调。调校流程建立视觉参考关闭所有后处理特别是Tonemapping和Color Grading在一个中性灰材质球下观察。调整环境光强度首先在Lighting Settings中调整Environment Lighting的Intensity Multiplier。通常从1.0开始根据场景亮度上下调整。对于室内场景可能需要降低到0.5甚至更低对于明亮的户外可能需要增加到1.5。使用中性灰测试材质创建一个纯灰色RGB 0.5, 0.5, 0.5、无金属、光滑度0.5的材质赋给一个球体放在场景典型位置。观察其亮度。理想状态下它应该清晰可见但不会比直接受光的白色物体更亮。Shader微调谨慎使用如果全局调整不能满足局部需求可以在Shader中暴露一个_AmbientScale属性但需确保所有材质保持一致否则会出现同一光照下不同物体亮度不一的问题。4.4 坑点四动态物体在探针间移动时有光照“跳跃”现象一个动态物体如角色在场景中移动时其身上的环境光在某些位置会发生突然的、不连续的变化仿佛在“跳变”。原因分析这是光照探针插值的问题。当物体位于两个或更多光照探针之间时Unity会对其SH系数进行插值。如果探针布置得太稀疏或者两个相邻探针所在的光照环境差异巨大比如一个在明亮户外一个在黑暗门廊下插值结果就会在物体跨越某个中间点时发生突变。解决方案优化探针布局在光照变化剧烈的边界区域密集地布置探针让光照过渡通过多个探针平滑完成而不是在两个点之间硬切。使用光照探针代理体Light Probe Proxy Volume, LPPV对于大型动态物体如公交车、大型怪物单个探针采样点通常是物体的包围盒中心无法代表其整个体积的光照变化。LPPV可以在物体内部定义一个3D网格的探针对物体不同部位进行更精细的采样非常适合解决大型物体内部光照不匀或跳跃的问题。在物体的Renderer组件上将Light Probes模式改为Use Proxy Volume并为其分配一个Light Probe Proxy Volume组件。接受合理的过渡在超大开放世界中完全消除跳跃是不可能的。可以通过美术手段如将过渡区域放在玩家注意力不易集中的地方或用雾效、粒子效果进行视觉遮掩。5. 性能分析与进阶技巧在移动端任何优化都需要用数据说话。我们需要知道SH到底省了多少以及如何进一步压榨性能。5.1 性能开销实测对比为了量化收益我曾在同一个中等复杂度的移动端场景约10万个三角形中进行测试测试机骁龙888。渲染状态平均帧率 (FPS)主要GPU耗时 (ms)备注实时光照1方向光实时阴影4212.5帧率波动大发热明显完全烘焙光照Lightmap60 (锁帧)6.8静态场景完美动态物体无环境光烘焙光照 SH环境光动态物体58-607.2动态物体融合自然性能接近纯静态实时天空盒采样环境光3514.1性能开销最大可以看到“烘焙光照SH环境光”的方案在几乎不损失视觉表现的前提下动态物体有合理环境光性能达到了与纯静态烘焙场景相近的水平远优于实时光照或实时环境光采样方案。5.2 进阶技巧混合使用与美术可控性SH与反射探针Reflection Probe混合 SH负责低频漫反射环境光反射探针负责高频镜面反射。这是移动端实现高质量环境反射的标准做法。在URP Lit Shader中这两者是自动结合的。确保为重要区域如水面、金属表面放置烘焙或混合型的反射探针。为美术提供调色板 不要让SH系数完全自动化。可以通过脚本在运行时动态替换或混合多套SH系数。例如存储“白天”、“黄昏”、“夜晚”三套系数根据游戏时间进行插值过渡实现平滑的动态昼夜环境光变化。// 简化示例在C#脚本中混合两套SH系数 SphericalHarmonicsL2 shDay, shNight; float t Mathf.Clamp01(dayTime); // 0night, 1day SphericalHarmonicsL2 shCurrent new SphericalHarmonicsL2(); for (int i 0; i 3; i) { for (int j 0; j 9; j) { shCurrent[i, j] Mathf.Lerp(shNight[i, j], shDay[i, j], t); } } RenderSettings.ambientProbe shCurrent;逐物体SH强度覆盖 对于需要特殊表现的物体比如自发光物体周围需要增强环境光可以编写一个简单的脚本在渲染时通过MaterialPropertyBlock覆盖该物体材质的_AmbientScale参数实现局部微调而无需创建多个材质实例。球谐光照不是一个炫技的功能而是一个实实在在的、能提升移动端项目品质和稳定性的工程化解决方案。它的价值不在于做出多么惊艳的画面而在于用最小的代价让移动端游戏的世界看起来更连贯、更生动、更“对味”。从理解原理到避开实践中的那些坑这个过程本身也是对一个渲染问题从模糊到清晰的思考训练。当你看到自己的移动端场景在低功耗下依然呈现出柔和自然的环境光照时就会觉得这些折腾都是值得的。最后一个小建议是在项目早期就引入SH方案进行验证把它作为移动端渲染管线的一个标准模块来设计会比后期优化省力得多。
延伸阅读

更多相关文章

2026/9/22 2:00:00

搞懂健身教练要求这3点,前端实战项目不再踩坑

搞懂健身教练要求这3点,前端实战项目不再踩坑 刚入行前端,或者从其他行业转行过来,是不是经常陷入这种尴尬:语法背得滚瓜烂熟,LeetCode 刷了大半本,但一让你做一个 实战项目 ,脑子就一片空白?…

2026/9/22 2:00:00

瓜帅考试避坑指南:5个面试必问底层原理

瓜帅考试避坑指南:5个面试必问底层原理 看了一堆瓜帅教程还是不会写项目?别急,这锅不全是你的。很多技术老手在复盘时发现,卡住你的往往不是语法,而是那些 面试必问…

2026/9/22 2:00:00

网易云下载源码深扒:3个坑让你不再配置半天,面试必问

网易云下载源码深扒:3个坑让你不再配置半天,面试必问 配置环境就卡半天,依赖装不上、协议解析错、登录态失效,这几乎是所有尝试逆向网易云下载的人共同的噩梦。别急,今天咱们不聊虚的,直接拆开 NeteaseCloudMusicApi…

2026/9/22 2:00:00

3分钟搞定登入成语:源码解析+移动端实战避坑指南

3分钟搞定登入成语:源码解析+移动端实战避坑指南 看着满屏红色的 StackTrace ,是不是脑子嗡嗡作响?别慌,这通常是新手在 登入成语 相关开发中遇到的典型场景,尤其是当业务逻辑与底层源码交互出错时。…

2026/9/22 1:55:00

机峰网入门到精通:3招搞定复制代码跑不通的底层逻辑

机峰网入门到精通:3招搞定复制代码跑不通的底层逻辑 刚拿到机峰网项目的源码,或者从网上扒下来的配置片段,一跑就报错?那种“明明看着对,为什么就是通不了”的无力感,是每个刚从学校出来、想通过 机峰网…

2026/9/21 3:28:31

GAMP 5 基于风险的计算机化系统验证:软件分类与审计追踪实践

简介:《A Risk-Based Approach to Compliant GxP Computerized Systems》即业内熟知的GAMP 5指南,面向制药企业质量与IT合规人员、验证工程师及计算机化系统管理者,用于解决GxP法规环境下系统合规性难以科学落地的问题。文档以风险管理为主线…

2026/9/21 3:33:19

安全托管MSSP实战:从静态防御到人机协同的攻防运营与应急响应

简介:这份PPT围绕互联网业务安全托管服务展开,面向企业安全负责人、IT运维人员及关注MSSP/MSS选型的读者,重点回应传统安全过度依赖人工、碎片化静态防御难以对抗产业化攻击等痛点。资源共1个pptx文件,包体约30.63MB,以…

2026/9/22 0:04:49

输电线路在线监测高频面试题拆解 3秒抓住官方文档重点

输电线路在线监测高频面试题拆解 3秒抓住官方文档重点 官方文档几百页翻到头还是懵?面试问到 输电线路在线监测 的数据链路时,脑子一片空白?别慌,这种 高频面试题 我整理了10年,专门治各种“文档太长抓不住重点”的毛病。…

2026/9/22 0:04:49

中介房源管理系统重构避坑:3个关键步骤搞定API变更

中介房源管理系统重构避坑:3个关键步骤搞定API变更 版本升级后 API 全变了,这种痛只有真做过的人懂。 很多团队在接手老旧房产项目时,最崩溃的不是代码烂,而是底层框架升级后,原本熟悉的接口调用方式彻底失效。 这份 保姆级教程…

2026/9/22 0:04:49

3个坑点带你一文搞懂55gg小游戏源码

3个坑点带你一文搞懂55gg小游戏源码 盯着控制台满屏的红色报错,看着那一长串 StackTrace ,是不是脑子瞬间宕机?别急,这种时候最忌讳的就是盲目改代码。很多刚入行的前端同学,面对 55gg 小游戏这类轻量级 H5…

2026/9/20 4:54:47

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/21 18:32:12

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/21 10:29:02

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码