
1. 项目概述从模型到像素的必经之路如果你在Unity里写过Shader或者哪怕只是看过一些教程大概率都见过类似UnityObjectToClipPos或者ComputeScreenPos这样的函数。它们就像Shader世界里的交通枢纽负责把顶点的“位置”这个最基本的信息从一个坐标系搬运到另一个坐标系。今天要聊的“裁剪空间”和“屏幕空间”就是这条搬运路线上最关键的两个中转站。不理解它们你的Shader就永远像是在黑箱里操作知其然不知其所以然遇到深度测试不对、屏幕特效偏移、后处理坐标错乱这些问题时只能靠玄学调试。简单来说这个过程是一条流水线你的模型顶点最初待在它自己的“模型空间”里比如一个立方体的中心在(0,0,0)。Unity的渲染管线会把它变换到“世界空间”决定这个立方体放在游戏世界的哪个位置再到“观察空间”以摄像机为原点的视角然后进入核心的“裁剪空间”。在这里GPU会进行裁剪操作决定哪些三角形在视野内。之后系统会自动进行“透视除法”将坐标归一化到“归一化设备坐标NDC”最后映射到“屏幕空间”决定这个顶点最终对应屏幕上的哪个像素。裁剪空间和屏幕空间正是这条流水线上承上启下的两个环节。裁剪空间决定了“什么东西能被看见”屏幕空间则回答了“看见的东西画在哪儿”。很多高级效果比如基于深度的雾效、屏幕空间反射SSR、边缘光以及UI Shader中精准的屏幕坐标对齐都深度依赖于对这两个空间的精确理解和计算。网上很多“魔改”Shader之所以不稳定问题往往就出在对这些空间变换的理解偏差上。2. 核心空间变换链路的深度解析要彻底搞懂裁剪空间和屏幕空间不能孤立地看必须把它们放在整个顶点变换的链条里。这个链条是渲染管线的数学基石。2.1 空间变换的全景图从模型的一个顶点到屏幕上的一个像素它经历了以下核心空间序列模型空间 (Model Space)顶点数据的原始坐标相对于模型自身的原点通常是模型中心或某个特定点。世界空间 (World Space)通过模型的变换矩阵位置、旋转、缩放将所有模型顶点转换到同一个全局坐标系中。这是UnityObjectToWorldDir等函数处理的阶段。观察空间 (View Space / Eye Space)以摄像机为原点摄像机前方为-Z轴或Z轴取决于API的坐标系。这是从世界空间乘以摄像机视图矩阵View Matrix得到的。在这个空间里透视投影的概念开始介入。裁剪空间 (Clip Space)这是本文的第一个主角。顶点坐标通过投影矩阵Projection Matrix从观察空间变换而来。这个空间的核心目的是为裁剪服务。归一化设备坐标 (NDC)这不是一个独立的“空间”而是裁剪空间坐标经过透视除法即(x/w, y/w, z/w)后得到的一个标准化坐标。在DirectX风格的API如Unity默认下NDC的x, y, z范围都是[-1, 1]。在这个立方体内的图元才会被光栅化。屏幕空间 (Screen Space)这是本文的第二个主角。NDC的xy坐标经过简单缩放和平移映射到具体的像素坐标。例如对于1920x1080的屏幕左下角可能是(0,0)右上角是(1920,1080)。深度值z通常也会被映射到一个特定的范围如[0, 1]用于深度缓冲。2.2 为什么需要裁剪空间投影矩阵的职责模型观察空间后为什么不直接画到屏幕上因为我们需要高效地丢弃视野之外的东西。在裁剪空间进行裁剪在数学和硬件实现上是最优的。裁剪空间的关键在于其定义一个顶点(x, y, z, w)在裁剪空间中如果满足-w x w,-w y w,-w z w那么这个顶点就在视锥体摄像机可见范围内。这个不等式定义了一个金字塔形透视投影或长方体正交投影的可见区域。投影矩阵Projection Matrix的魔法就在于此。它将观察空间中的视锥体“扭曲”成裁剪空间中的一个规则立方体即满足上述不等式的区域。对于透视投影离摄像机越远的物体其w值通常来自观察空间的-z值越大使得x,y的允许范围[-w, w]也越大这正好对应了透视中“近大远小”的视觉效果——远处的物体在裁剪空间里被“压缩”了。在Shader中我们常用的一行代码float4 clipPos UnityObjectToClipPos(v.vertex);其实等价于mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(v.vertex, 1.0)))。它一次性完成了从模型空间到裁剪空间的变换模型矩阵 - 视图矩阵 - 投影矩阵。注意这里有一个极易混淆的点。UNITY_MATRIX_VP是视图投影矩阵View-Projection Matrix它等于投影矩阵乘以视图矩阵。而unity_ObjectToWorld是模型矩阵。所以正确的串联顺序是clipPos mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, v.vertex))。UnityObjectToClipPos这个内置函数帮我们封装了这个计算并且正确处理了不同渲染路径下的差异强烈建议使用它而不是自己拼矩阵。2.3 从裁剪空间到屏幕空间透视除法与视口变换顶点着色器输出的clipPos之后GPU的固定功能管线会接手进行两个关键操作透视除法 (Perspective Division)对裁剪空间坐标的每个分量除以w分量ndcPos clipPos.xyz / clipPos.w。经过这一步坐标就变成了NDC。这个除法是“透视”效果的根本来源因为观察空间中的z值与距离相关被保留在了w分量中除法使得远处物体的xy坐标值变小。视口变换 (Viewport Transform)将NDC的xy坐标映射到屏幕像素坐标。这是一个线性映射screenPixelX (ndcPos.x * 0.5 0.5) * _ScreenParams.xscreenPixelY (ndcPos.y * 0.5 0.5) * _ScreenParams.y这里_ScreenParams.xy是屏幕的像素宽度和高度。(ndcPos.xy * 0.5 0.5)将范围从[-1, 1]映射到[0, 1]再乘以屏幕尺寸得到具体像素位置。在片段着色器中我们有时需要屏幕空间坐标。Unity提供了ComputeScreenPos函数来帮我们计算。它通常在顶点着色器中调用接收裁剪空间坐标clipPos计算出一个可以在片段着色器中进行透视插值后得到正确屏幕坐标的齐次坐标。// 在顶点着色器中 o.screenPos ComputeScreenPos(o.clipPos); // 在片段着色器中 float2 screenUV i.screenPos.xy / i.screenPos.w;这里得到的screenUV就是范围在[0, 1]之间的屏幕空间纹理坐标左下角为(0,0)右上角为(1,1)。它对于采样屏幕纹理如抓屏_GrabTexture或后处理用的_MainTex至关重要。3. 裁剪空间的实战应用与深度剖析理解了理论我们来看看在Shader里具体怎么用以及会遇到哪些坑。3.1 在Shader中获取与使用裁剪空间坐标在Unity的Surface Shader或顶点/片段着色器中获取裁剪空间坐标是常规操作。在顶点着色器中struct appdata { float4 vertex : POSITION; // ... 其他属性 }; struct v2f { float4 pos : SV_POSITION; // 这就是经过变换后的裁剪空间坐标 float4 clipPos : TEXCOORD0; // 有时我们也会显式存储一份 // ... 其他插值数据 }; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); // 标准写法 o.clipPos o.pos; // 如果需要传递给片元着色器 // 另一种等价写法了解即可不推荐 // o.pos mul(UNITY_MATRIX_MVP, v.vertex); // 旧版写法 // o.pos mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0))); return o; }这里o.pos就是SV_POSITION语义的裁剪空间坐标。它是顶点着色器必须输出的。在片段着色器中片段着色器接收的是经过光栅化插值后的裁剪空间坐标。但注意这个插值是在屏幕空间进行的线性插值而裁剪空间坐标是齐次坐标包含w。直接使用插值后的i.pos的xyzw分量是不正确的因为w分量没有被透视校正插值。这就是为什么我们需要ComputeScreenPos或手动进行透视除法的原因。3.2 深度值与裁剪空间Z/W裁剪空间的z分量和w分量携带了深度信息这对于深度相关的效果如雾、软粒子、遮挡溶解极其重要。在片段着色器中要获取当前片元的线性深度即离摄像机的实际距离通常有两种方法通过裁剪空间坐标计算float linearDepth LinearEyeDepth(i.clipPos.z, i.clipPos.w); // 或者更手动一点 // float sceneDepth i.clipPos.z / i.clipPos.w; // 这是NDC深度非线性的 // 需要根据投影矩阵参数反算回线性深度比较复杂不推荐自己算。LinearEyeDepth是Unity内置函数它正确地将裁剪空间的z/w经过插值后需要重新计算透视除法转换回观察空间下的线性深度。这里有个大坑你不能直接用i.clipPos.z / i.clipPos.w因为i.clipPos是经过顶点插值而来的它的z和w没有保持原始的齐次关系直接除法的结果在透视投影下是错误的。正确做法是传递clipPos.w即观察空间的-Z值并在片元中重新计算i.clipPos.z / i.clipPos.w或者使用LinearEyeDepth函数。通过采样深度纹理 这是更常用、更准确的方法尤其是在需要与其他物体深度比较时。// 需要摄像机开启深度纹理模式 float sceneDepth LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, screenUV)); float surfaceDepth LinearEyeDepth(i.clipPos.z / i.clipPos.w); // 当前片元的深度 float depthDifference sceneDepth - surfaceDepth; // 常用于边缘检测或相交效果3.3 应用案例裁剪空间下的顶点动画与几何剔除裁剪空间坐标可以直接用于一些特殊的屏幕空间效果。例如实现一个“屏幕边缘波动”的顶点动画v2f vert (appdata v) { v2f o; float4 worldPos mul(unity_ObjectToWorld, v.vertex); o.pos UnityObjectToClipPos(v.vertex); // 基础裁剪空间坐标 // 在裁剪空间进行偏移 float2 clipOffset float2(0, 0); // 获取NDC坐标在顶点着色器中w是准确的 float2 ndc o.pos.xy / o.pos.w; // 判断是否在屏幕边缘例如NDC的x接近1或-1 float edgeFactor saturate(abs(ndc.x) * 10 - 9); // 当|x|0.9时因子大于0 // 根据边缘因子和时间在裁剪空间添加y方向偏移 clipOffset.y sin(_Time.y * 5 worldPos.x) * edgeFactor * 0.05; // 将偏移量加到裁剪空间坐标上注意偏移量是除以w之前的 o.pos.xy clipOffset * o.pos.w; return o; }这个例子中我们在顶点着色器末端直接修改了裁剪空间坐标o.pos使得靠近屏幕左右边缘的顶点产生上下波动。这种操作非常高效因为是在顶点阶段完成的。实操心得在裁剪空间做偏移时偏移量必须乘以o.pos.w。因为裁剪空间的xy范围是[-w, w]你的偏移量比如0.05是相对于NDC空间的。如果不乘以w对于远处的物体w很大同样的偏移量在裁剪空间里会显得微乎其微导致效果不一致。这是新手常犯的错误。4. 屏幕空间的精准计算与纹理采样屏幕空间是片段着色器的主场我们在这里决定每个像素最终的颜色。4.1 正确计算屏幕UV如前所述最安全、最通用的获取屏幕UV的方法是使用ComputeScreenPos。v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.screenPos ComputeScreenPos(o.pos); return o; } fixed4 frag (v2f i) : SV_Target { float2 screenUV i.screenPos.xy / i.screenPos.w; // 现在 screenUV 就是标准的 [0,1] 纹理坐标 // 可以用来采样_GrabTexture, _CameraOpaqueTexture等 fixed4 col tex2D(_GrabTexture, screenUV); return col; }ComputeScreenPos的内部实现本质上就是做了(clipPos.xy * 0.5 clipPos.w * 0.5)的缩放平移并输出一个齐次坐标确保在片段着色器中进行透视校正插值后再除以w能得到正确的屏幕UV。一个重要的细节屏幕UV的原点。在Unity中屏幕纹理如_GrabTexture的采样原点通常是左下角(0,0)。这与DirectX的纹理坐标系一致。而有些后处理源_MainTex在OnRenderImage中的原点也可能是左下角。但请注意在片段着色器中SV_Position语义提供的像素坐标其原点在左上角。这是一个常见的混淆点。如果你用i.pos.xy / _ScreenParams.xy来计算UV得到的y坐标是反的。通常我们使用ComputeScreenPos或手动计算(i.pos.xy / _ScreenParams.xy)后再对y进行y 1 - y翻转。4.2 屏幕空间下的像素级操作有了屏幕UV我们就可以进行各种全屏或基于屏幕位置的效果。案例1简单的屏幕扭曲折射效果fixed4 frag (v2f i) : SV_Target { float2 screenUV i.screenPos.xy / i.screenPos.w; // 基于一个噪声图或法线图产生偏移 float2 distortion tex2D(_DistortionTex, screenUV * _Tiling).rg * 2 - 1; screenUV distortion * _Strength; // 采样抓屏纹理 fixed4 col tex2D(_GrabTexture, screenUV); return col; }案例2屏幕空间距离场用于UI高亮或轮廓有时我们需要知道当前像素在屏幕空间中离某个点的距离。float2 GetPixelSize() { return float2(1.0 / _ScreenParams.x, 1.0 / _ScreenParams.y); } fixed4 frag (v2f i) : SV_Target { float2 screenUV i.screenPos.xy / i.screenPos.w; float2 pixelSize GetPixelSize(); // 假设我们有一个屏幕空间的目标点坐标 targetScreenUV float2 targetScreenUV float2(0.5, 0.5); // 屏幕中心 float dist length((screenUV - targetScreenUV) / pixelSize); // 距离单位像素 float glow saturate(1.0 - dist / _Radius); // 基于像素距离的衰减 // ... 应用glow到颜色上 }这里GetPixelSize返回单个像素在UV空间中的大小。计算距离时除以pixelSize就将UV距离转换成了像素距离这样效果就不会随着屏幕分辨率变化而改变粗细。4.3 与深度、法线纹理协同工作现代渲染中屏幕空间反射SSR、环境光遮蔽SSAO等高级效果严重依赖屏幕空间坐标、深度纹理和法线纹理的配合。一个简化的SSR核心思路伪代码float3 RayMarchSSR(float3 rayOriginVS, float3 rayDirVS, float2 startScreenUV) { float stepSize 0.1; float3 currentPosVS rayOriginVS; for (int i 0; i MAX_STEPS; i) { currentPosVS rayDirVS * stepSize; // 将当前观察空间位置转换到裁剪空间再转换到屏幕UV float4 clipPos mul(UNITY_MATRIX_P, float4(currentPosVS, 1.0)); float2 uv (clipPos.xy / clipPos.w) * 0.5 0.5; // 采样该UV点的深度纹理得到场景在该点的观察空间深度 float sceneDepthVS LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv)); // 比较当前射线点的深度和场景深度 if (currentPosVS.z sceneDepthVS) { // 假设观察空间-Z为深度 // 射线击中了场景返回击中点的颜色采样屏幕颜色纹理 return tex2Dlod(_CameraOpaqueTexture, float4(uv, 0, 0)).rgb; } } return float3(0,0,0); // 未击中返回黑色或环境色 }这个例子清晰地展示了如何将观察空间的射线步进点通过投影矩阵UNITY_MATRIX_P转换回裁剪空间再计算屏幕UV从而与屏幕空间的深度纹理进行深度比较。这里的关键是理解UNITY_MATRIX_P就是投影矩阵用于将观察空间坐标转换到裁剪空间。5. 常见疑难杂症与深度避坑指南在实际项目中处理裁剪空间和屏幕空间时你会遇到各种诡异的问题。下面是我踩过的一些坑和解决方案。5.1 深度计算错误与Z-Fighting问题描述自己计算的深度与深度纹理采样结果对不上或者物体表面出现闪烁的Z-Fighting。原因与排查透视除法错误在片段着色器中直接使用了插值后的i.clipPos.z作为深度。这是绝对错误的。必须使用i.clipPos.z / i.clipPos.w或者LinearEyeDepth函数。精度问题在深度值非常接近时例如大平面地面与贴在上面的草浮点数精度可能导致谁在前谁在后不确定。这通常需要调整渲染队列、使用Offset着色器指令或者轻微地偏移模型顶点在着色器中clipPos.z 0.0001 * clipPos.w来解决。注意在裁剪空间偏移z时偏移量必须乘以w理由同之前的xy偏移。深度纹理格式确保摄像机正确开启了深度纹理camera.depthTextureMode DepthTextureMode.Depth;。在URP/HDRP中需要在Renderer Asset中配置。避坑技巧当你需要比较当前像素深度和场景深度时统一使用观察空间下的线性深度。使用LinearEyeDepth函数将两者都转换过来再比较。避免混合使用非线性深度来自深度缓冲的原始值和线性深度。5.2 屏幕坐标偏移或拉伸问题描述全屏效果在屏幕边缘不对或者在不同分辨率下效果不一致例如一个屏幕中心的圆形扭曲变成了椭圆形。原因与排查UV原点搞错确认你使用的屏幕纹理的原点。对于_GrabTexture和大多数后处理源是左下角。如果用了SV_Position计算的UV且没翻转Y就会上下颠倒。未考虑屏幕宽高比在屏幕空间做圆形、径向模糊等效果时必须考虑屏幕宽高比Aspect Ratio。_ScreenParams.x / _ScreenParams.y就是宽高比。在计算距离时应该对x或y分量进行补偿float2 uv screenUV * 2.0 - 1.0; // 转换到[-1,1]范围 uv.x * _ScreenParams.x / _ScreenParams.y; // 补偿宽高比 float dist length(uv); // 现在dist才是正圆形的距离场ComputeScreenPos 在VR/多目渲染下的问题在单通道立体渲染如VR或多摄像机渲染时ComputeScreenPos可能需要特殊处理。Unity有一个ComputeGrabScreenPos函数专门用于抓屏纹理它在这些情况下更可靠。对于常规后处理使用UnityStereoTransformScreenSpaceTex函数来处理UV。5.3 不同渲染路径与API的差异问题描述Shader在PC上正常在手机上花屏或者在Forward渲染下正常在Deferred下出错。原因与排查深度值范围差异这是最大的坑。在DirectXWindows, Xbox和MetaliOS, macOS上裁剪空间和NDC的z范围是[0, 1]近裁剪面为0远裁剪面为1。而在OpenGL包括OpenGL ES即大部分安卓机上z范围是[-1, 1]。Unity使用UNITY_NEAR_CLIP_VALUE和COMPUTE_EYEDEPTH等宏来屏蔽这个差异。永远不要硬编码深度比较值要使用Linear01Depth或LinearEyeDepth这类内置函数。投影矩阵差异不同API的投影矩阵可能略有不同例如是否反转了Y轴。UnityObjectToClipPos和ComputeScreenPos已经处理了这些。如果你自己拼装矩阵比如用于自定义投影务必使用UNITY_MATRIX_P而不是硬编码的投影矩阵并且注意UNITY_MATRIX_P在OpenGL下是包含Y反转的。渲染纹理坐标差异DirectX和OpenGL的纹理V坐标方向相反。Unity的宏UNITY_UV_STARTS_AT_TOP用于判断。在采样某些内置纹理如_CameraDepthTexture时Unity已经帮我们处理好了。但如果你自己渲染到纹理然后再采样需要注意这个差异可以使用tex2D函数Unity的封装会处理。5.4 性能优化注意事项避免在片段着色器中进行复杂的空间转换特别是矩阵乘法。尽量在顶点着色器中计算好然后插值到片段着色器。例如屏幕UV的计算虽然ComputeScreenPos在顶点着色器里做了一次乘法但比在片段着色器里为每个像素做一次矩阵乘法要高效得多。谨慎使用全屏屏幕空间操作屏幕空间反射、SSAO等效果虽然强大但需要采样深度/法线纹理并进行多次步进或采样开销巨大。务必设置合理的步进次数、采样距离和降采样。利用插值器v2f结构体中的TEXCOORD0等插值器是宝贵的资源。如果你需要传递裁剪空间坐标、屏幕UV、世界空间位置、观察空间位置等多个向量考虑将它们打包。例如屏幕UV的z分量可以存储裁剪空间的z/ww分量可以存储自定义数据。6. 进阶自定义投影与非线性空间的应用对于某些特殊效果如鱼眼镜头、小孔成像或者制作门户/镜子我们需要跳出标准的透视投影这就涉及到对裁剪空间和屏幕空间的更底层操控。6.1 实现一个简单的鱼眼扭曲效果我们可以通过在顶点着色器末端修改裁剪空间坐标来模拟非标准的投影。v2f vert (appdata v) { v2f o; // 先正常变换到裁剪空间 float4 clipPos UnityObjectToClipPos(v.vertex); // 将裁剪空间坐标视为方向向量忽略w进行鱼眼扭曲 float3 dir normalize(clipPos.xyz); // 这是一个近似严格来说需要先回到观察空间 // 更好的做法在观察空间进行扭曲 float4 viewPos mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, v.vertex)); float3 viewDir normalize(viewPos.xyz); // 假设扭曲是径向的基于viewDir的xz平面角度和y角度 float dist length(viewPos.xyz); float newDist dist * (1.0 _FishEyeStrength * dot(viewDir.xz, viewDir.xz)); // 简单的二次扭曲 viewPos.xyz viewDir * newDist; // 重新投影到裁剪空间 o.pos mul(UNITY_MATRIX_P, viewPos); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; }这个例子展示了思路在观察空间对顶点位置进行非线性变换然后再用标准投影矩阵UNITY_MATRIX_P变换到裁剪空间。这比直接在裁剪空间扭曲更符合物理。6.2 屏幕空间与后处理栈的集成在现代管线如URP中屏幕空间坐标的计算被封装得更好。例如在URP的全屏后处理Shader中你通常会看到struct Attributes { uint vertexID : SV_VertexID; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings Vert(Attributes input) { Varyings output; // 使用顶点ID生成一个覆盖整个屏幕的三角形 output.positionCS GetFullScreenTriangleVertexPosition(input.vertexID); output.uv GetFullScreenTriangleTexCoord(input.vertexID); return output; }这里GetFullScreenTriangleVertexPosition直接输出了裁剪空间坐标一个覆盖NDC空间[-1,1]的大三角形GetFullScreenTriangleTexCoord则直接给出了正确的屏幕UV。URP帮我们处理了所有底层空间变换让我们可以专注于效果本身。6.3 调试工具可视化裁剪空间与屏幕空间在开发复杂Shader时可视化中间数据是调试的利器。你可以写一个简单的调试Shader将不同空间的值映射到颜色上。fixed4 frag (v2f i) : SV_Target { // 可视化裁剪空间xy映射到[-1,1] - [0,1] // float2 clipNDC i.clipPos.xy / i.clipPos.w; // fixed3 color fixed3(clipNDC * 0.5 0.5, 0); // 可视化屏幕UV // float2 screenUV i.screenPos.xy / i.screenPos.w; // fixed3 color fixed3(screenUV, 0); // 可视化深度非线性 // float depth i.clipPos.z / i.clipPos.w; // fixed3 color fixed3(depth, depth, depth); // 如果是DX深度在[0,1] // 可视化线性深度观察空间距离 float linearDepth LinearEyeDepth(i.clipPos.z / i.clipPos.w, _ZBufferParams); // 将深度映射到一个可视范围例如0到100单位 linearDepth saturate(linearDepth / 100.0); fixed3 color fixed3(linearDepth, linearDepth, linearDepth); return fixed4(color, 1.0); }通过切换注释你可以直观地看到裁剪空间坐标是否对称、屏幕UV是否覆盖整个屏幕、深度值是否平滑变化从而快速定位空间计算错误。理解裁剪空间和屏幕空间是解锁Unity Shader高级技能的钥匙。它不仅仅是记住几个函数更是要建立起从3D模型到2D像素的完整空间变换心智模型。当你再看到屏幕特效闪烁、深度测试异常或者坐标对不齐时希望你能冷静地沿着这条变换链路模型-世界-观察-裁剪-NDC-屏幕一步步推导和排查。这个过程一开始可能有些烧脑但一旦掌握你对整个实时渲染管线的理解会上一个大台阶写出的Shader也会更加稳健和高效。