简介

学了一段时间shader,然而一直在玩后处理,现在终于下定决心钻研一下真正的带光照的shader。从Diffuse到Specular。一个游戏的画面好坏,很大程度上取决于光照和贴图。现实世界中,我们之所以能看见东西,是因为他们要么反射了光源发出的光,要么是自身能够发光。而在游戏世界中,如果没有了光,我们虽然可以直接根据贴图显示物体的材质,但是少了很多细节光影效果,游戏显得不真实。但是,真实的光照计算是一个非常复杂的过程,对于游戏这种至少30FPS的程序来说是完全不可能的,所以我们必须要使用一种近似的光照算法,来模拟光照效果。本篇文章就来学习一下基本的光照模型以及其在unity下的shader实现。

漫反射和镜面反射

我们观察世界是因为有光进入我们的眼睛,光在世界中主要有反射和折射两种属性,当光照在某种介质表面时,一部分光发生反射,另一部分光进入介质,发生折射,也有转化为其他能量的光。本篇文章只讨论反射,折射等其他现象以后再学习。光的反射分为两种,漫反射和镜面反射。
漫反射,是投射在粗糙表面上的光向各个方向反射的现象。当一束平行的入射光线射到粗糙的表面时,表面会把光线向着四面八方反射,所以入射线虽然互相平行,由于各点的法线方向不一致,造成反射光线向不同的方向无规则地反射,这种反射称之为“漫反射”或“漫射”。这种反射的光称为漫射光。很多物体,如植物、墙壁、衣服等,其表面粗看起来似乎是平滑,但用放大镜仔细观察,就会看到其表面是凹凸不平的,所以本来是平行的太阳光被这些表面反射后,弥漫地射向不同方向。

镜面反射,是指若反射面比较光滑,当平行入射的光线射到这个反射面时,仍会平行地向一个方向反射出来,这种反射就属于镜面反射,其反射波的方向与反射平面的法线夹角(反射角),与入射波方向与该反射平面法线的夹角(入射角)相等,且入射波、反射波,及平面法线同处于一个平面内。

兰伯特光照模型

先来学习一个最简单的光照模型,兰伯特光照模型。兰伯特光照模型是目前最简单通用的模拟漫反射的光照模型,定义如下:模型表面的明亮度直接取决于光线向量(light vector)和表面法线(normal)两个向量将夹角的余弦值。光线向量是指这个点到光从哪个方向射入,表面法线则定义了这个表面的朝向。
如果漫反射光强设置为Diffuse,入射光光强为I,光方向和法线夹角为θ,那么兰伯特光照模型可以用下面的公式表示:Diffuse = I * cosθ
进一步地,我们可以通过点乘来求得两个方向向量之间的夹角,入射光方向设置为L,法线方向设置为N,如果光方向向量和法线方向向量都为单位向量(这就是为什么我们在写shader的时候需要normalize操作的原因),那么它们之间的夹角余弦值就可以表示为:cosθ = dot(L,N),最终漫反射光强公式,也就是兰伯特光照模型可以表示为:Diffuse = I  * dot(L,N)

逐顶点计算着色shader

我们在shader中需要计算输出的颜色,逐顶点着色也就是说我们的计算主要放在了vertex shader中,根据顶点来计算,每个顶点中计算出了该点的颜色,直接作为vertex shader的输出,pixel(fragment) shader的输入,当到达pixel阶段时,直接输出顶点shader的结果。比如一个三角形面片,在vertex阶段,分别计算了每个顶点的颜色值,在pixel阶段时,这个面片经过投影,最终显示在屏幕上的像素,会根据该像素周围的顶点来插值计算像素的最终颜色,这种着色方式也叫做高洛德着色。
下面看一下unity shader实现的逐顶点着色:
Shader "ApcShader/DiffusePerVetex"
{//属性Properties{_Diffuse("Diffuse", Color) = (1,1,1,1)}//子着色器    SubShader{Pass{//定义TagsTags{ "RenderType" = "Opaque" }CGPROGRAM//引入头文件#include "Lighting.cginc"//定义Properties中的变量fixed4 _Diffuse;//定义结构体:应用阶段到vertex shader阶段的数据,如果定义了struct a2v{float4 vertex : POSITION;float3 normal : NORMAL;};//定义结构体:vertex shader阶段输出的内容struct v2f{float4 pos : SV_POSITION;fixed4 color : COLOR;};//定义顶点shaderv2f vert(a2v v){v2f o;o.pos = mul(UNITY_MATRIX_MVP, v.vertex);//把法线转化到世界空间float3 worldNormal = mul(v.normal, (float3x3)_World2Object);//归一化法线worldNormal = normalize(worldNormal);//把光照方向归一化fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);//根据兰伯特模型计算顶点的光照信息,dot可能有负值,小于0的部分可以理解为看不见,直接取0fixed3 lambert = max(0.0, dot(worldNormal, worldLightDir));//最终输出颜色为lambert光强*材质diffuse颜色*光颜色o.color = fixed4(lambert * _Diffuse.xyz * _LightColor0.xyz, 1.0);return o;}//定义片元shaderfixed4 frag(v2f i) : SV_Target{return i.color;}//使用vert函数和frag函数#pragma vertex vert#pragma fragment frag    ENDCG}}//前面的Shader失效的话,使用默认的DiffuseFallBack "Diffuse"
}

我们放置两个基本几何体,看一下shader的效果:

逐像素计算着色shader

逐像素计算时,我们的主要计算放到了pixel shader里,在vertex shader阶段只是进行了基本的顶点变换操作,以及顶点的法线转化到世界空间的操作,然后将转化后的法线作为参数传递给pixel shader。其他的计算都放到了pixel shader阶段,这样,针对每个像素,我们都可以来计算这个像素的光照情况,而不是像逐顶点计算时,先计算好顶点的颜色,然后差值得到中间的像素颜色。这种逐像素着色的方式也叫作冯氏着色(注意不是冯氏光照模型,不要搞混呦)。
下面看一下unity shader实现的逐像素着色:
Shader "ApcShader/DiffusePerPixel"
{//属性Properties{_Diffuse("Diffuse", Color) = (1,1,1,1)}//子着色器    SubShader{Pass{//定义TagsTags{ "RenderType" = "Opaque" }CGPROGRAM//引入头文件#include "Lighting.cginc"//定义Properties中的变量fixed4 _Diffuse;//定义结构体:应用阶段到vertex shader阶段的数据struct a2v{float4 vertex : POSITION;float3 normal : NORMAL;};//定义结构体:vertex shader阶段输出的内容struct v2f{float4 pos : SV_POSITION;float3 worldNormal : TEXCOORD0;};//定义顶点shaderv2f vert(a2v v){v2f o;o.pos = mul(UNITY_MATRIX_MVP, v.vertex);//把法线转化到世界空间o.worldNormal = mul(v.normal, (float3x3)_World2Object);return o;}//定义片元shaderfixed4 frag(v2f i) : SV_Target{//归一化法线,即使在vert归一化也不行,从vert到frag阶段有差值处理,传入的法线方向并不是vertex shader直接传出的fixed3 worldNormal = normalize(i.worldNormal);//把光照方向归一化fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);//根据兰伯特模型计算像素的光照信息,小于0的部分理解为看不见,置为0fixed3 lambert = max(0.0, dot(worldNormal, worldLightDir));//最终输出颜色为lambert光强*材质diffuse颜色*光颜色fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz;return fixed4(diffuse, 1.0);}//使用vert函数和frag函数#pragma vertex vert#pragma fragment frag  ENDCG}}//前面的Shader失效的话,使用默认的DiffuseFallBack "Diffuse"
}

还是一个立方体和一个圆柱体,采用了逐像素着色后的结果:

从vertex阶段到fragment阶段发生了什么

我们可以看一下逐顶点着色和逐像素着色的结果对比:
对于正方体,只有单个面,没有特别明显的差别,但是对于圆柱体,就可以看出一些差别了,逐顶点着色的圆柱体可以看出线条状的轮廓,其实每个线条都是由两个三角形面片组成的长方形面片。
为什么逐像素计算会得到更好的效果,因为我们逐像素取的光照的方向是一致的,法线方向也是通过上一步的vertex shader传递过来的,如果像素和顶点对应了的话,那不是每个像素的计算结果都会一样呢?然而,其实像素和顶点是不对应的,这个就是传说中的渲染流水线了,在顶点阶段计算的结果,并不是直接传递给像素着色器的,而是经过了一系列的插值计算,我们从vertex shader传递过来的法线方向,只代表了这一个顶点的顶点法线方向,而到了pixel阶段,这个像素所对应的法线等参数相当于其周围几个顶点进行插值后的结果。我们用这一个像素点对应的法线方向与光照方向进行计算,就可以获得该像素点在光照条件下的颜色值,而不是先计算好颜色再插值得到结果。

半兰伯特光照模型

实现了逐顶点和逐像素的兰伯特光照模型,我们再来看一下兰伯特光照模型的变种--半兰伯特光照。经过上面的对比,逐像素光照计算会获得更好的效果,所以我们下面就采用逐像素的方式来实现半兰伯特光照模型。
上面的shader计算光照的时候,我们计算法线方向和光方向的点乘值时,得到的结果有可能是负数,而兰伯特光照模型对于该情况的处理是,dot值为负数,说明该点不会受到光的照射,所以对于该光源,该点无光,直接使用max(0,diffuse)来将不应该受光的位置全都置为黑色。虽然听起来很有道理的样子,然而这种并好看。
然而,实际上,我们在现实世界中经常会发现,即使我们让一个物体不被光直接照射,我们也可能会看到物体,虽然亮度不是很高,这其实是由于物体之间光的反射造成的,也就是间接光照,间接光照是更高级的渲染,比如光线追踪算法等。但是在实时图形学,我们大部分情况是通过一个环境光(Ambient Light)统一代表了间接光,这样,即使在没有光的时候,我们也可以看见物体。
兰伯特光照出来的时候,貌似还没有这么高科技的技术,所以呢,有人就想到了一个取巧的技术(据说是《半条命》),既保证了兰伯特模型计算出来的光照结果大于0,又整体提升了亮度,使非直接受光面不是单纯的置为黑色。这是一个在图形学领域经常有的变换,区间转化,从(-1,1)转化到(0,1),如果不考虑无意义的负值,也可以说成从(0,1)转化到了(0.5,1)。方法很简单,乘以0.5再加上0.5。这样,原本亮度为1的地方,乘以0.5变成了0.5,加上0.5就又成了1,而原本光照强度为0的地方,就变成了0.5,原本为负数的地方,也能保证为大于0了。半兰伯特光照这种区间转化的原理图如下所示:
下面看一下逐像素计算的半兰伯特光照shader,比兰伯特光照的只是将法线向量与光方向向量的点乘结果用一种更好的方式区间转化到了(0,1)区间:
                           Shader "ApcShader/HalfLambert"
{//属性Properties{_Diffuse("Diffuse", Color) = (1,1,1,1)}//子着色器    SubShader{Pass{//定义TagsTags{ "RenderType" = "Opaque" }CGPROGRAM//引入头文件#include "Lighting.cginc"//定义Properties中的变量fixed4 _Diffuse;//定义结构体:应用阶段到vertex shader阶段的数据struct a2v{float4 vertex : POSITION;float3 normal : NORMAL;};//定义结构体:vertex shader阶段输出的内容struct v2f{float4 pos : SV_POSITION;float3 worldNormal : TEXCOORD0;};//定义顶点shaderv2f vert(a2v v){v2f o;o.pos = mul(UNITY_MATRIX_MVP, v.vertex);//把法线转化到世界空间o.worldNormal = mul(v.normal, (float3x3)_World2Object);return o;}//定义片元shaderfixed4 frag(v2f i) : SV_Target{//归一化法线,即使在vert归一化也不行,从vert到frag阶段有差值处理,传入的法线方向并不是vertex shader直接传出的fixed3 worldNormal = normalize(i.worldNormal);//把光照方向归一化fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);//半兰伯特光照,将原来(-1,1)区间的光照条件转化到了(0,1)区间,既保证了结果的正确,又整体提升了亮度,保证非受光面也能有光,而不是全黑fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;//最终输出颜色为lambert光强*材质diffuse颜色*光颜色fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz;return fixed4(diffuse, 1.0);}//使用vert函数和frag函数#pragma vertex vert#pragma fragment frag   ENDCG}}//前面的Shader失效的话,使用默认的DiffuseFallBack "Diffuse"
}

看一下兰伯特光照模型和半兰伯特光照模型的对比:

所以,正如某图形学大牛说的,图形学这个,没有什么道理,只要看起来好看,那就行了!

带有纹理的半兰伯特光照shader

光照模型再牛,没有纹理也是难看,所以,我们修改一下shader,加上纹理,然后找个帅帅哒模型穿上瞧一瞧。
光照计算主要放在Fragment shader中:
Shader "ApcShader/DiffuseWithTex"
{//属性Properties{_Diffuse("Diffuse", Color) = (1,1,1,1)_MainTex("Base 2D", 2D) = "white"{}}//子着色器    SubShader{Pass{//定义TagsTags{ "RenderType" = "Opaque" }CGPROGRAM//引入头文件#include "Lighting.cginc"//定义Properties中的变量fixed4 _Diffuse;sampler2D _MainTex;//使用了TRANSFROM_TEX宏就需要定义XXX_STfloat4 _MainTex_ST;//定义结构体:应用阶段到vertex shader阶段的数据struct a2v{float4 vertex : POSITION;float3 normal : NORMAL;float4 texcoord : TEXCOORD0;};//定义结构体:vertex shader阶段输出的内容struct v2f{float4 pos : SV_POSITION;float3 worldNormal : TEXCOORD0;//转化纹理坐标float2 uv : TEXCOORD1;};//定义顶点shaderv2f vert(a2v v){v2f o;o.pos = mul(UNITY_MATRIX_MVP, v.vertex);//把法线转化到世界空间o.worldNormal = mul(v.normal, (float3x3)_World2Object);//通过TRANSFORM_TEX宏转化纹理坐标,主要处理了Offset和Tiling的改变,默认时等同于o.uv = v.texcoord.xy;o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);return o;}//定义片元shaderfixed4 frag(v2f i) : SV_Target{//unity自身的diffuse也是带了环境光,这里我们也增加一下环境光fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;//归一化法线,即使在vert归一化也不行,从vert到frag阶段有差值处理,传入的法线方向并不是vertex shader直接传出的fixed3 worldNormal = normalize(i.worldNormal);//把光照方向归一化fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);//根据半兰伯特模型计算像素的光照信息fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;//最终输出颜色为lambert光强*材质diffuse颜色*光颜色fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;//进行纹理采样fixed4 color = tex2D(_MainTex, i.uv);return fixed4(diffuse * color.rgb, 1.0);}//使用vert函数和frag函数#pragma vertex vert#pragma fragment frag  ENDCG}}//前面的Shader失效的话,使用默认的DiffuseFallBack "Diffuse"
}
光照计算主要放在vertex shader中:
Shader "ApcShader/DiffuseWithTexX"
{//属性Properties{_Diffuse("Diffuse", Color) = (1,1,1,1)_MainTex("Base 2D", 2D) = "white"{}}//子着色器    SubShader{Pass{//定义TagsTags{ "RenderType" = "Opaque" }CGPROGRAM//引入头文件#include "Lighting.cginc"//定义Properties中的变量fixed4 _Diffuse;sampler2D _MainTex;//使用了TRANSFROM_TEX宏就需要定义XXX_STfloat4 _MainTex_ST;//定义结构体:应用阶段到vertex shader阶段的数据,如果定义了struct a2v{float4 vertex : POSITION;float3 normal : NORMAL;float4 texcoord : TEXCOORD0;};//定义结构体:vertex shader阶段输出的内容struct v2f{float4 pos : SV_POSITION;fixed4 color : COLOR;//转化纹理坐标float2 uv : TEXCOORD1;};//定义顶点shaderv2f vert(a2v v){v2f o;o.pos = mul(UNITY_MATRIX_MVP, v.vertex);//unity自身的diffuse也是带了环境光,这里我们也增加一下环境光fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;//把法线转化到世界空间float3 worldNormal = mul(v.normal, (float3x3)_World2Object);//归一化法线worldNormal = normalize(worldNormal);//把光照方向归一化fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);//根据兰伯特模型计算顶点的光照信息,dot可能有负值,小于0的部分可以理解为看不见,直接取0fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;//最终输出颜色为lambert光强*材质diffuse颜色*光颜色o.color = fixed4(lambert * _Diffuse.xyz * _LightColor0.xyz + ambient, 1.0);//通过TRANSFORM_TEX宏转化纹理坐标,主要处理了Offset和Tiling的改变,默认时等同于o.uv = v.texcoord.xy;o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);return o;}//定义片元shaderfixed4 frag(v2f i) : SV_Target{return i.color * tex2D(_MainTex, i.uv);}//使用vert函数和frag函数#pragma vertex vert#pragma fragment frag    ENDCG}}//前面的Shader失效的话,使用默认的DiffuseFallBack "Diffuse"
}
我们用一个人物模型,分别使用两种shader,进行一下对比,左侧的shader主要计算在vertex,右侧的shader主要计算放在pixel:
可以看出,如果模型比较细致,其实在diffuse情况下,是没有特别明显的区别的,而大部分计算放在vertex shader中,对于效率更有益处,vertex shader一般不是GPU的瓶颈,逐顶点计算可以比逐像素计算省很多,所以将尽可能多的计算放在vertex阶段而不是fragment阶段是一个很好的优化shader的策略。但是,注意!是在diffuse的情况,如果我们的shader中有高光specular,那么,用逐顶点计算高光就会出现特别难看的光斑,这个下篇文章再进行介绍。
由于unity shader中diffuse是带有环境光的,所以我们也在shader中计算了环境光。由于没有全局光照,所以间接光照就通过UNITY_LIGHTMODEL_AMBIENT这个宏进行访问

TRANSFORM_TEX宏

在添加了纹理之后,主要使用了一个宏和一个采样函数。采样函数顾名思义,tex2D,就是通过传入的纹理坐标,来获得纹理采样点所对应的颜色值。下面重点看一下Unity为我们提供的TRANSFORM_TEX宏,我们从UnityCG.cginc中可以找到这个宏的定义如下:

// Transforms 2D UV by scale/bias property
#define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)

如果我们使用了这个宏,就需要在shader中定义我们要采样的纹理的一个系数,命名方式为 纹理名_ST,float4类型。那么这个值是什么呢?

就是这个啦!我们在使用纹理时,unity会为我们提供两个参数,一个是Tiling,一个是Offset。简单来说,Tiling表示纹理的缩放比例,Offset表示了纹理使用时采样的偏移值。关于Tiling和Offset的介绍,可以参考这篇文章。知道了这个,也就好理解TRANSFORM_TEX宏所做的事情了,在采样时,将材质面板上设置的Tiling值通过XXX_ST.xy传递进来,用于和采样的坐标相乘,进行采样的缩放,将Offset值通过XXX_ST.zw传递进来,作为纹理采样的偏移。

Unity Shader-兰伯特光照模型与Diffuse Shader相关推荐

  1. Unity Shader漫反射光照模型与半兰伯特光照模型

    文章目录 一.基础光照模型中漫反射部分的计算公式 二.逐顶点光照实现 逐顶点代码 逐顶点效果图 三.逐像素光照模型实现 逐像素代码 逐像素效果图 四.半兰伯特光照模型实现 广义半兰伯特光照模型公式 半 ...

  2. Unity shader学习-漫反射-兰伯特光照模型和半兰伯特光照模型

    兰伯特漫反射公式:Diffuse = 直射光颜色 *物体颜色* max(0,cos夹角(光和法线的夹角) ) 下面给出顶点漫反射代码:   Shader "Unlit/005" { ...

  3. shader基础学习摘要(二) 兰伯特光照模型

    在第6.4节中,书中给出了计算基本光照模型中漫反射光部分的计算公式分别表示为: 目录 漫反射模型 逐像素光照(逐顶点光照改进版) 逐顶点光照(兰伯特原版) 半兰伯特光照模型 漫反射模型 兰伯特定律:反 ...

  4. 9.半兰伯特光照模型

    前情提要:前面一节的漫反射实际上是根据兰伯特光照模型来进行实现的 但这种做法有个缺点就是顶点法线和光的夹角大于90度的时候的成色是完全黑的这和我们生活中见到的事物是不一样的.所以就有了新的光照模型-- ...

  5. 庄懂老师TA学习笔记 - 半兰伯特光照模型

    先看一下兰伯特光照模型的公式 Max(0, nDir ·lDir) 通过公式我们知道,当法线方向和光照方向的反方向的角度大于90度时,点乘就会出现负数的结果,所以我们使用与0取最大值来排除掉这些没有实 ...

  6. 兰伯特光照模型(Lambert Lighting)和半兰伯特光照模型(Half-Lanbert)

    关于漫反射 光打到凹凸不平的平面上,光线会被反射到四面八方,被称为漫反射 关于这种模型,由于光线由于分散,所以进入人眼的光线强度和观察角度没有区别 在A点和B点接收到的光线强度是一样的 在漫反射下,光 ...

  7. 伪兰伯特光照模型处理阴影过渡

    默认shader的光照过渡太明显,不够柔和,使用伪兰伯特光照模型进行处理 伪兰伯特 默认材质 Shader "Custom/SimulateLambert" {Properties ...

  8. 光照模型-兰伯特光照模型

    兰伯特光照模型理论基础

  9. Shader学习第六篇:Lambert (兰伯特)光照模型

    漫反射的定义 漫反射是投射在粗糙表面上的光向各个方向反射的现象.当一束平行的入射光线射到粗糙的表面时,表面会把光线向着四面八方反射,所以入射线虽然互相平行,由于各点的法线方向不一致,造成反射光线向不同 ...

最新文章

  1. 【新鲜出炉】25套的精美 Web 应用程序图标素材
  2. python 基础教程:对 property 属性的讲解及用法
  3. python画海绵宝宝_脑洞大开的万圣节,10招带你和宝宝花式玩南瓜
  4. 第二讲:第一个Python程序(干货)
  5. Servlet API
  6. Selenium WebDriver的TestNG注释完整指南
  7. golang 切片 接口_Go编程模式:切片,接口,时间和性能
  8. Java快速入门学习笔记5 | Java语言中的while循环语句
  9. hdu 1542 Atlantis (线段树+扫描线)
  10. 从服务器上下载下来的代码,部署到本地时,Url自动带www前缀
  11. java字符串替换一部分_字符串中部分字符替换
  12. mysql 分析explain命令执行sql的计划
  13. 嵌入式系统应用开发学习笔记(五):HLS
  14. maya! board_3D角色模型很难做?Maya、Zbrush人头建模终极秘笈
  15. 工具说明书 - 英语语法检查工具Grammarly
  16. Code::Blocks 的配色方案
  17. Android插件化动态加载apk
  18. 山东科技大学OJ题库 1013-多少张钞票
  19. Lua程序设计 | 字符串、表、函数与IO
  20. 《电气工程制图与读图》

热门文章

  1. miui10.2.2 或以上的小米手机上照片旋转问题及解决
  2. mybatisplus报 Invalid bound statement (not found):
  3. 常用sql server 脚本
  4. 键盘·由于其配置信息(注册表中的)不完整或已损坏,Windows 无法启动这个硬件设备。 (代码19)
  5. Python 利用opencv给白底照片换色,全网唯一
  6. GeneXus学习笔记-Excel导出
  7. lol服务器维护8.21,lol8.21版本更新了什么 lol8.21版本更新内容一览
  8. Android无法打开相册查看视频
  9. php一句话木马调用cmd命令,一句话木马(webshell)是如何执行命令的
  10. 大学计算机D(VB.NET)