【Win 10 应用开发】UI Composition 札记(五):灯光
UI Composition 除了能够为 UI 元素建立三维空间外,还有相当重要的一个部件——灯光。宇宙万物的精彩缤纷,皆源于光明,光,使我们看到各种东西,除了黑洞之外的世界都是五彩斑谰的。故而,真要模拟现实物体,合理的灯光照射是很关键,不然就“不像”了。
Composition API 为各种灯光效果提炼了一个公共基类——CompositionLight,它带有两个规范性的属性:
Targets:可视化元素的集合。用来确定场景中哪些东西应该被照亮。比如,你模拟了一面墙,墙壁上挂着各种画,有山水,有鸟兽,有美女,有蝙蝠,如果你要看画,黑乎乎的你连根狗毛也看不见的,所以你看到很多美术馆或博物馆都会安装各种灯源,只有打灯你才能看到这些画的。如果你希望看美女,那么就把美女加入 Targets 集合,这样美女就会被灯光照亮。
ExclusionsFromTargets:这是一个排除项列表。与上面的刚好反过来,就是指定你不希望被照亮的物体。如果你觉得蝙蝠太狰狞太恐怖,不想看,你可以把它排除掉,就不会被灯光照亮了。
环境光
环境光类似于咱们家里的白炽光、节能灯等,这种光源比较均匀,基本可以把整个房间照亮。
我们看一个环境光的例子。下面示例,在界面上加载一张图片,然后我们用环境光去照亮它。顺便放一个 Slider 控件,目的是可以调节光照的强度。
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <Image Source="Assets/5.jpg" Stretch="Uniform" Name="img"/> <Slider Grid.Row="1" Margin="2,9" StepFrequency="0.1" Value="1" Minimum="0" Maximum="10" ValueChanged="OnSliderValChanged"/> </Grid>
切换到代码文件,在页面类的构造函数中,咱们添加一下灯光效果。
AmbientLight light = null; public MainPage() { this.InitializeComponent(); Visual v = ElementCompositionPreview.GetElementVisual(img); Compositor compos = v.Compositor; light = compos.CreateAmbientLight(); light.Targets.Add(v); }
注意,我为什么要把 AmbientLight 的变量声明到类级别呢,因为可以在后面调整它的强度。下面是 Slider 控件的 ValueChanged 事件的处理代码。
private void OnSliderValChanged(object sender, RangeBaseValueChangedEventArgs e) { if(light != null) { light.Intensity = (float)e.NewValue; } }
这里要先判断一下 light 变量是否为 null,因为这个事件处理是在 XAML 代码中关联的,即在页面类实例构造过程中会调用这个方法(主要是设置 Value 属性的值时发生),那个时候,环境光对象还没有创建,如果不判断,就会出现 null 引用异常。
AmbientLight 类表示环境光,它有一个 Color 属性,用以指定光的颜色,默认是白光。当物体被白光照亮时,它呈现的是本色(本来面目)。所以,上面代码的执行效果如下图。
Intensity 表示光照强度,从上面的例子咱们看到,这个值应该大于 0,小于等于 0 就全黑了,什么都看不见,那就没有意义了,值也不要太大,所以我这个例子最大就到 10 ,当然你可以设置 100、1000,可是强度太大了,会亮瞎眼的,什么也看不见,也是没有意义的。光照强度默认是 1 ,我们可以根据需要设置合适的值。
我们还可以换一下其他颜色的光,比如,我们改一下代码,用充满幽灵意味的绿光去照射一下。
light.Color = Colors.Green;
然后,效果很惊人。
定点光
点光,即 PointLight,它就像一盏小灯泡,发出的光并不能像环境光那样覆盖全面,而是点状的,但它可以照亮四周的物体,而且距离物体近的话,照得更亮,这就很像火把、蜡烛。所以,PointLight 类的属性会比环境光多一些,也复杂一些。
Color 和 Intensity 属性是一样的,前者表示灯光的颜色,后者表示强度。除此之外,还有以下这几个:ConstantAttenuation、QuadraticAttenuation、LinearAttenuation,这几个属性的性质是一样的,只是算法不同,有的是平方值的,有的是线性的。这些值是用来设置光的衰减速度,啥意思呢,我们刚刚不是说过吗,点状光的照亮程度是跟距离有关,随着灯光与物体的距离增大,亮度会衰减。当然,如果光线很强的情况下,距离远可能照亮的范围更大,近距离情况下,会把局部照得更亮。这几个值就是用来描述光线衰减的速度。在现实世界中,这可能与空气能见度或空气密度有关,因为这些要素会影响光的传播。但在虚拟图形中不存在真实的大气,所以需要通过算法来模拟。
由于点状光是一个发光点,所以它肯定会有位置的,即坐标,下面两个属性用来确定点状光的坐标:Offset 属性确定位置,它是一个三维坐标;CoordinateSpace 又是啥呢,它要求指定一个可视化对象,用来计算光照的强度的。你想啊,大晚上,你在一片荒野上点根火把,你会觉得这火把好像不怎么亮,但是,如果你在一个狭窄的山洞里面点一根火把,你就会觉得它特别亮。所以,这个属性就是设置一个容器,好确定这点光到底能照多亮。
下面我们看看定点光的例子。
在界面上我们放置一个文本,然后,下面的 Slider 控件用来调整点光的衰减速度,即 ConstantAttenuation 属性,这个值越大,表明同样距离下灯光会更弱,因为它衰减得更快更明显,这个值是大于0的任意值。
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <Border Background="Black" Grid.Row="0" Name="bd"> <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="欢迎观临" FontSize="150" FontFamily="华文行楷" Foreground="Gold" Name="text"/> </Border> <Slider Grid.Row="1" Margin="2,7" Maximum="5" Minimum="1" Value="1" StepFrequency="0.1" Name="sld"/> </Grid>
TextBlock 为什么要放到一个 Border 中呢,前面说了,定点光需要一个容器来计算照亮程度,所以,Border 是用来作为参考容器的。
切换到代码视图,在页面类的构造函数中,我们来加一下定点光。
public MainPage() { this.InitializeComponent(); // 获取容器 Visual vsContainer = ElementCompositionPreview.GetElementVisual(bd); // 获取 TextBlock 的可视化对象 Visual txtVisual = ElementCompositionPreview.GetElementVisual(text); Compositor compos = vsContainer.Compositor; // 创建光源 PointLight light = compos.CreatePointLight(); // 灯光颜色 light.Color = Colors.Silver; // 强度 light.Intensity = 3.6f; // 位置 light.Offset = new Vector3(500f, 280f, 45f); // 照射目标 light.Targets.Add(txtVisual); // 相对容器 light.CoordinateSpace = vsContainer; // 处理 ValueChanged 事件 sld.ValueChanged += (k, x) => { light.ConstantAttenuation = (float)sld.Value; }; }
这一回处理 ValueChanged 事件就不需要判断 light 是否为null了,因为附加这个事件处理时,light 对象已经初始化。
注意,这里我们不仅要获取 TextBlock 的Visual ,尽管我们的照亮目标是它,但是,因为这种光源需要容器,所以我们要同时获得 Border 的 Visual。
来,看看效果吧。
锥光
这种光源类似手电筒的光,其实与上面的 Pointlight 很像,但锥光带有内圈和外圈。所以,锥光也有颜色、强度、衰减程度等参数,当然也会有位置。
InnerConeAngle 是内圈的角度,OuterConeAngle 是外圈的角度,用弧度角表示。如果想用角度,可以用 InnerConeAngleInDegrees 和 OuterConeAngleInDegrees 属性。
InnerConeIntensity 表示内圈的光线强度,OuterConeIntensity 表示外圈的光线强度。
Offset 表示光的位置,和上面的定点光类似,但锥光多了个 Direction 属性。用过手电你都知道的,它有个照射方向。如果光源位于物体前方,要想让它照亮物体,Z轴上的方向必须是负值,只有负值才会照进屏幕里面;如果光源在物体后面,Z轴上的方向当然要正值,这样照射方向才会指向屏幕外。
我们做个例子。在界面上放一张图,先给大家看看原图。
这书房是不是很高大上呢。然后我们让它在 Image 元素上加载。
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Border Name="bd" Background="Black"> <Image Name="img" Source="Assets/2.jpg"/> </Border> </Grid>
Image 元素外面也需要一个容器,这里我还是用Border,因为锥光和定点光一样,需要一个容器来计算光照。
定位到代码文件,在页面类的构造函数中添加光源。
public MainPage() { this.InitializeComponent(); // 获取目标元素与容器元素 Visual container = ElementCompositionPreview.GetElementVisual(bd); Visual vimg = ElementCompositionPreview.GetElementVisual(img); // 创建光源 SpotLight light = vimg.Compositor.CreateSpotLight(); // 设置容器 light.CoordinateSpace = container; // 添加照亮目标 light.Targets.Add(vimg); // 外圈和内圈光线的颜色 light.OuterConeColor = Colors.Blue; light.InnerConeColor = Colors.LightYellow; // 外圈和内圈光线的强度 light.InnerConeIntensity = 3.2f; light.OuterConeIntensity = 1f; // 角度 light.InnerConeAngleInDegrees = 30f; light.OuterConeAngleInDegrees = 90f; // 位置 light.Offset = new Vector3(550f, 270f, 150f); // 方向 light.Direction = new Vector3(-1f, 1.1f, -1f); }
好了,看看效果吧。
OK,本篇就说到这里了,开饭了。