GPU发展

GPU英文全称Graphic Processing Unit,中文翻译为“图形处理器”。NVIDIA在1999年发布GeForce 256图形处理芯片时首先提出GPU的概念。GPU所采用的核心技术有硬件T&L、立方纹理(Cube map)和顶点混合、纹理压缩和凹凸映射贴图、双重纹理四像素256位渲染引擎等,而硬件T&L(Transform and Lighting,多边形转换与光源处理)技术可以说是GPU的标志。 2001年第三代modern GPU提供了vertex programmability(顶点编程能力),允许应用程序指定一个序列的进行定点操作控制。所谓vertex,就是组成3D图形的顶点,vertex信息包含了3D模型在空间内的坐标等信息,vertex shader可以通过特定的算法在工作中改变3D模型的外形。到了2003年,GPU开始同时支持vertex programmability 和 fragment programmability(片段编程能力,也成像素编程)。同时DirectX 和 OpenGL也得到了发展。

什么是Shader

Shader,即着色器。是一些在GPU上运行的小程序,可编程图形管线的算法片段,用以告诉图形硬件如何计算和输出图像。 主要分为两类:Vertex Shader 和 Fragment Shader。

渲染管线

渲染管线也称为渲染流水线或像素流水线或像素管线,是显示芯片内部处理图形信号相互独立的的并行处理单元。在某种程度上可以把渲染管线比喻为工厂里面常见的各种生产流水线,工厂里的生产流水线是为了提高产品的生产能力和效率,而渲染管线则是提高显卡的工作能力和效率。

光栅化

光栅化就是把顶点数据转换为片元的过程。片元中的每一个元素对应于帧缓冲区中的一个像素。 光栅化其实是一种将几何图元变为二维图像的过程。该过程包含了两部分的工作。第一部分工作:决定窗口坐标中的哪些整型栅格区域被基本图元占用;第二部分工作:分配一个颜色值和一个深度值到各个区域。光栅化过程产生的是片元。 把物体的数学描述以及与物体相关的颜色信息转换为屏幕上用于对应位置的像素及用于填充像素的颜色,这个过程称为光栅化,这是一个将离散信号转换为模拟信号的过程。

渲染管线: 渲染管线

unity图形处理流程: unity图形处理流程

Shader、材质和贴图

输入的贴图或者颜色,经过Shaer将其与顶点数据以一定方式组合起来,然后输出。绘图单位可以依据这个输出来将图像绘制到屏幕上。输入的贴图或者颜色等,加上对应的shader,以及对shader的特定的参数设置,将这些内容(shader以及输入参数)打包存储在一起,得到的就是一个material(材质)。之后,我们便可以将材质赋予三维物体来进行渲染(输出)了。

GLSL/HLSL/Cg

Shader language 目前主要有3种:GLSL(OpenGL Shder Language, 基于OpenGL), HLSL(High Level Shading Language, 基于DirectX), 还有NVIDIA公司的Cg语言(C for Graphic)。

OpenGL 支持Windows、Linux、MacOS等许多平台,DirectX仅支持Win平台。而Cg是一个可以被OpenGL和Direct3D广泛支持的图形处理器编程语言。而且Cg语言是Microsoft和NVIDIA相互协作在标准硬件光照语言的语法和语义上达成了一致而开发,所以,HLSL和Cg其实是同一种语言。

所以尽量选择Cg比较好。

总结

一般来说,着色器会分为顶点(Vertex)着色器和片段(Fragment)着色器两个部分。

顶点着色器,主要求出各个顶点在投影面的坐标。所以传进去的值需要有顶点的三维坐标,还需要有摄像机的位移旋转矩阵。 片段着色器,主要通过给出的数据,结合从顶点着色器得到的坐标值,计算出每一个像素点应该显示的颜色值。所以传进去的值,会默认包括了顶点程序的坐标,然后我们给予的贴图信息、颜色信息和UV坐标信息。片段着色器是针对于每个像素点的,所以片段着色器的输出,就是该像素点应该显示的颜色值。

于是,我们就可以通过编写顶点着色器程序,通过一定的方式去改变顶点在投影面的坐标,让模型变形;或者通过编写片段着色器程序,让模型表现出来的颜色根据我们的需要而改变。

Shader(着色器)实际上就是一小段程序,它负责将输入的Mesh(网格)以指定的方式和输入的贴图或者颜色等组合作用,然后输出。输入的贴图或者颜色等,加上对应的Shader,以及对Shader的特定的参数设置,将这些内容(Shader及输入参数)打包存储在一起,得到的就是一个Material(材质)。

Shader开发者要做的就是根据输入,进行计算变换,产生输出而已。

Unity Shader形态

Unity 有三种编写shader的方式:

  • surface shaders
  • vertex and fragment shaders
  • fixed function shaders

fixed function shader (固定功能着色器):

对应于固定管线硬件的操作,最简单的着色器类型,只能使用Unity3D自带的固定语法和提供的方法,适用于任何硬件,使用难度最小。    

vertex and fragment shader (顶点片段程序着色器):

顶点和片段着色器,如前所述,是可编程图形管线主要支持的方式。是效果最为丰富的着色器类型,使用Cg/HLSL语言规范,着色器由顶点程序和片段程序组成。所有效果都需要自己编写,使用难度相对较大。

surface shader (表面着色器):

Unity推荐的shader类型。同样使用Cg/HLSL语言规范的着色器类型,不过把光照模型提取出来,可以使用Unity3D自带的一些光照模型,也可以自己编写光照模型,着色器同样由顶点程序和片段程序组成,不过本身有默认的程序方法,使用者可以只针对自己关系的效果部分进行编写。由于选择性比较大,所以可以编写出较为丰富的效果,使用难度相对vertex and fragment shader小。 可以理解其是对Vertex 和 Fragment shader的一种包装。 (surface shader有一个问题,它不支持SubShader内部的多pass,所以某些需要多pass的效果要实现起来会比较困难。) 

Unity建议从ShaderLab语法开始学习shader。Fixed function shader 只能被ShaderLab编写。(但是 vertex and fragment shader 和surface shader 是不限于shaderlab的,可以使用Cg/HLSL/GLSL)。

Shaderlab基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Shader "MyShader" { 
Properties { 
    _MyTexture ("My Texture", 2D) = "white" { } 
    //其他属性
} 
SubShader { 
    // - surface shader or
    // - vertex and program shader or
    // - fixed function shader 
} 
SubShader { 
    // 一个更简单的shader,可以运行在更弱的硬件上
}
Fallback "Legacy Shaders/VertexLit"
[CustomEditor]
}

首先是一些属性定义,属性名是前面带下划线的,显示在编辑器的名字是后面字符串中的。 接下来是一个或者多个的子着色器,只有一个能被执行,哪一个子着色器被使用是由运行的平台所决定的。子着色器是代码的主体,每一个子着色器中包含一个或者多个的Pass。 最后指定一个回滚,用来处理所有Subshader都不能运行的情况(比如目标设备实在太老,所有Subshader中都有其不支持的特性)。 CustomEditor是定制编辑器。

需要提前说明的是,在实际进行 surface shader 的开发时,我们将直接在Subshader这个层次上写代码,系统将把我们的代码编译成若干个合适的Pass。


参考:
Shader编程教程
Unity Manual

https://code.visualstudio.com/docs/editor/codebasics

VS Code 是微软推出的跨平台编辑器。它采用常见的UI布局,左侧是explorer 显示你打开的文件和文件夹,右侧是编辑窗口。 另外还有需要多额外的特性。

Files, Folders & Projects

VS Code 是基于文件和文件夹的,你可以直接打开一个文件或者文件夹。另外,VSCode可以读取许多工具和平台定义的项目文件。如果你打开的文件夹有这些项目文件比如 project.json 或是 Visual Studio 的项目文件,它都会自动读取它们并提供更多功能,比如智能提示等。

基本布局

VS Code 的 UI 被划分为四个区域:

  • Editor 是你编辑文件的地方,可以最多并排打开三个窗口。
  • Side Bar 包含不同的视图窗口,比如Explorer。
  • Status Bar 指示当前打开的文件和项目的信息。
  • View Bar 在最左侧,让你切换不同视图,并有额外的上下文提示。

注:你可以把Side Bar移到右边(View->Move Sidebar),或者用Ctrl+B切换显示关闭。

并排编辑

你可以同时打开三个editor进行编辑。有多种方式打开一个新的editor:

  • Ctrl并点击Explorer中的文件。
  • Ctrl+\ 把当前editor分为两个。
  • Open to the Side 来自Explorer窗口的命令。

当你有多个editor打开,可以使用Ctrl加数字1,2,3来切换不同窗口。

Explorer

用来浏览、打开和管理项目所有文件和文件夹。 一旦打开一个文件夹,其内容就会显示在Explorer中。你可以对它们进行许多操作:

  • 创建、删除、重命名文件和文件夹。
  • 通过拖拽移动文件和文件夹。
  • 用右键菜单查看更多操作。

你可把外部文件拖到VSCode来复制它们。 默认情况下,VSCode会排除一些文件夹(比如.git)。可以使用 files.exclude配置规则。 这对于排除自动生成的文件很有用,比如Unity自动生成的 *.meta文件。

注:使用Ctrl+P可以快速搜索并打开文件。

Working Files

在Explorer 顶部有一个 Working Files 标签。这是激活文件的列表。一些情况下,文件会出现在这个列表:

  • 修改一个文件。
  • 双击打开一个文件。
  • 打开一个不是当前文件夹的文件。

重新点击文件就会再次激活它们,不需要时关闭就即可。

注:你可在设置中配置 Working Files 的样式,比如explorer.workingFiles.maxVisible设置列表最大数量,explorer.workingFiles.dynamicHeight 设置是否自动设置高度。

Save/Auto Save

默认情况,VSCode需要你手动保存文件,使用Ctrl+S。 当然,你也可以使用自动保存。 在命令板可以设置,按F1,然后输入auto设置。也可以在File菜单选项设置。

搜索文件

使用Ctrl+Shift+F可以快速搜索当前打开文件。 同时支持正则表达式搜索。

也可以使用Ctrl+Shift+J开启高级搜索。

通过Search下面两个输入栏,你可以指示包括或排除哪些文件。 点击左侧按钮,可以开启glob pattern syntax:

  • * 匹配一个或多个字符
  • ? 匹配一个字符
  • ** 匹配任何数量路径片段,包括空
  • {} 群组条件 (比如 {*/.html,*/.txt},搜索所有html和txt文件)
  • [] 指示字符范围(比如 example.[0-9] 匹配 example.0, example.1, …)

Command Palette

F1可以打开命令面板。你可以获取所有VSCode的功能。 这个面板UI可以实现许多功能,下面是另外一些有用的快捷键:

  • Ctrl+P 输入文件名打开文件
  • Ctrl+Tab 循环之前打开的文件
  • F1
  • Ctrl+Shift+O 跳转到特定符合。
  • Ctrl+G 跳转到特定行号。

输入?查看可以使用的指令。

快速文件导航

按住Ctrl 再按Tab 会显示一个所有打开过的文件列表,使用Tab切换文件,当选到某个要打开的文件,松开Ctrl就可以打开它。 另外,也可以使用Alt+Left 和 Alt+Right切换文件和编辑位置。

文件编码支持

在 User Settings 或 Workspace Settings 的 files.encoding,可以分别设置全局和当前工作空间的编码格式。 在状态栏可以查看当前编码格式,点击可以重新保存为新的格式。

命令行启动

可以使用命令行启动VSCode(前提是已经加入PATH),只需输入:

1
Code .

也可以打开或者创建文件,使用空格分隔任意多个文件,文件存在会打开,不存在会创建新文件:

1
code index.html style.css readme.md

设置

有两种设置:User和Workspace User是全局的,Workspace只保存在当前工作空间,会重写User设置。

编辑器进阶

  • Ctrl+Shift+] 跳转到匹配的括号。
  • 选择 和 多光标: Alt+Click 可以多处选择,Ctrl+Alt+Down 或 Ctrl+Alt+Up可以插入多个光标。 Ctrl+D 选择光标指的整个单词,Ctrl+K Ctrl+D 将最后添加的光标移向当前选择内容下一次出现的位置。 Ctrl+Shift+L 会选择所有当前选择的内容,还有Ctrl+F2在各个内容切换。
  • Shift+Alt+Left 和 Shift+Alt+Right,收缩和扩大选择范围。
  • F12转到定义,Ctrl并停到一个位置,会显示预览。
  • 在 C# 和 TypeScript中,可以使用Ctrl + T 跳转到任意符号。
  • F2重命名。
  • Ctrl+Shift+M显示所有错误。

连接Unity 和 VS Code

最简单的方法是利用Unity plug-in。

  1. 下载插件

    git clone https://github.com/dotBunny/VSCode.git

  2. 把plug-in添加进你的Unity项目 把刚下载的文件添加进Unity。然后打开Unity Preferences,选择VSCode窗口:

    勾选Enable Integration,就会开启功能。 点击Write Workspace Settings,会进行相关配置,比如在VSCode排除无关文件(.meta)等。

  3. 打开项目

现在在Assets菜单,可以看到Open C# Project In Code选项。这会在VSCode打开整个项目根目录。

调试Unity

目前VSCode自带调试只支持通过 Mono,所以只能在Mac OS X平台使用。 不过已经有网友提供了方法:http://forum.unity3d.com/threads/vs-code-unity-debugger-extension-preview.369775/

  • 首先下载:unity-debug-0.5.0.zip
  • 然后解压,复制unity-debug文件夹到 %USERPROFILE%.vscode\extensions(如果extensions不存在就新建一个)
  • 重新打开VSCode,打开你的Unity项目。
  • 选择Debug视图,并点击齿轮设置按钮,在弹出的菜单选择Unity Debugger。
  • 如果没有这个选项,说明你没有把上面的文件夹放到正确的位置,或者你的项目中已经有了.vscode/Launch.json文件,你需要先删除它。
  • 现在你的Unity项目中会有一个.vscode/Launch.json文件。
  • 现在你可以在VSCode的C#文件中使用断点进行调试。在VSCode点击绿色箭头开启调试,然后再Unity中点击Play,就可以进行调试。

好吧,这几个概念相信大家都很迷糊,反正我是一只很迷糊。。。 有空的可以看看这个知乎问题:怎样理解阻塞非阻塞与同步异步的区别?

同步与异步:

  • 同步:
    所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。

  • 异步:
    异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。 同步和异步关注的是消息通信机制。同步是调用者主动等待调用的结果。异步则是调用者调用后,被调用者主动通知调用者结果。

同步异步更多表示一种协作方式。 同步IO中,对同一个描述符的操作必须是有序的。甲发送一次请求,乙接收了请求并返回结果,然后甲才可以进行下一次请求。 异步IO中,异步IO可以允许多方同时对同一个描述符发送IO请求,或者一次发多个请求,当然有机制保证如何区分这些请求。甲方有请求就发送,乙方也全都接受,乙方有了结果就返回,有时候可能更甲收到的结果顺序与请求的顺序并不同。

引用一个例子: 你打电话给书店要找一本书。如果是同步,老板会让你等着,然后去找书,过段时间找到了(或者没找到),再拿起电话告诉你结果。而异步的话,老板会说”知道了,等我找找,到时候给你电话“,然后就把你电话挂了。过了一会,老板又给你打过来电话,告诉你结果。

阻塞与非阻塞:

首先要明确一个很重要的地方,在处理IO(不管是Soket还是文件之类的)的时候,阻塞和非阻塞都是同步IO。

  • 阻塞:
    阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。 要注意,不要把阻塞调用和同步调用等同起来,实际上它们是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。例如,我们在CSocket中调用Receive函数,如果缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。Socket接收数据的另外一个函数recv则是一个阻塞调用的例子。当socket工作在阻塞模式的时候, 如果没有数据的情况下调用该函数,则当前线程就会被挂起,直到有数据为止。

  • 非阻塞:
    非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。只是应用在请求的读取和发送。阻塞其实就是等待,发出通知,等待结果完成。非阻塞属于发出通知,立即返回结果,没有等待过程。

还是上面例子: 你打电话问书店老板有没有一本书。如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。

数据流概念

通过网络传输数据,或者对文件数据进行操作时,都需要先将数据转换为数据流。数据流(Stream)是对串行传输数据的一种抽象表示。 典型的数据流和某个外部数据源相关,数据源可以是文件、外部设备、内存、网络套接字等。根据数据源不同,.NET提供多个从Stream类派生的子类来对不同的数据源提供支持,每个类都代表了一种具体的数据流类型。例如和磁盘文件相关的文件流FileStream,和Socket相关的NetworkStream,和内存相关的MemoryStream。

流提供3种基本操作:
写入:将数据从内存缓冲区传输到外部源。
读取:将数据从外部源传输到内存缓冲区。
查找:重新设置流的当前位置,以便随机读写。需要注意的是,并不是所有的流类型都能够支持查找,如网络流没有当前位置的概念,因此不支持查找功能。

Stream类及其派生类都提供了Read和Write方法,可支持在字节级别上对数据进行读写。Read方法从当前流读取字节序列,Write方法向当前流中写入字节序列。但是仅支持字节级别的数据处理会给开发人员带来不便。因此,除了Stream及其派生类的读写方法之外,.NET框架同样提供了其他多种支持流读写的类。

BinaryReaderBinaryWriterStreamReaderStreamWriter.

文件流 FileStream

FileStream类的实例代表一个文件流。使用FileStream类可以对文件系统上的文件进行读取、写入、打开和关闭操作。 使用文件流首先要获取实例,有许多方法可以实现。 读取文件,可以使用FileStream的Read方法,也可以创建BinaryReader或StreamReader来读取。要注意,有时磁盘文件很大,如果直接将文件所有数据读入内存是很危险的,所以对文件读取时,一般创建一个较小字节的字节数组作为缓冲区,分块循环读取。 写入文件,可以使用FileStream的Write方法,也可以创建BinaryWriter或StreamWriter来读取。

MSDN FileStream 类。

内存流 MemoryStream

和文件流不同,MemoryStream类表示的是保存在内存中的数据流,由内存流封装的数据可以在内存中直接访问。内存流一般用于暂时缓存数据,以降低应用程序中对临时缓冲区和临时文件的需要。

既然字节数组也在内存中存储,为什么还要引人内存流的概念?这是因为内存流相对于字节数组而言,具有流特有的特性,并且容量可自动增长。在数据加密以及对长度不定的数据进行缓存等场合,使用内存流比较方便。

Memorystream类的构造函数有多种重载形式,常用的有以下3种:

  1. MemoryStream( )。该构造函数初始分配的容量大小为0,随着数据的不断写人,其容量可以不断地自动扩展。一般用于数据内容及大小都不确定的场合。
  2. MemoryStream(Byte[])。 通过该构造函数获取的MemoryStream实例根据Byte类型的字节数组进行初始化,并且实例的容量大小固定为字数组的长度。由于实例的容量不能扩展,该构造函数一般用于数据不发生变化的场合。
  3. MemoryStream(int capacity)。初始容量大小为capacity,并且可以扩充容量。

Memorystream的Read和Write与文件流类似。不再赘述。 Memorystream支持对数据流的查找和随机访问。

MSDN Memorystream类。

网络流 NetworkStream

在网络上传输数据时,使用的是网络流(NetworkStream)。网络流的意思是数据在网络的各个位置之间是以连续的字节形式传输的。为了处理这种流,C#在System.Net.Sockets命名空间中提供了一个NetworkStream类,用于发送和接收网络数据。 可以将NetworkStream看作在数据来源端和接收端之间架设了一个数据通道,这样一来,读取和写入数据就可以针对这个通道来进行。需要注意的是,NetworkStream类仅支持面向连接的套接字。

对于NetworkStream流,写人操作是指从来源端内存缓冲区到网络上的数据传输;读取操作是从网络上到接收端内存缓冲区(如字节数组)的数据传输。

一旦构造了一个NetworkStream对象,就可以使用它进行网络数据发送和接收。

MSDN NetworkStream 类。

对于需要网络功能的程序,除非我们准备定义一些新的协议或者对细节进行更灵活的控制,否则的话,一般情况下不需要直接使用Socket类,而是使用进一步封装后的TcpListener类、TcpClient类以及UdpClient类来实现。这主要是因为使用socket编写程序比较麻烦,而且容易出错。但是,由于封装后的类不可避免地要涉及套接字的概念,因此我们还需要了解Socket的基本用法。

Socket 概念

Socket中文翻译是套接字,是支持TCP/IP网络通信的基本操作单元。它是应用层与TCP/IP协议族通信的中间软件抽象层,是一组接口。它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

可以将套接字看作不同主机间的进程进行双向通信的端点,在一个双方可以通信的套接字实例中,既保存了本机的IP地址和端口,也保存了对方的IP地址和端口,同时也保存了双方通信采用的协议等信息。

套接字有3种不同的类型:流套接字、数据报套接字和原始套接字。 流套接字用来实现而向连接的TCP通信,数据报套接字实现无连接的UDP通信,原始套接字实现IP数据包通信。3种类型的套接字均可以使用system.NetSockets命名空间中的Socket类来实现。

Socket通信流程

面向连接的套接字

我们知道TCP和UDP协议,它们分别是面向连接和无连接的。在面向连接的Socket中,使用TCP来建立两个地址端点的会话。一旦建立这种连接,就可以在设备之间进行可靠的数据传输。 就好像打电话,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。

TCP Socket连接的过程可以简单的分为:1. 服务端监听 2. 客户端请求 3.建立连接。

服务器创建socket。 服务器为socket绑定ip地址和端口号。 服务器socket监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket并没有被打开。

客户端创建socket。 客户端打开socket,根据服务器ip地址和端口号试图连接服务器socket。

服务器socket接收到客户端socket请求,被动打开,开始接收客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端谅解请求。

客户端连接成功,向服务器发送连接状态信息。

服务器accept方法返回,连接成功。 客户端向socket写入信息。 服务器读取信息。

客户端关闭。 服务器端关闭。

无连接的套接字

UDP使用无连接的套接字,无连接的套接字不需要在网络设备之间发送连接信息。因此,很难确定谁是服务器谁是客户端。如果一个设备最初是在等待远程设备的信息,则套接字就必须用Bind方法绑定到一个本地”IP地址/端口”上。完成绑定之后,该设备就可以利用套接字接收数据了。由于发送设备没有建立到接收设备地址的连接,所以收发数据均不需要Connect方法。

由于不存在固定的连接,所以可以直接使用SendTo()方法和ReceiveFrom()方法发送和接受数据。两个主机之间通信结束后,可以像TCP一样,使用Shutdown和Close方法。

必须使用Bind方法绑定本地IP和端口后,才可以使用ReceiveFrom()方法接收数据,如果只发送不接收,就不用绑定。

关于Socket更多内容和示例请看 MSDN Socket 文档。