本篇开始将进行《入门精要》初级篇最后一部分的学习,完成透明效果的学习。
一个像素的RGBA参数中的最后一项A指的就是透明度Alpha。在之前的实践中,一般值都默认是1,我们认为完全不透明,就像一个片元着色器输出的fixed4颜色值的第四个数值总会是1;当像素透明度为0时则表示像素完全不显示(完全透明)。Unity中实现透明效果通常有两种方法,透明度测试(Alpha Test)和透明度混合(Alpha Blending)。
透明度测试原理很简单,就是在正常光照的基础上,加上一个float类型的_Cutoff透明度参考数值,定义一个完全透明/不透明的界限,一般可以定义“大于它的不透明,小于它的完全透明”。除此之外跟之前的光照步骤再没有任何的区别了,后续会给在Unity中给它实现出来。
无需关闭深度写入——由于是直接根据每个片元RGBA的.a值判断是否留下(不透明)/裁剪掉(完全透明),相当于只是在片元着色器的步骤做改变,因此它无需关闭深度写入(ZWrite),关于什么是深度写入以及它与渲染顺序的关系,将在后面章节详细讲述。
透明效果挺“极端”——正因上述实现的方法,透明测试出的片元要么不透明,要么完全透明,得到的效果就像在物体上挖了一个洞!
透明效果存在锯齿——边界处往往会因为判断的精度问题存在锯齿,这在后面的实践过程中会有所体现。
看到“混合”是不是很熟悉?没错!就在图形渲染管线3.0-光栅化和像素处理阶段中像素处理阶段的最后一个环节——混合(Blend)。
只有透明或者半透明的物体,才会需要混合操作。混合是混合源颜色和目标颜色,而透明度混合就是拿当前片元的透明度作为混合因子,与颜色缓冲区中的片元颜色值混合,得到新的颜色值。
需要关闭深度写入,但不关闭深度测试——与透明度测试不同,透明度混合需要关闭深度写入,但深度测试继续进行。这意味着还会进行根据深度值判断“谁前谁后”的操作,
这个逻辑要搞明白!!至于为什么要这样做?在后面的渲染顺序中会进行详细叙述。
效果更加柔滑——透明度混合的边界效果将比透明度测试更加柔滑,我认为这是因为透明度混合执行的操作是颜色的混合而非直接“一刀切”的缘故,这样就不会涉及到判断的精度问题了。
无法对模型进行像素级别的排序——这种情况尤其是在面对模型网格之间有相互交叉的结构,会出现遮挡错误的半透明效果。
对于关闭深度写入的透明度混合,无法对模型进行像素级别的深度排序,当模型网格之间有相互重叠的情况,往往会出现错误的效果,这一点将在第5节进行讨论,以及实现开启深度写入的透明度混合的方法。
一般地,引擎在进行渲染时,都默认剔除了物体背面的渲染图元,这个背面是相对于摄像机方向而言的,例如上述提到的透明度测试和透明度混合,都是仅实现了正面渲染。
为了实现双面,可以使用Unity中的Cull指令来控制需要剔除哪个面的渲染图元,这个将在后面的第6节展示。
深度写入跟渲染管线3.0中提到的深度测试、模板测试一样,我们都可以自行开启/关闭,开启深度写入就代表着——默许用当前的片元深度值覆盖掉原有z-buffer的深度值。
那么上述的透明度混合操作,为什么需要关闭深度写入呢?——试想一下,一个正方体的前、后面,前面更靠近照相机,如果我们开启了深度写入,意味着正方体前面的片元深度深度总是小于背面的深度值,渲染过程中后面的面将永远处于被剔除状态,永远都看不到。但这违背了“半透明物体”的视觉效果了!
场景中物体的渲染顺序十分重要,因为将影响着最终画面上物体之间的遮挡关系是否正确。
对于不透明物体来说,深度缓冲已经为我们解决了渲染顺序的问题。而对于关闭了深度写入的透明度混合操作,渲染顺序就变得不可控了。无论是对于同时渲染两个半透明物体,还是半透明和不透明物体一起渲染,不正确的渲染顺序得到的画面将会非常奇怪。可见,关闭深度写入造成了多么大的混乱!

可以发现,效果是有锯齿的,不是特别圆滑。
Shader "Unity Shaders Book/Chapter 8/AlphaTest"
{
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5 //这是clip判断条件
}
SubShader {
//透明度测试都应该包含的三个标签
Tags { "Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout" }
Pass {
Tags { "LightMode"="ForwardBase" }
// 是否开启双面渲染的效果
// Cull off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//Properties
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _Cutoff;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
//alpha test
clip(texColor.a - _Cutoff);
//equal to:
//if((texColor.a - _Cutoff) < 0.0){
// discard;
// }
//half lambert:
float halfLambert = saturate(dot(worldNormal, lightDir)) * 0.5 + 0.5;
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * halfLambert;
return fixed4(ambient + diffuse, 1.0);
}
ENDCG
}
}
FallBack "Transparent/Cutout/VertexLit"
}
跟之前进行的着色相比,除了给Pass设置标签,透明度测试会额外给SubShader也设置标签,Queue就是其中之一。为了严格要求上述提到过的渲染顺序问题,Unity提供了渲染队列(render queue),也就是给每个SubShader提供一个可以提前定义渲染队列的机会,这个机会就是Queue这个Tag了,每个渲染队列还有对应的队列索引号,索引号越小越早被渲染。
ShaderLab的Queue标签提供的5个渲染队列
| 队列名称 | 队列索引号 | 解释 |
| Background | 1000 | 最先渲染,通常使用这个队列来渲染那些需要绘制在背景上的物体 |
| Geometry | 2000 | 默认的渲染队列,大多数物体(不透明物体)使用这个队列 |
| AlphaTest | 2450 | 需要透明度测试的物体使用这个队列,在所有不透明物体后再渲染 |
| Transparent | 3000 | 该队列中的物体会按从后往前的顺序进行渲染,使用了透明度混合的物体都应使用这个队列 |
| Overlay | 4000 | 使用该队列实现一些叠加效果,最后渲染的物体都应使用这个队列 |
标签设置为“True”,意味着使用当前SubShader的物体不会受Projector的影响,这样的设置通常用于半透明物体:
Tags {"IgnoreProjector"="True"}
该标签可以让Unity把当前Shader归入到提前定义的组,其实就是对着色器进行一个分类,例如当前Shader是个不透明着色器、当前Shader是个透明的着色器等等:
//不透明
Tags {"RenderType"="Opaque"}
//透明度测试
Tags {"RenderType"="TransparentCutout"}

Shader "Unity Shaders Book/Chapter 8/Alpha Blend"
{
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
_MainTex ("Texture", 2D) = "white" {}
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1 //控制整体的透明度
}
SubShader
{
//标签需要设置:Queue, RenderType, IgnoreProjector
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
Pass
{
Tags { "LightMode"="ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//properties
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v{
float4 vertex : POSITION;
float4 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
float halfLambert = saturate(dot(worldNormal, worldLightDir)) * 0.5 + 0.5;
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * halfLambert;
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
}
}
我认为,透明度混合的重点在于关闭深度写入和进行源颜色与目标颜色混合这两个步骤上,这就涉及到了之前【Unity Shader】初识Shader,基础总结!中提到的ShaderLab的ZWrite和Blend渲染状态设置命令。
与透明度测试一样,透明度混合也为SubShader设置三个标签,
ShaderLab的ZWrite设置命令
| 命令语义 | 解释 |
| ZWrite On | 打开深度写入 |
| ZWrite Off | 关闭深度写入 |
代码中就体现在了Pass中进行的混合状态设置:
Pass {
...
ZWrite Off
...
}
对于Blend命令,须知:
ShaderLab的Blend设置命令
| 命令语义 | 解释 |
| Blend Off | 关闭混合 |
| Blend SrcFactor DstFactor | 开启混合,并设置混合因子。源颜色*SrcFactor,目标颜色*DstFactor,二者相加后再存入颜色缓冲器中 |
| Blend SrcFactor DstFactor, SrcFactorA, DstFactorA | 同上,但是使用不同的因子来混合颜色(RGB)和透明度(A) |
| BlendOp Op | 不将混合颜色相加,而是执行不同的操作(Op) |
| BlendOp OpColor, OpAlpha | 同上,但是对颜色 (RGB) 通道和 Alpha (A) 通道使用不同的混合操作 |
代码中我们使用了
Blend SrcAlpha OneMinusSrcAlpha
其中这个混合命令,意味着混合后储存到颜色缓冲器中的新目标颜色值将会是:
上面用到了一个OneMinusSrcAlpha,也就是“1-源颜色”,这里就涉及到了Unity中混合的运算系数。ShaderLab:混合 - Unity 手册中有为我们列举一系列的混合系数:

同时还列举出了常见的混合类型,供我们参考并直接使用:

由于关闭了深度写入,我们就只能纯粹用Unity提供的渲染队列来解决渲染顺序这个问题。但是当遇到渲染模型本身遮挡效果复杂的情况下,效果往往会出错,比如《入门精要》中演示的Knot模型的例子,我还是用了之前透明度混合的Shader,得到的效果肯定是错误的:

这种情况下,就要重新启用深度写入才能实现正确的遮挡效果。

Shader "Unity Shaders Book/Chapter 8/AlphaBlendZWrite"
{
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
_MainTex ("Texture", 2D) = "white" {}
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1 //控制整体的透明度
}
SubShader
{
//标签需要设置:Queue, RenderType, IgnoreProjector
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
//extra pass that renders to depth buffer only
Pass
{
ZWrite On
ColorMask 0
}
Pass
{
Tags { "LightMode"="ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//properties
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v{
float4 vertex : POSITION;
float4 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
float halfLambert = saturate(dot(worldNormal, worldLightDir)) * 0.5 + 0.5;
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * halfLambert;
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
}
}
又想达到正确的半透明效果,又想实现正确的遮挡关系,那当然二者要分开进行了!于是第一次接触到了两个Pass!但第一个用于实现正确遮挡关系的Pass很简单:
首先,打开了深度写入,
ZWrite On
接着,ShaderLab 命令:ColorMask用于设置颜色通道的写入遮罩,防止GPU写入渲染目标中的通道,把ColorMask的值设为0,意味着不会写入任何颜色。
ColorMask 0
之前实现的无论是透明度测试还是透明度混合,都是单面渲染,即只渲染了物体正面的效果(剔除了相对于摄像机是背面的方向),可是现实中的透明物体我们都是能看到正反面的!
是否剔除背对摄像机的渲染图元,Unity中是由Cull指令决定的,Cull指令有三个选项设置:
引擎为了节省性能,一般情况下会一直开启剔除功能,如果设置为Off的话,渲染图元数量会成倍的增加!因此除非是想达到双面渲染的效果,一般是不会关闭剔除功能的。
透明度测试实现双面渲染很简单,只需要关闭剔除即可!
//Turn off culling
Cull Off
在Alpha Scale相同的情况下,双面(上)和单面(下)效果对比看看:


双面效果就相对复杂一些,需要写满两个Pass,一个用于前面,一个用于后面,Shader代码如下:
Shader "Unity Shaders Book/Chapter 8/AlphaBlendBothSide"
{
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
_MainTex ("Texture", 2D) = "white" {}
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1 //控制整体的透明度
}
SubShader
{
//标签需要设置:Queue, RenderType, IgnoreProjector
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
//extra pass that renders to depth buffer only
Pass
{
Tags { "LightMode"="ForwardBase"}
//front
Cull Front
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//properties
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v{
float4 vertex : POSITION;
float4 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
float halfLambert = saturate(dot(worldNormal, worldLightDir)) * 0.5 + 0.5;
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * halfLambert;
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
Pass
{
Tags { "LightMode"="ForwardBase" }
//back
Cull Back
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//properties
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v{
float4 vertex : POSITION;
float4 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
float halfLambert = saturate(dot(worldNormal, worldLightDir)) * 0.5 + 0.5;
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * halfLambert;
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}
同样看看对比:在Alpha Scale相同的情况下,双面(上)和单面(下)效果:


很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
我有一个包含模块的模型。我想在模块中覆盖模型的访问器方法。例如:classBlah这显然行不通。有什么想法可以实现吗? 最佳答案 您的代码看起来是正确的。我们正在毫无困难地使用这个确切的模式。如果我没记错的话,Rails使用#method_missing作为属性setter,因此您的模块将优先,阻止ActiveRecord的setter。如果您正在使用ActiveSupport::Concern(参见thisblogpost),那么您的实例方法需要进入一个特殊的模块:classBlah
我有一个围绕一些对象的包装类,我想将这些对象用作散列中的键。包装对象和解包装对象应映射到相同的键。一个简单的例子是这样的:classAattr_reader:xdefinitialize(inner)@inner=innerenddefx;@inner.x;enddef==(other)@inner.x==other.xendenda=A.new(o)#oisjustanyobjectthatallowso.xb=A.new(o)h={a=>5}ph[a]#5ph[b]#nil,shouldbe5ph[o]#nil,shouldbe5我试过==、===、eq?并散列所有无济于事。
我有一些Ruby代码,如下所示:Something.createdo|x|x.foo=barend我想编写一个测试,它使用double代替block参数x,这样我就可以调用:x_double.should_receive(:foo).with("whatever").这可能吗? 最佳答案 specify'something'dox=doublex.should_receive(:foo=).with("whatever")Something.should_receive(:create).and_yield(x)#callthere
Sinatra新手;我正在运行一些rspec测试,但在日志中收到了一堆不需要的噪音。如何消除日志中过多的噪音?我仔细检查了环境是否设置为:test,这意味着记录器级别应设置为WARN而不是DEBUG。spec_helper:require"./app"require"sinatra"require"rspec"require"rack/test"require"database_cleaner"require"factory_girl"set:environment,:testFactoryGirl.definition_file_paths=%w{./factories./test/
我遵循MichaelHartl的“RubyonRails教程:学习Web开发”,并创建了检查用户名和电子邮件长度有效性的测试(名称最多50个字符,电子邮件最多255个字符)。test/helpers/application_helper_test.rb的内容是:require'test_helper'classApplicationHelperTest在运行bundleexecraketest时,所有测试都通过了,但我看到以下消息在最后被标记为错误:ERROR["test_full_title_helper",ApplicationHelperTest,1.820016791]test
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
我已经构建了一些serverspec代码来在多个主机上运行一组测试。问题是当任何测试失败时,测试会在当前主机停止。即使测试失败,我也希望它继续在所有主机上运行。Rakefile:namespace:specdotask:all=>hosts.map{|h|'spec:'+h.split('.')[0]}hosts.eachdo|host|begindesc"Runserverspecto#{host}"RSpec::Core::RakeTask.new(host)do|t|ENV['TARGET_HOST']=hostt.pattern="spec/cfengine3/*_spec.r
我在app/helpers/sessions_helper.rb中有一个帮助程序文件,其中包含一个方法my_preference,它返回当前登录用户的首选项。我想在集成测试中访问该方法。例如,这样我就可以在测试中使用getuser_path(my_preference)。在其他帖子中,我读到这可以通过在测试文件中包含requiresessions_helper来实现,但我仍然收到错误NameError:undefinedlocalvariableormethod'my_preference'.我做错了什么?require'test_helper'require'sessions_hel