Unity3d渲染系列教程二 shader基础(译)

课程目标:

顶点转换

像素着色

Shader属性

从顶点着色器传递数据到片段着色器

查看编译的shader代码

使用tiling 和offset属性从纹理取样

 

这是渲染的第二部分教程。第一部分讲解了矩阵。现在是时候开始写我们的第一个shader了。

这部分的教程是基于Unity 5.4.0b10.

 

Texturing a sphere.

在unity3d中创建一个新的场景。使用默认的相机跟平行光就可以了。接下来让我们通过GameObject/ 3D Object / Sphere,创建一个球体。并把它的位置设置在原点,并调整摄像机,把球体放到视野的中心。

Default sphere in default scene.

这个场景很简单。对于我们从基础理解shader是很有帮助的。

先让我们看一下场景中的光照设置。通过Window / Lighting.打开光照设置界面。我们目前只关注 Scene分页。

Default lighting settings.

这个分页有一部分是关于环境光的设置。在这儿你可以选择一个天空盒作用当前场景的背景,环境光以及反射。我们把天空盒设为None来关闭它。

我们同时在这个界面上关闭 precomputed and real-time globalillumination。

No more skybox.

关闭天空盒后。环境光自动变成纯色。默色的的是深灰加一丝蓝色。反射颜色则是纯黑色。如图所示。

正同你所期望的一样。现在这个球体变暗了,并且背景变成了纯色。那这纯蓝的背景又是比哪儿来的呢?

Simplified lighting.

这背景色是在相机中定义的。它默认显示天空盒。但我们把天空盒关闭之窗之后背景就变成纯色显示了。

Default camera settings.

 

为了让渲染更加简单,让我们把平行方设为不可用或者直接删除它。现在场景中就只剩下一个纯色背景和一个球体的轮廓。

In the dark.

目前的我们的场景是用两步绘制出来。首先,场景用背景色填充,然后球体的轮廓绘制在场景上面。

那么unity怎么知道有一个球体需要绘制呢? 我们场景中的球体有个meshrenderer组件。如果这个球体在摄像机的视野内,它就应该被绘制出来。Unity会通过检查物体的边界框是否与相机的视锥体相交来验证这一点。

Default sphere object.

Transform组件用来改变网格以及边界框的位置,朝向以及它的大小。如果这个对象最后在摄像机的视野内,这个对象就会被渲染。

最后。GPU就会着手绘制这个对象网格。渲染指令是由对象的材质定义的。对象的材质引用一个shader,它是一个GPU程序,加上一些相关的设置。

Who controls what.

目前我们的对象使用的默认的材质。我们将会使用自定义的shader来替换它。现在就让我们从头创建自己的shader。

通过Assets / Create / Shader / Unlit Shader来创建一个新的shader,并且将它重命名My First Shader

Your first shader.

打开这个shader文件并且清除它的内容,让我们从头开始编写我们的第一个shader.shader是由Shader关键字定义的。后面跟着用来选择这个shader的菜单项的一个字符串。菜单项并不需要跟文件名保持一致。然后跟着一对大括号包含着这个shader具体的内容

Shader "Custom/MyFirst Shader" {

 

}

保存这个文件。你会看到这个shader不被支持的警告。因为这个shader并没有内容,暂时不用管它。我们现在通过Assets / Create / Material创建一个新材质,然后在材质的shader菜单中选择我们刚刚创建的shader

 

Material with your shader.

将球体用我们新建的材质替换默认的材质。球体将会变成粉红色。这是因为unity会用内置的error shader替换出错的shader。 

Material with your shader.

着色器错误提示我们需要子着色器。您可以使用它们将多个着色器分组在一起。 这允许您为不同的构建平台或细节级别提供不同的子着色器。 例如,您可以为桌面和手机分别创建一个子着色器。我们只需要一个子着色器块。

Shader "Custom/MyFirst Shader" {

 

        SubShader{

              

        }

}

子着色器必须包含至少一个pass。 pass是对象实现渲染的地方。一个子着色器可以拥有多个pass,在这里我们只需要一个就够了。

Shader "Custom/MyFirst Shader" {

 

        SubShader{

 

               Pass{

 

               }

        }

}

我们的球体现在看起来可能白色了。因为这是空 pass的默认显示。这意味着我们的shader已经没有错误了。或者也可能会报错。这取决于不同的编译平台。

A white sphere.

现在是时候开始编写我们的shader程序了。Shader用的是unity的着色语言,它是HLSL和CG着色语言的变体。先让我添加上程序开始跟结束的标志。

               Pass{

                       CGPROGRAM

 

                       ENDCG

               }

现在shader编译器提示我们的shader缺少顶点跟片段着色器。Shader由两个程序组成。顶点着色器负责处理顶点数据以及网格。包括从从对象空间转换到显示空间。片段着色器负责为网格中的三角形中的每个像素着色

Vertex and fragment program.

我们先通过#pragma指令告诉编译顺我们想要哪两个程序作为我们的顶点跟片段着色器。

                       CGPROGRAM

 

                       #pragma vertexMyVertexProgram

                       #pragma fragmentMyFragmentProgram

 

                       ENDCG

What's a pragma?

现在编译器会警告我们找不到指定的程序。现在让我们来实现它。

顶点以及片段着色器是以方法的形式存在的。跟C#的很相似。让我们先创建两个无返回值的空方法。

                       CGPROGRAM

 

                       #pragma vertexMyVertexProgram

                       #pragma fragmentMyFragmentProgram

 

                       void MyVertexProgram () {

 

                       }

 

                       void MyFragmentProgram () {

 

                       }

 

                       ENDCG

现在shader能够正常编译了。但是这个球体消失了。或者有可能依然会报错。这取决于你使用哪个渲染平台。

这是因为Unity的着色器编译器会根据不同的目标平台将我们的代码转换成不同的程序。不同的平台需要不同的解决方案。例如.windows平台上的Direct3D,Macs平台上的OpenGl以及移动平台上的OpenGL ES等等。

你最终使用的编译器取决于你的目标平台。由于这些编译器各不相同,因此每个平台可能得到不同的结果。就像我们的空程序能正常在OpenGL和Direct3D 11上工作,但是在Direct3D 9上运行时却会失败。

在unity编辑器里选择shader文件在inspector窗口中会显示这个shader的一些信息,包括编译错误等。按钮“Compile and show code”甚至还包含编译后的代码入口以及一个下拉菜单。点击这个按钮unity会编译这个shader并且在编辑器打开编译后的代码。

Shader inspector, with errors for all platforms.

你可以通过点击“Compile and show code”按钮的下拉菜单来选择你要编译的平台。Unity默认选择unity编辑器使用的图形设备。您也可以手动编译其他平台,这使您可以快速确保着色器在多个平台上编译,而不需要完整的构建。

Selecting OpenGLCore.

若要编译选定的程序。关闭弹出菜单然后点击“Compile and show code”按钮。在下拉菜单中点击“show”按钮会展示这个shader中使用的变量。这个暂时我们还用不到

例如,下面是我们的shader用OpenGlCore编译器编译后生成的代码。

// Compiled shaderfor custom platforms, uncompressed size: 0.5KB

 

// Skipping shadervariants that would not be included into build of current scene.

 

Shader "Custom/MyFirst Shader" {

SubShader {

 Pass {

  GpuProgramID 16807

Program "vp" {

SubProgram "glcore " {

"#ifdef VERTEX

#version 150

#extensionGL_ARB_explicit_attrib_location : require

#extensionGL_ARB_shader_bit_encoding : enable

void main()

{

    return;

}

#endif

#ifdef FRAGMENT

#version 150

#extensionGL_ARB_explicit_attrib_location : require

#extensionGL_ARB_shader_bit_encoding : enable

void main()

{

    return;

}

#endif

"

}

}

Program "fp" {

SubProgram "glcore " {

"// shaderdisassembly not supported on glcore"

}

}

 }

}

}

生成的代码被拆分成了两块。顶点跟片段着色器分别对应vp 和fp。不管如何,在opengl的情况下,两个程序都会在vp块中调用。让我们先忽略其他代码,将注意力放在这两个代码块上。

#ifdef VERTEX

void main()

{

    return;

}

#endif

#ifdef FRAGMENT

void main()

{

    return;

}

#endif

下面是为Direct3D 11生成的代码,我们只保留我们感兴趣的部分。它看起来跟上面的很不同,但是很明显他也并没有做很多事情。

Program "vp" {

SubProgram "d3d11 " {

      vs_4_0

   0: ret

}

}

Program "fp" {

SubProgram "d3d11 " {

      ps_4_0

   0: ret

}

}

在我们写代码的时候,我会经常展示编译后的 OpenGLCore 和D3D11代码。这样你就可以知道底层究竟是怎么工作的。

引用其他的文件。

在你编写你自己的shader的时候你可能需要很多的样板代码。类似于定义一些共用变量,方法以及其他一些东西。在C#程序中,我们将这些代码放在其他类里面。但是shaders没有类的概念。它把所有的代码都放在一个文件里,并没有提供类或者命名空间类似的功能。

庆幸的是,我们可以把代码拆分到多个文件里,然后使用#include指令将其他文件的内加载到当前文件里面。一个典型的例子就是#include"UnityCG.cginc"

                       CGPROGRAM

 

                       #pragma vertexMyVertexProgram

                       #pragma fragmentMyFragmentProgram

 

                       #include"UnityCG.cginc"

 

                       void MyVertexProgram () {

 

                       }

 

                       void MyFragmentProgram () {

 

                       }

 

                       ENDCG

UnityCG.cginc是unity内置的shader文件之一。它也includes了一些其他的必要的文件,而且包含一些公用的方法。

Include file hierarchy, starting at UnityCG.

引用文件的层次结构。

UnityShaderVariables.cginc 定义一组渲染所必需的着色器变量,如转换、相机和光照数据。这些都是在需要时由Unity赋值的。

HLSLSupport.cginc 帮你处理不同平台的差异,让你可以在不同的目标平台上使用相同的代码。因此,您不必担心使用需要使用平台特定的数据类型等。

UnityInstancing.cginc具体用于instancing支持,这是一种减少绘制调用的特定渲染技术。虽然它不直接包含文件,但它依赖于UnityShaderVariables.

请注意,这些文件的内容被复制到您自己的文件中,替换#include指令。这发生在预处理步骤中,这个步骤执行所有的预处理指令。预处理指令是由#开始的语句,比如#include和#pragma。完成该步骤后,编译器会再次对代码进行处理,并对其进行编译。

为了能正确显示对象。我们的顶点着色器程序需要返回顶点的四维投影坐标。就像我们在矩阵那部分所讨论的一样。

将顶点着色器方法的返回值从void 改为float4 。 float4 是一个简单的4个浮点数字的集合。然后在方法实现中暂直接返回0.

                       float4MyVertexProgram () {

                               return 0;

                       }

 

你们可能会奇怪,不是应该返回float4类型的值么。返回0不会报错吗?编译器会自动为float4的四个元素都用我们的返回的0来填充。当然,我们也可以显示返回 return float4(0,0,0,0)。

不过我们编译器还是会报错。因为它不知道我们返回的float4代表的是什么。我们必须使用SV_POSITION 语义来说明我们返回的是顶点的坐标。SV表示的是systemvalue 而POSITION表示这是最终的系统坐标。

                       float4MyVertexProgram () : SV_POSITION {

                               return 0;

                       }

片段着片器输出的是一个像素的最终的RGBA颜色值。我们同样可以使用float4来代表颜色。return 0 将会返回纯黑色。

                       float4MyFragmentProgram () {

                               return 0;

                       }

Wouldn't 0 alpha be fully transparent?

片段程序也需要语义。 在这种情况下,我们必须指出最终颜色的写入位置。 我们使用SV_TARGET,它是默认的着色器目标,帧缓冲区,它保存了我们正在生成的图像。

                       float4MyFragmentProgram () : SV_TARGET {

                               return 0;

                       }

而片段着色器是用顶点着色器的返回值作为参数的。所以我们需要让片段着色器拥有跟顶点着色器的返回相匹配的参数。

                       float4MyFragmentProgram (float4 position): SV_TARGET {

                               return 0;

                       }

片段着色器的参数名字你可以自定义。但是同样的你必须给它指定正确的语义。

                       float4MyFragmentProgram (

                               float4position : SV_POSITION

                       ): SV_TARGET {

                               return 0;

                       }

Can we omit the position parameter?

现在我们的着色器就能正常地编译了。但是运行后你会发现球体不见了。这是因为我们把所有顶点都绘制到一个点上了。

我们看一下用OpenGLCore编译后的代码。你会发现它已经给输出值赋值了。而且我们返回的0已经被拥有四个元素的vec代替了。

#ifdef VERTEX

void main()

{

    gl_Position= vec4(0.0, 0.0, 0.0, 0.0);

    return;

}

#endif

#ifdef FRAGMENT

layout(location = 0) out vec4SV_TARGET0;

void main()

{

    SV_TARGET0 =vec4(0.0, 0.0, 0.0, 0.0);

    return;

}

#endif

D3D11程序也是如此,虽然他们的语法不同。

Program "vp" {

SubProgram "d3d11 " {

      vs_4_0

     dcl_output_siv o0.xyzw, position

   0: mov o0.xyzw, l(0,0,0,0)

   1: ret

}

}

Program "fp" {

SubProgram "d3d11 " {

      ps_4_0

      dcl_outputo0.xyzw

   0: mov o0.xyzw, l(0,0,0,0)

   1: ret

}

}

顶点转换

为了能让我们球体正常显示。我们的顶点着色器需要返回正确的顶点坐标。首先我们需要知道顶点的对象空间坐标。我们可以顶点着色器添加一个参数使用POSITION作为语义来获得我们需要的坐标。当然这个坐标也是格式的。所以我们的参数也应该是float4类型的

                       float4MyVertexProgram (float4 position : POSITION) : SV_POSITION{

                               return 0;

                       }

先让我们直接返回这个位置看看会发生什么。

                       float4MyVertexProgram (float4 position : POSITION) : SV_POSITION{

                               return position;

                       }

现在编译后的顶点着色器就拥有一个vec4输入值并且将它赋值给方法的输出。

in  vec4 in_POSITION0;

void main()

{

    gl_Position= in_POSITION0;

    return;

}

Bind "vertex" Vertex

      vs_4_0

      dcl_inputv0.xyzw

     dcl_output_siv o0.xyzw,position

   0: mov o0.xyzw,v0.xyzw

   1: ret

Raw vertex positions.

 

现在界面会显示一个扭曲的黑色球体。那是因为我们使用对象空间坐标作为它的显示坐标。正因为如此。我们移动球体最终显示并不会有什么差别。

我们必须将原始的顶点坐标与模型视图投影矩阵(MVP)相乘。这个矩阵将对象变换,相机变换以及投影变换 相结合。就是把对象空间坐标转换成投影坐标。就像我们在矩阵那一节做的一样。UNITY_MATRIX_MVP 保存了这个4x4矩阵 。UNITY_MATRIX_MVP是在UnityShaderVariables 文件中定义的。我们使用mul方法 将顶点跟这个矩阵相乘。这会将我们的球体正确地投影到显示器上。当然你对球体的移动旋转缩放也会影响这个球体的显示。

                       float4MyVertexProgram (float4 position : POSITION) : SV_POSITION{

                               return mul(UNITY_MATRIX_MVP, position);

                       }

Correctly positioned.

如果您检查OpenGLCore顶点程序,您会注意到突然出现了很多uniform变量。即使它们没有被使用,访问矩阵触发了编译器来包含这些变量。

您还将看到矩阵乘法,编码为一系列乘法和加法。

uniform         vec4 _Time;

uniform         vec4 _SinTime;

uniform         vec4 _CosTime;

uniform         vec4 unity_DeltaTime;

uniform         vec3 _WorldSpaceCameraPos;

in  vec4 in_POSITION0;

vec4 t0;

void main()

{

    t0 =in_POSITION0.yyyy *glstate_matrix_mvp[1];

    t0 =glstate_matrix_mvp[0] * in_POSITION0.xxxx + t0;

    t0 =glstate_matrix_mvp[2] * in_POSITION0.zzzz + t0;

    gl_Position= glstate_matrix_mvp[3] * in_POSITION0.wwww + t0;

    return;

}

D3D11编译后的代码不会包含未使用的变量。它把矩阵相乘编译成一个mul 和三个mad指令。mad指令代表乘法后面跟着一个加法。

Bind "vertex" Vertex

ConstBuffer "UnityPerDraw" 352

Matrix 0[glstate_matrix_mvp]

BindCB  "UnityPerDraw" 0

      vs_4_0

     dcl_constantbuffer cb0[4],immediateIndexed

      dcl_inputv0.xyzw

     dcl_output_siv o0.xyzw,position

      dcl_temps 1

   0: mul r0.xyzw,v0.yyyy, cb0[1].xyzw

   1: mad r0.xyzw,cb0[0].xyzw,v0.xxxx, r0.xyzw

   2: mad r0.xyzw,cb0[2].xyzw,v0.zzzz, r0.xyzw

   3: mad o0.xyzw,cb0[3].xyzw,v0.wwww, r0.xyzw

   4: ret

像素着色。

现在我们得到了对象正确的形状。让我们再给它添加一些颜色。最简单的办法就是使用固定的颜色。就像黄色一样。

                       float4MyFragmentProgram (

                               float4position : SV_POSITION

                       ) : SV_TARGET{

                               return float4(1, 1, 0, 1);

                       }

Yellow sphere.

当然你不想所有的对象都是黄色的。理想情况下,我们的着色器应该能支持任何颜色。你可以使用该材质设置你想要的颜色。这个可以通过着色器的properties来完成。

Shader Properties

shader properties在单独的一个大括号中声明。

Shader "Custom/MyFirst Shader" {

 

        Properties{

        }

 

        SubShader{

               …

        }

}

将一个属性名字为_Tint添加到我们的块内。当然你可以任意命名,但是惯例是一个下划线加一个大写的字母,后面跟着小写的字母。

        Properties{

               _Tint

        }

属性名后面必须在小括号内跟一个字符串以及一个类型。就像你调用方法一样。字符串是你在材质面板中显示的名字。在这个示例中,我们的_Tint类型是Color

        Properties{

               _Tint("Tint", Color)

        }

在属性的最后我们需要声明该属性的默认值。让我们把它设置为白色。

        Properties{

               _Tint("Tint", Color) = (1, 1, 1, 1)

        }

现在我们应该能在着色器的inspector中的属性部分看到tint属性了。

我们现在就可以选择想要的颜色了。

Material Properties.

访问属性

为了使用这个属性,我们需要添加一个同名变量到着色器代码中。然后我们可以在片段着色器中返回这个变量。

                       #include"UnityCG.cginc"

 

                       float4_Tint;

 

                       float4MyVertexProgram (float4 position : POSITION) : SV_POSITION{

                               return mul(UNITY_MATRIX_MVP, position);

                       }

 

                       float4MyFragmentProgram (

                               float4position : SV_POSITION

                       ): SV_TARGET {

                               return _Tint;

                       }

注意,变量一定要在使用之前定义

现在编译后的片段程序就已经包含了tint变量了。

uniform         vec4 _Time;

uniform         vec4 _SinTime;

uniform         vec4 _CosTime;

uniform         vec4 unity_DeltaTime;

uniform         vec3 _WorldSpaceCameraPos;

uniform         vec4 _Tint;

layout(location = 0) out vec4SV_TARGET0;

void main()

{

    SV_TARGET0 =_Tint;

    return;

}

ConstBuffer "$Globals" 112

Vector 96[_Tint]

BindCB  "$Globals" 0

      ps_4_0

     dcl_constantbuffer cb0[7],immediateIndexed

      dcl_outputo0.xyzw

   0: mov o0.xyzw,cb0[6].xyzw

   1: ret

Green sphere.

从顶点着色器传向片段着色器传递数据

目前为止所有的像素都是同样的颜色的,这是不够的。一般情况下,顶点数据在片段着色器会发挥很大的作用。例如,我们可以将坐标想象成颜色,因为它们都是float4类型的。但是转换后的顶点坐标并不是非常有用的。所以让我们使用对象空间坐标作为顶点的颜色。但我们怎么把这个额外的数据从顶点着色器传递给片段着色器?

Gpu是通过光栅化三角形来生成颜色的。这需要三个顶点的数据并且对三角形中的点进行插值进计。这个三角形中的每个像素都会调用一次片段着色器并且把插值后得到的数据传进去。

Interpolatingvertex data.

所以顶点着色器的输出并不是直接作为片段着色器的输入的。中间还有一个插值的过程。现在我们的程序中 SV_POSITION 会被进行插值计算。但是其他的数据也是可以的。

为了取得对象空间坐标的插值,在片段着色器中添加一个变量。因为我们只需要XYZ三个坐标的值。所以用float3作为类型就可以了。我们可以把这个坐标直接当作颜色返回就行了。不过同样我们需要提供第四个元素的值,我们让他保持为1就可以了。

                       float4MyFragmentProgram (

                               float4position : SV_POSITION,

                               float3localPosition

                       ): SV_TARGET {

                               return float4(localPosition, 1);

                       }

当然我们还需要为这个参数提供语义告诉编译器如何理解这个数据。这儿我们使用TEXCOORD0.作为语义

                       float4MyFragmentProgram (

                               float4position : SV_POSITION,

                               float3localPosition : TEXCOORD0

                       ): SV_TARGET {

                               return float4(localPosition, 1);

                       }

We're not working with texture coordinates, so why TEXCOORD0?

编译后的顶点着色器现在会使用这个插值数据代替原来的tint变量给最终颜色赋值

in  vec3 vs_TEXCOORD0;

layout(location = 0) out vec4SV_TARGET0;

void main()

{

    SV_TARGET0.xyz = vs_TEXCOORD0.xyz;

    SV_TARGET0.w = 1.0;

    return;

}

     ps_4_0

     dcl_input_ps linear v0.xyz

      dcl_outputo0.xyzw

   0: mov o0.xyz,v0.xyzx

   1: mov o0.w,l(1.000000)

   2: ret

当然现在顶点着色器需要为片段着色器提供这个值。我们可以在方法参数中添加一个out 类型的参数并且使用相同的 TEXCOORD0 作为语义。当然名字并不需要跟片段着色器保持一致。编译器是用语义作为标识的。

                       float4MyVertexProgram (

                               float4position : POSITION,

                               out float3 localPosition : TEXCOORD0

                       ): SV_POSITION {

                               return mul(UNITY_MATRIX_MVP, position);

                       }

将X Y Z的值从position复制到localPosition中

                       float4MyVertexProgram (

                               float4position : POSITION,

                               out float3 localPosition : TEXCOORD0

                       ): SV_POSITION {

                               localPosition= position.xyz;

                               return mul(UNITY_MATRIX_MVP, position);

                       }

What does .xyz do?

这额外添加的输出当然会包含 在顶点着色器的编译后的代码中。现在我们的球体已经成功被绘制了。

in  vec4 in_POSITION0;

out vec3 vs_TEXCOORD0;

vec4 t0;

void main()

{

    t0 =in_POSITION0.yyyy *glstate_matrix_mvp[1];

    t0 =glstate_matrix_mvp[0] * in_POSITION0.xxxx + t0;

    t0 =glstate_matrix_mvp[2] * in_POSITION0.zzzz + t0;

    gl_Position= glstate_matrix_mvp[3] * in_POSITION0.wwww + t0;

   vs_TEXCOORD0.xyz =in_POSITION0.xyz;

    return;

}

Bind "vertex" Vertex

ConstBuffer "UnityPerDraw" 352

Matrix 0[glstate_matrix_mvp]

BindCB  "UnityPerDraw" 0

      vs_4_0

     dcl_constantbuffer cb0[4],immediateIndexed

      dcl_inputv0.xyzw

     dcl_output_siv o0.xyzw,position

      dcl_outputo1.xyz

      dcl_temps 1

   0: mul r0.xyzw, v0.yyyy, cb0[1].xyzw

   1: mad r0.xyzw,cb0[0].xyzw,v0.xxxx, r0.xyzw

   2: mad r0.xyzw,cb0[2].xyzw,v0.zzzz, r0.xyzw

   3: mad o0.xyzw,cb0[3].xyzw,v0.wwww, r0.xyzw

   4: mov o1.xyz,v0.xyzx

   5: ret

Interpreting local positions as colors.

使用Structures

你是不是觉得我们的参数列表看着有点乱?当我们需要传的参数越多参数列表就会看起来越乱。我们可以使用Structures一次传递多个数据。

首先我们需要定义Structures,它本质上只是一系列变量的集合。下面这个Interpolators结构包含了我们已经处理过的2个参数的。注意每个变量定义后面需要用分号结尾

                       struct Interpolators {

                               float4 position : SV_POSITION;

                               float3 localPosition : TEXCOORD0;

                       };

使用这个结构让我们的代码看起来更整洁一点。

                       float4_Tint;

                                  

                       struct Interpolators {

                               float4position : SV_POSITION;

                               float3localPosition : TEXCOORD0;

                       };

 

                       InterpolatorsMyVertexProgram (float4 position : POSITION) {

                               Interpolators i;

                               i.localPosition= position.xyz;

                               i.position =mul(UNITY_MATRIX_MVP, position);

                               return i;

                       }

 

                       float4MyFragmentProgram (Interpolators i) : SV_TARGET{

                               return float4(i.localPosition, 1);

                       }

用代码修改颜色值。

因为颜色的值的范围是[0,1],所以负值会被转变成0,这让我们的球体看起来有点暗。我们希望它们的值在[0,1]范围内,所以我们为所有的通道加上½

                               return float4(i.localPosition + 0.5, 1);

Local position recolored.

我们还可以将tint作为因子乘到我们的结果中去

                               return float4(i.localPosition + 0.5, 1) *_Tint;

uniform         vec4 _Tint;

in  vec3 vs_TEXCOORD0;

layout(location = 0) out vec4SV_TARGET0;

vec4 t0;

void main()

{

    t0.xyz = vs_TEXCOORD0.xyz+ vec3(0.5, 0.5, 0.5);

    t0.w = 1.0;

    SV_TARGET0 =t0 * _Tint;

    return;

}

ConstBuffer "$Globals" 128

Vector 96[_Tint]

BindCB  "$Globals" 0

      ps_4_0

     dcl_constantbuffer cb0[7],immediateIndexed

     dcl_input_ps linear v0.xyz

      dcl_outputo0.xyzw

      dcl_temps 1

   0: add r0.xyz,v0.xyzx, l(0.500000, 0.500000, 0.500000, 0.000000)

   1: mov r0.w,l(1.000000)

   2: mul o0.xyzw, r0.xyzw, cb0[6].xyzw

   3: ret

Local position with a red tint,so only X remains.

纹理

如果您想为网格添加更多的细节和多样性,而无需添加更多三角形,则可以使用纹理。然后,将纹理投影到三角形网格上。

纹理坐标用于控制投影。无论纹理的实际宽高比如何,都是用一个1单元的2D坐标覆盖整个纹理区域。水平坐标为U,垂直坐标为V.因此,它们通常称为UV坐标。

UV coordinates covering an image.

U坐标从左到右增加。所以它在图像的左侧为0,中间为1/2,右侧为1 V坐标相同。它从底部到顶部增加,除了Direct3D,它从顶部到底部。你几乎从不需要担心这种差异

使用UV坐标

Unity的默认网格包含纹理映射的UV坐标。顶点程序可以通过带有TEXCOORD0语义的参数访问它们。

                       InterpolatorsMyVertexProgram (

                               float4position : POSITION,

                               float2 uv : TEXCOORD0

                       ){

                               Interpolatorsi;

                               i.localPosition= position.xyz;

                               i.position= mul(UNITY_MATRIX_MVP, position);

                               return i;

                       }

我们的顶点程序现在也有多个输入参数。同样,我们可以使用一个struct来存储它们。

                       struct VertexData {

                               float4 position : POSITION;

                               float2 uv : TEXCOORD0;

                       };

                      

                       InterpolatorsMyVertexProgram (VertexData v) {

                               Interpolatorsi;

                               i.localPosition= v.position.xyz;

                               i.position= mul(UNITY_MATRIX_MVP, v.position);

                               return i;

                       }

                      

让我们直接将UV坐标传递给片段程序,替换对象空间坐标。

                       struct Interpolators {

                               float4position : SV_POSITION;

                               float2 uv : TEXCOORD0;

//                                               float3localPosition : TEXCOORD0;

                       };

 

                       InterpolatorsMyVertexProgram (VertedData v) {

                               Interpolatorsi;

//                                               i.localPosition= v.position.xyz;

                               i.position= mul(UNITY_MATRIX_MVP, v.position);

                               i.uv = v.uv;

                               return i;

                       }

通过将它们解释为颜色通道,我们可以使UV坐标可见,就像本地位置一样。 例如,U变为红色,V变为绿色,而蓝色始终为1

                       float4MyFragmentProgram (Interpolators i) : SV_TARGET {

                               return float4(i.uv, 1, 1);

                       }

您将看到编译后的顶点着色器现在将UV坐标从顶点数据复制到interpolator结构中输出。

in  vec4 in_POSITION0;

in  vec2 in_TEXCOORD0;

out vec2 vs_TEXCOORD0;

vec4 t0;

void main()

{

    t0 =in_POSITION0.yyyy *glstate_matrix_mvp[1];

    t0 =glstate_matrix_mvp[0] * in_POSITION0.xxxx + t0;

    t0 =glstate_matrix_mvp[2] * in_POSITION0.zzzz + t0;

    gl_Position= glstate_matrix_mvp[3] * in_POSITION0.wwww + t0;

   vs_TEXCOORD0.xy =in_TEXCOORD0.xy;

    return;

}

Bind "vertex" Vertex

Bind "texcoord" TexCoord0

ConstBuffer "UnityPerDraw" 352

Matrix 0[glstate_matrix_mvp]

BindCB  "UnityPerDraw" 0

      vs_4_0

     dcl_constantbuffer cb0[4],immediateIndexed

      dcl_inputv0.xyzw

      dcl_inputv1.xy

      dcl_output_sivo0.xyzw, position

      dcl_outputo1.xy

      dcl_temps 1

   0: mul r0.xyzw, v0.yyyy, cb0[1].xyzw

   1: mad r0.xyzw,cb0[0].xyzw,v0.xxxx, r0.xyzw

   2: mad r0.xyzw,cb0[2].xyzw,v0.zzzz, r0.xyzw

   3: mad o0.xyzw,cb0[3].xyzw,v0.wwww, r0.xyzw

   4: mov o1.xy,v1.xyxx

   5: ret

Unity在其球体周围包裹UV坐标。在球体的极点上折叠纹理的顶部和底部。同时你会看到一条从北极到南极的缝将图像的左侧和右侧连接在一起。因此,沿着该接缝,同时将拥有0和1的U坐标值。这是通过沿着接缝具有重复的顶点来完成的,除了U坐标不同它们完全是一样的。

 

UV as colors, head-on and from above.

添加纹理

要添加纹理,您需要导入一个图像文件。 下面这张是我用于测试所用的图片。

Texture for testing.

通过将图像文件拖到项目视图上将图像添加到项目中。 您也可以通过Asset / Import NewAsset...菜单项来完成。 图像将被导入为默认设置的2D纹理,这正是我们所需要的。

 

Imported texture with default settings.

我们需要添加另外一个着色器属性来使用这张纹理。普通纹理的属性类型是2D.当然还有其他一些类型的纹理。纹理的默认值是一个字符串,它的值可以是“white”“black”gray”中的一个。

对主纹理的约定命名为_MainTex,所以我们也将使用这个名字。同时,这个名字可以让你用脚本Material.mainTexture来访问它。

        Properties{

               _Tint("Tint", Color) = (1, 1, 1, 1)

               _MainTex("Texture", 2D) = "white" {}

        }

What are the curly brackets for?

现在我们可以直接将图片拖上去或者通过 Select 按扭为我们的纹理赋值了。

Texture assigned to our material.

同时。我们需要在着色器里声明一个sampler2D类型的同名变量来访问纹理

                       float4_Tint;

                       sampler2D_MainTex;

使用UV坐标在片段着色器中使用tex2D方法对纹理进行采样。

                       float4MyFragmentProgram (Interpolators i) : SV_TARGET {

                               return tex2D(_MainTex, i.uv);

                       }

uniform  sampler2D _MainTex;

in  vec2 vs_TEXCOORD0;

layout(location = 0) out vec4SV_TARGET0;

void main()

{

    SV_TARGET0 =texture(_MainTex, vs_TEXCOORD0.xy);

    return;

}

SetTexture 0[_MainTex] 2D 0

      ps_4_0

     dcl_sampler s0, mode_default

     dcl_resource_texture2d (float,float,float,float)t0

     dcl_input_ps linear v0.xy

      dcl_outputo0.xyzw

   0: sample o0.xyzw,v0.xyxx, t0.xyzw, s0

   1: ret

 Textured sphere.

现在每个片段都会在纹理上采样并显示。 正如预期的那样,但它在两极附近会显得很扭曲。 这又是为什么?

纹理失真是因为插值在三角形上是线性的。Unity的球体在极点附近只有少数几个三角形, 而在顶点时UV值是最扭曲的。UV坐标在两个三角形的顶点之间是非线性地变化,但是在一个三角形的顶点之间的变化是线性的。因此,纹理中的直线在三角形边界上会突然改变方向。

Linear interpolation across triangles.

不同的网格具有不同的UV坐标,产生不同的映射。Unity默认球使用经纬度纹理映射,但它的网格是一个低分辨率立方体球。对于测试它是足够的,但想获得更好的结果最好使用自定义球体网格。

最后我们因为乘上tint变量调整纹理的外观

                               return tex2D(_MainTex, i.uv) * _Tint;

Textured with yellow tint.

Tiling and Offset

在向着色器添加纹理属性之后,材质inspector不只是添加纹理字段。它还添加了Tiling和Offset属性。现在改变这些值还没有效果。

这个额外的数据是存储在材质中的,如果着色器想要访问它们,首先需要声明一个float4类型变量,变量名是纹理名字后加个 _ST 后缀。

                       sampler2D_MainTex;

                       float4_MainTex_ST;

Tiling变量默认值是(1,1),它用于控制纹理的缩放。它保存在这个变量的XY坐标中。你只需要简单地将它乘上UV坐标就可以了。你可以在顶点着色器或者片段着色器中执行这个操作。不过在顶点着色器中处理会更有效率。因为它只对每个顶点进行操作。

                       InterpolatorsMyVertexProgram (VertexData v) {

                               Interpolatorsi;

                               i.position = mul(UNITY_MATRIX_MVP, v.position);

                               i.uv = v.uv * _MainTex_ST.xy;

                               return i;

                       }

Tiling.

offset变量用于控制纹理的移动。它保存在变量的ZW坐标位置。它需要与UV缩放后的值相加。

                               i.uv = v.uv *_MainTex_ST.xy + _MainTex_ST.zw;

Offset.

UnityCG.cginc包含一个宏帮我们提供一个简便的方法处理这个步骤

                               i.uv = TRANSFORM_TEX(v.uv,_MainTex);

纹理设置

目前我们使用的都是纹理导入的默认设置。让我们来看一下纹理的设置选项,看它们分别能做些什么。

Default import settings.

 Wrap Mode 表明当采样的UV值不在[0-1]的范围时如何处理。

Clamped表明我们把值限制在[0-1]的范围内,也就是小于0的值当0处理,大于1的值会被当1处理。

Repead, 这意味着超出范围的值它们的整数部分将会被忽略。

默认模式是Repead,它会使纹理平铺。

如果你不想平铺纹理,你会想用Clamped来代替。这会防止纹理重复绘制,相反,纹理边界将被复制,使其看起来像是拉伸。

Does the wrap mode matter when staying within the 0–1range?

It matters when you have UV coordinates that touch the 0and 1 boundaries. When using bilinear or trilinear filtering, adjacent pixelsare interpolated while sampling the texture. This is fine for pixels in themiddle of the texture. But what are the neighboring pixels of those that lie onthe edge? The answer depends on the wrap mode.

When clamped, pixels on the edge are blended withthemselves. This produces a tiny region where the pixels don't blend, which isnot noticeable.

When repeated, pixels on the edge are blended with theother side of the texture. If the sides are not similar, you'll notice a bit ofthe opposite side bleeding through the edge. Zoom in on a corner of a quad withthe test texture on it to see the difference.

 

Tiling at (2, 2) while clamped.

Mipmaps andFiltering

当纹素(纹理的像素)与它们投射的像素不完全匹配时会发生什么?这个问题以什么方式处理。是由FilterMode控制的。

最直接的过滤模式是 Point (no filter)。这意味着当纹理在一些UV坐标处被采样时,使用最近的纹素。这将使纹理具有块状外观,除非纹素跟显示像素完全匹配。因此,它通常用于像素完美匹配的渲染,或者当需要块状风格时。

默认是使用bilinear filtering(双线性滤波)。当纹理在两个纹素被采样时,会进行插值处理。由于纹理是二维的,这同时发生在U轴和V轴上。因此发生的是双线性滤波而不仅仅是线性滤波。

当纹素小于显示像素密度时,即对纹理进行放大时,结果会看起来模糊不清。而当你对纹理进行缩小操作时,一个显示像素会在纹理上采样超过一个纹素,所以部分纹素会被忽略,这将导致图片过渡太剧烈,好像图像被锐化。

解决这个问题的办法是在纹理密度太高的时候使用较小的纹理。显示器上出现的纹理越小,应该使用的版本越小。这些较小的版本被称为MIPMAP,并为您自动生成。每个连续的MIPMAP具有先前级别的宽度和高度的一半。因此,当原始纹理大小为512x512时,MIP映射为256x256、128x128、64×64、32×32、16x16、8x8、4x4和2x2。

 

Mipmap levels.

如果你喜欢,你可以禁用MIPMAP。首先,您要将纹理类型设置为 Advanced.。然后你就可以禁用MIPMAP并应用更改。想要查看mipmap开启与否的区别是使用一个扁平的物体,类似于quad然后从一个角度看向它

With and without mipmaps.

那么,哪个MIPMAP级别是用在哪里,它们看起来有什么不同呢?通过在高级纹理设置中启用 Fadeout Mip Maps ,我们可以使过渡可见。启用后,将在inspector中显示一个Fade Range滑条。它定义了一个MIPMAP范围,MIPMAP将在该范围内过渡到实灰色。进度条左边的点代表在什么时候开始发生转变。右边的点代表什么时候转变结束

Advanced settings for mipmaps.

What is the use of fading to gray?

为了能够更好地观察这个效果,现在将纹理的Aniso Level 设置为0。

Successive mipmap levels.

一旦你知道他不同的mipmaps levels分别显示在哪里,你应该就能看到在他们中间纹理质量的突然变化。纹理投影越小,纹理密度越高,看起来越锐利。直到下一个MIPMAP级别突然开始,它又变得模糊了。

所以,如果没有使用MIPMAP,图像就从模糊变为锐利,直到变得过于尖锐。如果使用MIPMAP你从模糊到尖锐,突然模糊,再次尖锐,突然模糊。

这些模糊-尖锐带是bilinear filtering(双线性滤波)的特征。您可以通过将过滤器模式转换为Trilinear(三线性)消除它们。这与双线性滤波相同,只是它也在相邻的MIPMAP水平之间进行插值。因此是三线性的。这使得采样更加昂贵,但它平滑了MIPMAP级别之间的转换。

Trilinear filtering between normal and gray mipmaps.

另一种有用的技术是anisotropic filtering(各向异性滤波)。你可能已经注意到当你把它设置为0时,纹理变得模糊了。这与MIPMAP级别的选择有关。

当纹理以某个角度投影时,由于透视,你经常会发现它的一个维度比另一个维度扭曲得多。一个很好的例子是纹理的接地平面。在一定距离内,纹理的向前向后维度将显得更小,即左右维度。

选择哪个mipmap级别是基于最差的维度。如果差异很大,那么你会得到在某一个维度非常模糊的结果。各向异性过滤通过解除尺寸来缓解这种情况。除了均匀缩小纹理外,它还提供在任一维中缩放不同数量的版本。因此,您不仅拥有256x256mipmap,而且还拥有256x128,256x64等等。

Without and with anisotropic filtering.

请注意,这些额外的mipmap不像普通的mipmap那样预先生成。相反,它们是通过执行额外的纹理采样来模拟的。所以他们不需要更多的空间,但是更昂贵的采样工作。

Anisotropic bilinear filtering, transitioning to gray.

Aniso Level控制各向异性过滤的深度。0时,它被禁用。1时,它变为启用并且提供最小的效果。16时,它是最大的。但是,这些设置受project's quality settings的影响。

您可以通过Edit /Project Settings / Quality访问质量设置。您将在渲染部分找到Anisotropic Textures设置。

Rendering quality settings.

当各向异性纹理被禁用时,不管纹理的设置如何,都不会发生各向异性过滤。当它被设置为每个纹理时,它完全由每个纹理单独控制。它也可以设置为Forced On,这会像每个纹理的Aniso Level设置为至少9。但是,Aniso Level设置为0的纹理仍然不会使用各向异性过滤。

 


  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值