一篇搞定Unity Shader入门精要 (更新中) - 知乎 (zhihu.com)
渲染流水线
每一个阶段做了些什么,输入和输出是什么,改变了什么
更详细的解释: GPU Rendering Pipeline——GPU渲染流水线简介 - 知乎 (zhihu.com)
一篇文章搞懂到底什么是渲染流水线 - iwiniwin - 博客园 (cnblogs.com)
常用的渲染管线划分方法是分为三个阶段:应用阶段—(输出渲染图元)—几何阶段—(输出屏幕空间的顶点信息)—光栅化阶段
其中光栅化阶段包括逐片元操作。
每个阶段依据上个阶段的数据进行操作,并为下个阶段准备数据。
应用阶段一般在 CPU 上完成。首先从硬盘或内存中读取模型或纹理数据,加载到应用程序中,然后进行粗粒度的剔除,设置基本的渲染参数和状态,最后进行 Draw Call。
几何阶段到逐片元操作一般在 GPU 上完成,这是因为 GPU 的并行性比较好。例如模型不同顶点的光照或着色运算模式相同,就可以放到 GPU 的不同工作单元上并行执行
应用阶段
应用阶段首先准备场景数据,包括物体、摄像机、光源等。然后预先进行一些优化,例如算法加速和遮挡剔除等。接着设置渲染状态,即设置渲染的方式,包括渲染顺序、目标(FrameBuffer 或 RenderTexture)和模式(前向渲染或延迟渲染)。最后调用 Draw Call,把图元输出到 GPU 处理。
基本场景数据,场景物体数据:物体的变换信息(位置、旋转和缩放)和网格数据(顶点位置、法线位置等)
光源信息:光源类型、位置、方向和角度等。
摄像机信息:摄像机位置、方向、裁剪平面、FOV 和视口尺寸等。
光源和阴影,设置光源:方向光、点光源和聚光源对应的不同参数。
设置阴影:物体是否产生或接受阴影以及阴影的参数。
逐光源绘制阴影贴图:阴影算法不唯一。
加速算法和粗粒度剔除(culing):把不可见的物体剔除
可见光裁剪和可见场景物体裁剪
剔除影响范围不在视锥体内的光源、不在视锥体内的物体和被其他物体完全遮挡的物体。
相关算法:八叉树、BSP 树、KD 树、BVH 包围盒等。
渲染设置(Material, Texture, Shading, Shader)
一文看懂材质/纹理 Material, Texture, Shading, Shader 的区别 - 知乎 (zhihu.com)
绘制设置 使用哪个着色器渲染、是否使用合批(GPU Instance 或动态批处理)。
绘制顺序 可以按照不同方式进行渲染排序,例如依据与摄像机的距离、材质的 RenderQueue、UGUI 的 UICanvas 的相关属性。
渲染目标 渲染到 FrameBuffer、RenderTexture 或是多个目标。
渲染模式 前向渲染或延迟渲染。
输出到显存 输出顶点数据、纹理数据、变换矩阵等到显存。
几何阶段
几何阶段首先进行视图变换和顶点着色,然后进行曲面细分和几何着色,进行投影,接着进行裁剪(视锥体裁剪、正面或背面剔除)、最后进行屏幕映射。
顶点着色器
视图变换
模型空间——(M 模型变换)——世界空间——(V 视图变换)——观察空间——(P 投影变换)——裁剪空间
顶点着色:计算顶点光照。
曲面细分:根据已有顶点生成更多顶点,将现有网格细分。
几何着色器:根据给定的图元生成更多的图元。例如 Unity 粒子系统依据单个顶点生成一个四边形。
裁剪
剔除在标准设备坐标系之外的顶点。如果一个图元因此被分割,那么会生成额外顶点来补齐。
OpenGL 的标准设备坐标系:x、y、z 的范围是(-1,1)。
DirectX 的标准设备坐标系:x、y 的范围是(-1,1),z 的范围是(0,1)。
投影
进行透视除法(把 x、y、z 除以 w)。
正交模式的 w 始终为 1,透视模式的 w 在近处较小、远处较大。
投影坐标系——(投影)——标准设备坐标系(NDC)。
屏幕映射
标准设备坐标系——(屏幕映射)——屏幕坐标系
OpenGL 的屏幕坐标系原点:左下方。
DirectX 的屏幕坐标系原点:左上方。
光栅化阶段
光栅化阶段分为三角形设置和三角形遍历。
三角形设置:根据顶点在屏幕坐标系中的位置计算三角形的边界信息。
三角形遍历
根据三角形的边界信息遍历被三角形覆盖的像素。对每个被覆盖的像素进行线性插值,得到对应的片元数据。
注意:片元数据不等同于最终的像素,因为同一像素可能被多个三角形覆盖,最终由后续步骤决定保留哪些片元、如何混合片元。
抗锯齿
SSAA(超采样抗锯齿,Super Sampling Anti-Aliasing):将渲染的分辨率放大,渲染到放大n倍的buffer上,然后对buffer进行下采样。
MSAA(多重采样抗锯齿,MultiSampling Anti-Aliasing):对每个像素设置多个采样点,对每个子采样点进行覆盖测试(判断子采样点是否在三角形内部)和遮挡测试(将子采样点的深度和深度缓存进行比较)。通过测试的子采样点的覆盖信息被用于后续逐片元操作的着色混合。
FXAA(快速近似抗锯齿,Fast Approximate Anti-Aliasing)/TXAA(随机采样抗锯齿,Temporal Anti-Aliasing):不在当前阶段。
逐片元操作
逐片元操作是对被三角形覆盖的片元序列进行逐个操作。首先进行片元着色,然后进行颜色混合(同一像素包含不同片元时的处理手段,例如透明度测试、深度测试、模板测试、混合),最后输出到目标缓冲区。24671/answer/1121443495
片元着色器(Fragment Shader)
依据插值得到的片元数据计算最终的片元颜色。
颜色混合
透明度测试:舍弃透明度小于给定阈值的片元。
深度测试:将片元的深度值与当前深度缓冲进行比较。
模板测试:对片元的模板值进行测试。
混合:对同一像素中的不同片元进行混合。
CPU和GPU之间的通信(几何阶段和光栅化阶段)
Render - 浅谈 CPU 与 GPU 如何分工? - 知乎 (zhihu.com)
中央处理器(CPU)进行准备工作,比如哪些对象要被绘制,需要如何被绘制。
CPU向图形处理器(GPU)发送指示。
GPU根据CPU的指示进行图形的绘制。
把数据从硬盘到RAM再到显存(GPU),顶点,法线,三角形,
然后设置渲染状态,顶点着色器/片元着色器,光源属性,材质等
最后调整draw call ,CPU告诉GPU渲染哪里,显示正确的像素
GPU流水线
绿色表示完全可编程控制,黄色表示可配置,蓝色表示由GPU固定实现,不可修改。实线表示必须由开发者编程实现,虚线表示该Shader是可选的。
(顺便提一下,曲面细分着色器可用于细分图元,例如将三角面细分成更小的三角面来添加几何细节。几何着色器可决定输出的图元类型和个数,当输出的图元减少时,实际上起到了裁剪的作用,当输出的图元增多或类型改变时,起到了产生或改变图元的作用)
顶点着色器
顶点着色器的处理单位是顶点,输入进来的每个顶点都会调用一次顶点着色器。顶点着色器本身不可以创建或者销毁任何顶点,而且无法得到顶点和顶点之间的关系,例如我们无法得知两个顶点是否属于同一个三角网格。但正因为这样的相互独立性,GPU可以利用本身的特性并行化处理每一个顶点,这意味着这一阶段的处理速度会很快。
顶点着色器完成的工作主要有:坐标变换和逐顶点光照。
顶点着色器必须进行顶点的坐标变换,需要时还可以计算和输出顶点的颜色。例如我们可能需要进行逐顶点的光照。
坐标变换,就是对顶点的坐标进行某种变换。顶点着色器可以在这一步中改变顶点的位置,这在顶点动画中是非常有用的。无论我们在顶点着色器中怎样改变顶点的位置,一个基本的顶点着色器必须要完成的一个工作是,把顶点坐标从模型空间转换到齐次裁剪空间。(四元数和欧拉角和线性代数)
把顶点坐标转换到齐次裁剪空间后,接着通常再由硬件做透视除法,最终得到归一化的设备坐标(NDC)。
裁剪
裁剪阶段的目的是将那些不在摄像机视野内的顶点裁减掉,并剔除某些三角图元的面片(面片通常是由一个一个更小的图元来构成的)。
一个图元和摄像机视野的关系有3种:完全在视野内,部分在视野内,完全在视野外。完全在视野内的图元就继续传递给下一个流水线阶段,完全在视野外的图元不会继续向下传递,因为它们不需要被渲染。而那些部分在视野内的图元需要被裁剪。例如,一条线段的顶点在视野内,而另一个顶点不在视野内,那么在视野外部的顶点应该使用一个新的顶点来代替,这个新的顶点位于这条线段和视野边界的交点处。
屏幕映射
这一步输入的坐标仍然是三维坐标系下的坐标(范围在单位立方体内)。屏幕映射的任务是把每个图元的x和y坐标转换到屏幕坐标系下,这实际上是一个缩放的过程。屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。
屏幕映射得到的屏幕坐标决定了这个顶点对应屏幕上哪个像素以及距离这个像素有多远。
屏幕映射不会对输入的z坐标做任何处理。实际上,屏幕坐标系和z坐标一起构成了窗口坐标系。这些值会被一起传递到光栅化阶段。
三角形设置
这个阶段会计算光栅化一个三角网格所需的信息。具体来说,上一个阶段输出的都是三角网格的顶点,但如果要得到整个三角网格对像素的覆盖情况,我们就必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式。这样一个计算三角网格表示数据的过程就叫做三角形设置。它的输出是为了给下一个阶段做准备。
三角形遍历
三角形遍历阶段将会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话,就会生成一个片元。而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换。
三角形遍历阶段会根据上一个阶段的计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值。像素和片元是一一对应的,每个像素都会生成一个片元,片元中的状态记录了对应像素的信息,是对三个顶点的信息进行插值得到的。
这一步的输出就是得到一个片元序列。需要注意的是一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了但不限于它的屏幕坐标,深度信息,以及其他从几何阶段输出的顶点信息,例如法线,纹理坐标等。
片元着色器(信号的处理,可以使用傅里叶变换优化时间复杂度)
片元着色器用于实现逐片元的着色操作,输出是一个或者多个颜色值(即计算该片元对应像素的颜色,但不是最终颜色)。这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。为了在片元着色器中进行纹理采样,我们通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。
根据上一步插值后的片元信息,片元着色器计算该片元的输出颜色
虽然片元着色器可以完成很多重要效果,但它的局限在于,它仅可以影响单个片元。也就是说,当执行片元着色器时,它不可以将自己的任何结果直接发送给它的邻居们。当然导数信息例外。
逐片元操作Merger
逐片元操作阶段负责执行很多重要的操作,例如修改颜色,深度缓冲,进行混合等。
这一阶段有几个主要任务
决定每个片元的可见性。这涉及了很多测试工作,例如深度测试,模板测试等。
如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。
一个片元,只有通过了所有的测试后,才能和颜色缓冲区中已经存在的像素颜色进行混合,最后再写入颜色缓冲区。
模板测试
模板测试,可以作为一种丢弃片元的辅助方法,与之相关的是模板缓冲。如果开启了模板测试,GPU会首先读取(使用读取掩码)模板缓冲区中该片元位置的模板值,然后将该值和读取到(使用读取掩码)的参考值进行比较,这个比较函数可以是由开发者指定的,例如小于时舍弃该片元,或者大于等于时舍弃。如果这个片元没有通过这个测试,该片元就会被舍弃。不管一个片元有没有通过模板测试,我们都可以根据模板测试和下面的深度测试结果来修改模板缓冲区,这个修改操作也是由开发者指定的。开发者可以设置不同结果下的修改操作,例如,在失败时模板缓冲区保持不变,通过时将模板缓冲区中对应位置的值加1等。模板测试通常用于限制渲染的区域。另外模板测试还有一些更高级的用法,如渲染阴影,轮廓渲染等。
深度测试
如果开启了深度测试,GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较。这个比较函数也是由开发者设置的。通常如果这个片元的深度值大于等于当前深度缓冲区中的值,那么就会舍弃它。因为我们总想只显示出离摄像机最近的物体,而那些被其他物体遮挡的就不需要出现在屏幕上。如果这个片元没有通过这个测试,该片元就会被舍弃。和模板测试不同的是,如果一个片元没有通过深度测试,它就没有权利更改深度缓冲区中的值。而如果它通过了测试,开发者还可以指定是否要用这个片元的深度值覆盖掉原有的深度值,这是通过开启/关闭深度写入来做到的。
混合
为什么需要混合?渲染过程是一个物体接着一个物体画到屏幕上的。而每个像素的颜色信息被存储在一个名为颜色缓冲的地方。因此,当我们执行这次渲染时,颜色缓冲中往往已经有了上次渲染之后的颜色结果,那么我们是使用这次渲染得到的颜色完全覆盖掉之前的结果,还是进行其他处理?这就是混合需要解决的问题。
对于不透明物体,开发者可以关闭混合操作。但对于不透明物体,我们就需要使用混合操作来让这个物体看起来是透明的。
使用混合函数来进行混合操作。混合函数通常和透明通道息息相关,例如根据透明通道的值进行相加,相减,相乘等。
需要注意的是,上面给出的测试顺序并不是唯一的,对于大多数GPU来说,它们会尽可能在执行片元着色器之前就进行这些测试。但是,如果将这些测试提前的话,其检验结果可能会与片元着色器中的一些操作冲突。例如,如果我们在片元着色器进行了透明度测试,而这个片元没有通过透明度测试,我们会通过调用API来手动将其舍弃掉。这就导致GPU无法提前执行各种操作。因此现代的GPU会判断片元着色器中的操作是否和提前测试发生冲突,如果有冲突,就会禁用提前测试。但是,这样也会造成性能上的下降,因为有更多片元需要被处理了。这也是透明度测试会导致性能下降的原因。
Unity Shader的结构
######
void Initialization(){
//从硬盘上加载顶点着色器的代码
string vertexShaderCode=LoadShaderFromFile(VertexShader.shader);
//从硬盘上加载片元着色器代码
string fragmentShaderCode= LoadShaderFromFile(FragmentShader.shader);
//把顶点着色器加载到GPU中
LoadVertexShaderFromString(vertexShader.Code);
//把片元着色器加载到GPU中
LoadVertexShaderFromString(fragmentShader.Code);
//设置名为"vertexPosition"的属性的输入,即为模型顶点坐标
SetVertexShaderProperty("vertexPosition",vertices);
//设置名为"MainTex"的属性输入,someTexture是某张已加载的纹理
SetVertexShaderProperty("MainTex",someTexture);
//设置名为'MVP'的属性输入,MVP是计算好的变换矩阵
SetVertexShaderProperty("MVP",MVP);
//关闭混合测试
Disable(Blend);
//设置深度测试
Enable(ZTest);
SetTestFuncition(LessOrEqual);
//其他设置
}
//每一帧进行渲染
void OnRendering(){
//调用渲染命令
DrawCall();
//当设计多种渲染指令,我们还需要在这里改变各种渲染设置
}
######
VertexShader.shader:
{
//输入:顶点位置,纹理,MVP变换矩阵
in float3 vertexPosition;
in sampler2D MainTex;
in Matrix4x4 MVP;
//输出:顶点经过MVP变化的位置
out float position;
void main(){
//使用MVP对模型的顶点坐标进行变化
position=MVP*vertexPosition;
}
}
######
FragmentShader.shader:
{
//输入:VertexShader输出的position劲过光栅化系统插值后的片元对应的position
in float4 position ;
//输出片元的颜色
out float4 fragColor;
void main(){
//将片元设成白色
fragColor=float4(1.0,1.0,1.0,1.0)
}
}
######
shader要用在材质上 Material
为 Unity 编写着色器时,使用以下语言:
一种称为 HLSL 的编程语言。使用它可编写着色器程序本身。
一种称为 ShaderLab 的 Unity 特定语言。使用它可定义 Shader 对象,它充当着色器程序的容器。类似一个框架
Unlit Shader
会产生一个不包含光照,但是包含雾效的基础顶点/片元着色器。
2、Image Effect Shader
实现各种屏幕后处理效果提供了一个基础模板,可在此模板上进行修改。
3、Compute Shader
会产生一种特殊的Shader文件,一般在利用GPU的并行性来进行一些与常规的渲染流水线无关的计算。一般在做渲染的时候,不会创建这类文件,当然,具情况而定!
4、Standard Surface Shader
有典型的表面着色器的实现方法。
Properties 让属性出现在材质的面版上
对于复杂的2D,3D,Cube赋值是复杂的要么为空要么通过TexGen,CubeReflect,TexGenCubeNormal等选项来控制固定管线的纹理坐标生成
材质和Unityshader的桥梁 Properties(property属性)
Shader "Custom/MyShader"
{
Properties //属性面板
{
//一般格式为下
//在shader访问的名称 显示名称,类型 初始默认值
// Name ("display name" PropertyType)=DefaultVale
_Int("数字",Int) = 2
_Range("range",Range(0.0,5.0))=3.0
_Color ("Color", Color) = (1,1,1,1)
_2D("2D",2D)="white"{}
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
FallBack "Diffuse"
}
核心 SubShader
为了适配不同的显卡,可以有多个,但必须有一个,Unity添加shader时会选中第一个能在目标平台运行的subshader,都不支持会使用Fallback语意指定的Unity Shader。
定义:
SubShader{
//可选的 标签
[Tags]
//可选的 状态
[RenderSetup]
Pass{
//定义了一次完整的渲染流程
//true Shader code will be write here
//Surface Shder 表面着色器
//Vertex/Fragment Shader 顶点/片元着色器
//Fixed Function Shader 固定函数着色器
}
//Other Passes
}
状态设置
可以设置显卡的各种状态
如果在SubShader块中设置了状态,就会应用到全部的Pass。可以在Pass中单独设置状态。
subShader的标签
Tags是一个键值对,都是string类型,用于告诉引擎何时渲染
Tags {"Name1"="Value1" ... ... }
MVP矩阵
//TDO
unity Shader 的学习
最简单的顶点/片元着色器
顶点/片元着色器的基本结构
Shader "MyShaderName" //包含有shader的类型
{
Properties
{
//属性
}
SubShader
{
//针对显卡A的SubShader
Pass
{
//设置渲染状态和标签
//开始的CG代码片段
CGPROGRAM
//改代码的编译指令
#pragma vertex vert //顶点着色器
#pragma fragment frag //片元着色器
//Cg代码的编写
ENDCG
//其他
}
}
SubShader
{
//针对显卡B的SubShader
}
//上述SubShader都失败后用于回调的Unity Shader
Fallback"VertexLit"
}
简单的shader样例
Shader "SimpleShader/Chapter5-SimpleShader"
{
SubShader
{
pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//这里的POSITION是语法是不可省略的,该顶点在剪裁空间的位置
//定义了输入和输出
float4 vert(float4 v:POSITION):SV_POSITION
{
//MVP :模型,观察,投影矩阵
return UnityObjectToClipPos(v);
}
//SV_Target语义进行限定,告诉渲染器把用户输出的颜色储存到一个渲染目标,这里输出到默认的缓存帧中
fixed4 frag():SV_Target
{
return fixed4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
}
Object2World 模型空间转世界空间
Unity5.5版本中_Object2World已经变成unity_ObjectToWorld,_World2Object也变成了unity_WorldToObject。
但由于Unity的向下兼容性,Unity会自动改写它们,不会出错。如下是自动改后的提示,出现在代码最上方。
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
2.UNITY_MATRIX_MVP 把顶点从模型空间转换到裁剪空间用到的矩阵
还有在顶点着色器中,我们往往第一行就会用到UNITY_MATRIX_MVP:mul(UNITY_MATRIX_MVP, v.vertex); 这是把顶点从模型空间转换到裁剪空间,不用我们手动变换空间了,不过这在unity5.6中已经改为:UnityObjectToClipPos(v.vertex); 在UnityShaderUtilities.cginc里,注意5.6以上版本才有这个文件。官方实现如下:
// Tranforms position from object to homogenous space
inline float4 UnityObjectToClipPos(in float3 pos)
{
// More efficient than computing M*VP matrix product
return mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0)));
}
模型数据从哪里来
在顶点着色器中我们使用POSITION来得到模型的顶点位置
现在我们像得到模型上每个顶点的纹理坐标和法线方向,我们需要为顶点着色器定义一个新的参数,修改后
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "SimpleShader/Chapter5-SimpleShader"
{
SubShader
{
pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//使用一个结构体来定义顶点着色器的输入
struct a2v
{
//POSITION语义告诉Unity用模型空间的顶点坐标填充vertex变量
float4 vertex:POSITION;
//NORMAL语义告诉Unity用模型空间的法线方向填充normal变量
float3 normal:NORMAL;
//TEXCOORD0语义告诉Unity用模型空间的的第一套纹理坐标填充变量
float4 texcoord:TEXCOORD0;
};
float4 vert(a2v v):SV_POSITION
{
return UnityObjectToClipPos(v.vertex);
}
fixed4 frag():SV_Target
{
return fixed4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
}
此外unity还有语义 TANGENT,COLOR等
a2v的含义:应用:application 顶点着色器(vertex shader)
a2v表示数据从应用阶段传到顶点着色器阶段
填充到语义的数据是从 Mesh Render组件提供的,每帧调用Draw call时,Mesh Render组件把它负责渲染的模型数据发送给Unity shader。
顶点着色器和片元着色器之间的通信
我们希望从顶点着色器输出一些数据,模型的法线,纹理坐标传递给片元着色器,为此要定义一个新的结构体
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "SimpleShader/Chapter5-SimpleShader"
{
SubShader
{
pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct a2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 texcoord:TEXCOORD0;
};
//使用一个结构体来定义顶点着色器的输出
struct v2f
{
//SV_POSITION语义告诉unity,
// pos包含里面包含了顶点在裁剪空间中的位置信息
float4 pos:SV_POSITION;
//COLOR0语义包含了可以用于存储的颜色信息
fixed3 color:COLOR0;
};
v2f vert(a2v v)
{
//声明输出结构
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
//v.normal包含了顶点的法线方向,其分量范围在[-1.0,1.0]
//下面代码映射到[0.0,1.0]
//存储到o.color中传递给片元着色器
o.color=v.normal*0.5+fixed3(0.5,0.5,0.5);
return o;
}
fixed4 frag(v2f i):SV_Target
{
//将插值后的i。color显示到屏幕上
return fixed4(i.color,1.0);
}
ENDCG
}
}
}
如何使用属性
材质提供调节Unity shader中参数的方式
我们想在材质面板上显示一个颜色的拾取器,可以直接控制模型在屏幕上的颜色
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "SimpleShader/Chapter5-SimpleShader"
{
Properties
{
//声明一个Color类型的属性
_Color("Color Tint",Color)=(1.0,1.0,1.0,1.0)
}
SubShader
{
pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//在CG代码中,我们需要定义一个与属性名称和类型都匹配的变量
fixed4 _Color;
struct a2v
{
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
fixed3 color:COLOR0;
};
v2f vert(a2v v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.color=v.normal*0.5+fixed3(0.5,0.5,0.5);
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 c=i.color;
//使用_Color属性来控制颜色
c*=_Color.rgb;
return fixed4(c,1.0);
}
ENDCG
}
}
}
Unity Shader — UnityCG.cginc介绍-CSDN博客
语义学 - Win32 apps | Microsoft Learn
图形学 | Shader |用一篇文章理解法线变换、切线空间、法线贴图 - 知乎
floor ceil fract 函数的区别_fract函数-CSDN博客
Unity3D -- shader常用函数和变量3 - 知乎
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
/*
struct appdata_full {
float4 vertex : POSITION; //顶点坐标
float4 tangent : TANGENT; //切线
float3 normal : NORMAL; //法线
float4 texcoord : TEXCOORD0;
float4 texcoord1 : TEXCOORD1;//第二纹理坐标
float4 texcoord2 : TEXCOORD2;//第三纹理坐标
float4 texcoord3 : TEXCOORD3;//第四纹理坐标
fixed4 color : COLOR;//顶点颜色
UNITY_VERTEX_INPUT_INSTANCE_ID
};
*/
Shader "SimpleShader/False Color"
{
SubShader
{
pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include"UnityCG.cginc"
struct v2f
{
float4 pos:SV_POSITION;
fixed4 color:COLOR0;
};
v2f vert(appdata_full v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
//可视化法线方向
o.color=fixed4(v.normal*0.5+fixed3(0.5,0.5,0.5),1.0);
//可视化切线方向
o.color=fixed4(v.tangent.xyz*0.5+fixed3(0.5,0.5,0.5),1.0);
//可视化副切线方向
fixed3 binormal=cross(v.normal,v.tangent.xyz)*v.tangent.w;
o.color=fixed4(binormal*0.5+fixed3(0.5,0.5,0.5),1.0);
//可视化第一组纹理坐标
o.color=fixed4(v.texcoord.xy,0.0,1.0);
//可视化第二组纹理坐标
o.color=fixed4(v.texcoord1.xy,0.0,1.0);
//可视化纹理第一组纹理小数部分
o.color=frac(v.texcoord); //floor的[0,1],负数会反向取整
if(any(saturate(v.texcoord)-v.texcoord)){
//any输入参数只要有其中一个不为0,则返回true。saturate(v): 将v夹取到 [0,1]区间.
o.color.b=0.5;
}
o.color.a=1.0;
//可视化顶点颜色
//o.color=v.color
return o;
}
fixed4 frag(v2f i):SV_Target{
return i.color;
}
ENDCG
}
}
}
Game-Programmer-Study-Notes/Content/《GPU Gems 2》全书提炼总结/Part1/README.md at master · QianMo/Game-Programmer-Study-Notes · GitHub
在shader中使用流程控制语句很慢
Unity中的基础光照
标准光照模型
自发光(emissive)给定一个方向,表面本身会发射多少辐射量
可以由光源发射进入摄像机,直接使用该材质的自发光颜色
Cemissive-Memissive
高光反射(specular):表面完全镜面反射的光
计算高光,表面法线,视角方向,光源方向,反射方向等如图
r=2*(n*l )(n-l);
Cspecular = (Clight * Mspecular)Max(0,vr)Mgloss
Mgloss 是材质的光泽度(gloss)也被称为反光度(shininess)用于控制高光区域的亮点有多款,Mgloss越大亮点越小 Mspecular 材质的高光反射颜色,对于高光的反射强度和颜色,Clight 是光源的颜色和强度,同样防止vr为负数
Blinn提出一个简单的修改 h=(v+l)/|v+l|
Blinn: Cspecular = (Clight * Mspecular)Max(0,nh)Mgloss
反射角公式推导
可以看成R=L+2*S
然后S是L在N上的投影N是单位向量所以
漫反射(diffuse):表面向每个方向发射的光
对于漫反射角度不重要,认为各个角度上反射光是一样的,但是入射角度很中交
Cdiffuse=(Clight*Mdiffuse)max(0,n*l)
n是表面法线,l是光源的单位矢量,m是材质的反射颜色,光源颜色Clight
需要防止法线和光源方向的点乘的结果为负值(背光)取最大值截取到零
环境光(ambient):其余间接光照
通常是全局变量,模拟间接光照 Cambient = Gambient
逐像素还是逐顶点
在哪里计算光照模型?
在片元着色器中计算是逐像素光照(per-pixel lighting)
在顶点着色器中计算是逐顶点光照(per-vertex lighting)
逐像素光照中:以每个像素为基础,得到法线(可以是对顶点法线插值得到,也可以是法线纹理中采样得到的)然后进行光照计算,这种面片之间对顶点法线进行插值的技术叫Phong着色,不同于Phong着色
逐顶点光照:高洛德着色(Gourand shading) 在逐顶点光照中,每个顶点上计算机光照,然后会在渲染图元内进行内部插值,最后输出成像素颜色,由于顶点着色数目远远少于像素数目,逐顶点的计算量往往要小于逐像素光照,由于逐顶点光照依赖于线性插值得到像素光照,因此当光照模型中有非线性计算(计算高光反射)时逐顶点就会有问题。
实践逐顶点光照(满发射)
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Simple Shader/Chapter 6/Diffuse vertex-Level"
{
Properties
{
_Diffuse("Diffuse",Color) = (1,1,1,1)
}
SubShader
{
Pass
{
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include"Lighting.cginc"
fixed4 _Diffuse;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
};
struct v2f{
float4 pos:SV_POSITION;
float3 color:COLOR;
};
v2f vert(a2v v)
{
v2f o;
//把点从模型到(世界)屏幕空间
o.pos=UnityObjectToClipPos(v.vertex);
//获得环境光照
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
//转换法线从模型到屏幕坐标
//fixed3 worldNormal=
//UnityObjectToWorldNormal(v.normal);
fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));
//全局光照
fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse= _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLight));
//计算需要知道材质的漫反射颜色_Diffuse,顶点法线 v。normal,光原的颜色和强度信息,
//使用内置变量LightColor访问pass来得到颜色和强度信息,多光源不对
//光源方向可以由_WorldSpaceLightPos0得到,在计算点积时,需要选择
//只有处于同一坐标下才有意义,我们选择世界空间坐标,而a2v 得到的法线是位于模型空间的
//可以使用顶点变化矩阵的逆矩阵对法线进行变化,因此先到到 ObjecttoWorld的逆矩阵_World2Object
//调换在mul中的位置得到和转置矩阵相同的矩阵乘法,法线是一个三位向量只要前三行就行
//用saturate来归一化
o.color=ambient+diffuse;
//最后对光相加
return o;
}
fixed4 frag(v2f i):SV_Target
{
return fixed4(i.color,1.0);
}
ENDCG
}
}
FallBack"Diffuse" //Shader的回调
}
//计算需要知道材质的漫反射颜色_Diffuse,顶点法线 v。normal,光原的颜色和强度信息,
//使用内置变量LightColor访问pass来得到颜色和强度信息,多光源不对
//光源方向可以由_WorldSpaceLightPos0得到,在计算点积时,需要选择
//只有处于同一坐标下才有意义,我们选择世界空间坐标,而a2v 得到的法线是位于模型空间的
//可以使用顶点变化矩阵的逆矩阵对法线进行变化,因此先到到 ObjecttoWorld的逆矩阵_World2Object
//调换在mul中的位置得到和转置矩阵相同的矩阵乘法,法线是一个三位向量只要前三行就行
实践逐像素(漫反射)
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
Shader "Simple Shader/DiffusePixelLevelMat"
{
Properties
{
_Diffuse("Diffuse",Color) = (128,127,222,1)
}
SubShader
{
Pass
{
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include"Lighting.cginc"
fixed4 _Diffuse;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
};
struct v2f{
float4 pos:SV_POSITION;
float3 worldNormal:TEXCOORD0;
};
//不需要计算光照模型,把世界空间下的法线坐标传给片元着色器就行
v2f vert(a2v v)
{
v2f o;
//把点从模型到(世界)屏幕空间
o.pos=UnityObjectToClipPos(v.vertex);
o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
return o;
}
fixed4 frag(v2f i):SV_Target
{
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse=_LightColor0.rgb *_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));
fixed3 color=ambient+diffuse;
return fixed4(color,1.0);
}
ENDCG
}
}
FallBack"Diffuse" //Shader的回调
}
显然阴影更圆滑一点
半兰伯特模型
Cdiffuse = (Clight Mdiffuse)(a(nl)+b)
大部分情况下a,b取均值0.5
实现高光反射
Cspecular = (Clight * Mspecular)Max(0,vr)Mgloss
四个参数,入射光颜色,材质的高光系数反射角可以由视角方向V和反射方向R,反射方向R可以由表面法线N和光源方向L计算而得
r=l-2*(n*l)n CG 提供了计算反射方向的函数reflect
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
Shader "Simple Shader/Chapter 6/Diffuse vertex-Level"
{
Properties
{
_Diffuse("Diffuse",Color) = (1,1,1,1)
_Specular("Specular",Color)=(1,1,1,1)
_Gloss("Gloss",Range(8.0,256))=20
}
SubShader
{
Pass
{
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include"Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
};
struct v2f{
float4 pos:SV_POSITION;
float3 color:COLOR;
};
v2f vert(a2v v)
{
v2f o;
//把点从模型到(世界)屏幕空间
o.pos=UnityObjectToClipPos(v.vertex);
//获得环境光照
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
//转换法线从模型到屏幕坐标
fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));
//全局光照
fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse= _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLightDir));
//获取反射角
fixed3 reflectDir=normalize(reflect(-worldLightDir,worldNormal));
//获取入光
fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-mul(unity_ObjectToWorld,v.vertex).xyz);
//高光
fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(reflectDir,viewDir)),_Gloss);
o.color=ambient+diffuse+specular;
//最后对光相加
return o;
}
fixed4 frag(v2f i):SV_Target
{
return fixed4(i.color,1.0);
}
ENDCG
}
}
FallBack"Specular" //Shader的回调
}
高光反射并非线性计算,在顶尖着色器中进行插值的过程是线性的
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
Shader "Simple Shader/Chapter 6/Diffuse vertex-Level"
{
Properties
{
_Diffuse("Diffuse",Color) = (1,1,1,1)
_Specular("Specular",Color)=(1,1,1,1)
_Gloss("Gloss",Range(8.0,256))=20
}
SubShader
{
Pass
{
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include"Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v{
float4 vertex:POSITION;
float3 normal:NORMAL;
};
struct v2f{
float4 pos:SV_POSITION;
float3 worldNormal:TEXCOORD0;
float3 worldPos:TEXCOORD1;
};
v2f vert(a2v v)
{
v2f o;
//把点从模型到(世界)屏幕空间
o.pos=UnityObjectToClipPos(v.vertex);
//转换法线从模型到屏幕坐标
o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 frag(v2f i):SV_Target
{
//获得环境光照
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
//转换法线从模型到屏幕坐标
fixed3 worldNormal=normalize(i.worldNormal);
//全局光照
fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse= _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLightDir));
//获取反射角
fixed3 reflectDir=normalize(reflect(-worldLightDir,worldNormal));
//获取入光
fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-i.worldPos.xyz);
//高光
fixed3 specular= _LightColor0.rgb*_Specular.rgb*pow(saturate(dot(reflectDir,viewDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
FallBack"Specular" //Shader的回调
}
内置函数得到是没有归一化的,要用normalize来归一化
纹理基础
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "Simple Shader/Single Texture"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color("Color",Color)=(1,1,1,1)
_Specular("Specular",Color)=(1,1,1,1)
_Gloss("Gloss",Range(8.0,256))=20
}
SubShader
{
Tags { "LightMode"="ForWardBase" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#include "Lighting.cginc"
fixed4 _Color;
fixed4 _Specular;
sampler2D _MainTex;
fixed4 _MainTex_ST; //纹理的属性S缩放,T平移
float _Gloss;
struct a2v
{
fixed3 normal:NORMAL;
fixed4 vertex:POSITION;
fixed4 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=v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw; //乘以横纵
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 albedo=tex2D(_MainTex,i.uv).rgb*_Color.rgb;
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
fixed3 diffues=_LightColor0.rgb*albedo*max(0,dot(worldNormal,worldLightDir));
fixed3 viewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir=normalize(worldLightDir+viewDir);
fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(worldNormal,halfDir)),_Gloss);
return fixed4(ambient+diffues+specular,1.0);
}
ENDCG
}
}
FallBack"Specular";
}
与其他属性不同的是,我们还需要为纹理属性设置一个float4类型的变量,_MainTex_ST,在Unitt中需要用纹理名_ST,.xy是缩放值,zw是平移值,
属性:
Wrap Mode:Repeat
Wrap Mode:clamp
File Mode:滤波的模式也有三种,point,Bilinear,Trilinear 越来越清开销也大
纹理缩放,多级渐远纹理(mipmapping),二进制压缩
将纹理类型 TextrueType选择Advance再勾选Genrate Mip Maps即可开启
纹理过滤模式中的Bilinear、Trilinear以及Anistropic Filtering(转) - 知乎
凹凸映射
法线方向的范围【-1,1】,像素的范围【0,1】做映射
pixel=(nomal+1)/2
方向是相对空间来说的
对于模型顶点自带的法线,他们是定义在模型空间的,因此直接将修改后的模型空间中的表面的法线存到一张纹理中,模型空间的法线纹理
但实际会采用另一种坐标空间,模型顶点的切线空间来存法线,切线空间的法线纹理,这个切线空间就是顶点本身,z轴是顶点的法线方向,x轴是顶点的切线方向,y则是由两者做叉积得到的复法线,被称为BTN空间
切线空间下的计算:
Normal Map In Tangent Space
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 7/Normal Map In Tangent Space" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {} //法线纹理的定义
_BumpScale ("Bump Scale", Float) = 1.0
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;//为了得到该纹理的属性(平铺和偏移属性)
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;//一般两个使用同一组纹理坐标,为了减少插值寄存器的使用数目
float _BumpScale;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
/*用TANGENT来描述float4类型让unity把顶点的切线方向填充到变量中
和法线方向的normal不同这里是一个float4的类型,tangent.w副切线的方向
*/
};
struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float3 lightDir: TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
// Unity doesn't support the 'inverse' function in native shader
// so we write one by our own
// Note: this function is just a demonstration, not too confident on the math or the speed
// Reference: http://answers.unity3d.com/questions/218333/shader-inversefloat4x4-function.html
float4x4 inverse(float4x4 input) {
#define minor(a,b,c) determinant(float3x3(input.a, input.b, input.c))
float4x4 cofactors = float4x4(
minor(_22_23_24, _32_33_34, _42_43_44),
-minor(_21_23_24, _31_33_34, _41_43_44),
minor(_21_22_24, _31_32_34, _41_42_44),
-minor(_21_22_23, _31_32_33, _41_42_43),
-minor(_12_13_14, _32_33_34, _42_43_44),
minor(_11_13_14, _31_33_34, _41_43_44),
-minor(_11_12_14, _31_32_34, _41_42_44),
minor(_11_12_13, _31_32_33, _41_42_43),
minor(_12_13_14, _22_23_24, _42_43_44),
-minor(_11_13_14, _21_23_24, _41_43_44),
minor(_11_12_14, _21_22_24, _41_42_44),
-minor(_11_12_13, _21_22_23, _41_42_43),
-minor(_12_13_14, _22_23_24, _32_33_34),
minor(_11_13_14, _21_23_24, _31_33_34),
-minor(_11_12_14, _21_22_24, _31_32_34),
minor(_11_12_13, _21_22_23, _31_32_33)
);
#undef minor
return transpose(cofactors) / determinant(input);
}
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
// Construct a matrix that transforms a point/vector from tangent space to world space
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
/*
float4x4 tangentToWorld = float4x4(worldTangent.x, worldBinormal.x, worldNormal.x, 0.0,
worldTangent.y, worldBinormal.y, worldNormal.y, 0.0,
worldTangent.z, worldBinormal.z, worldNormal.z, 0.0,
0.0, 0.0, 0.0, 1.0);
// The matrix that transforms from world space to tangent space is inverse of tangentToWorld
float3x3 worldToTangent = inverse(tangentToWorld);
*/
//wToT = the inverse of tToW = the transpose of tToW as long as tToW is an orthogonal matrix.
float3x3 worldToTangent = float3x3(worldTangent, worldBinormal, worldNormal);
// Transform the light and view dir from world space to tangent space
o.lightDir = mul(worldToTangent, WorldSpaceLightDir(v.vertex));
o.viewDir = mul(worldToTangent, WorldSpaceViewDir(v.vertex));
///
/// Note that the code below can only handle uniform scales, not including non-uniform scales
///
// Compute the binormal
// float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w;
// // Construct a matrix which transform vectors from object space to tangent space
// float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
// Or just use the built-in macro
// TANGENT_SPACE_ROTATION;
//
// // Transform the light direction from object space to tangent space
// o.lightDir = mul(rotation, normalize(ObjSpaceLightDir(v.vertex))).xyz;
// // Transform the view direction from object space to tangent space
// o.viewDir = mul(rotation, normalize(ObjSpaceViewDir(v.vertex))).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentViewDir = normalize(i.viewDir);
// Get the texel in the normal map
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
fixed3 tangentNormal;
// If the texture is not marked as "Normal map"
// tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
// tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
// Or mark the texture as "Normal map", and use the built-in funciton
tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
vert:我们把模型空间下切线方向,副切线方向,法线方向按行排列得到从模型空间到切线空间的变换矩阵rotation,在计算副切线的时候使用v.tangent.w和叉积得到结果,这是因为和切线与法线的面都垂直的方向有两个w决定了选择哪一个方向。
frag:只需要采样得到切线空间下的法线空间,在这切线空间下光照计算
我们首先利用tex2D纹理对法线纹理_BumpMap进行采样,把color反射回来,如果没有设置Normal map的属性就要手动操作,先把packeNormal的xy分量按公式映射回法线,然后乘以_BumpScale控制凹凸程度来得到tangNormal的xy分量,由于法线都是单位矢量,因此tangent.z的分量可以由tangentNormal.xy来计算,切线空间下的保证z为正数,
世界空间下的计算
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 7/Normal Map In World Space" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {}
_BumpScale ("Bump Scale", Float) = 1.0
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
// Compute the matrix that transform directions from tangent space to world space
// Put the world position in w component for optimization
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
return o;
}
fixed4 frag(v2f i) : SV_Target {
// Get the position in world space
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
// Compute the light and view dir in world space
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// Get the normal in tangent space
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
bump.xy *= _BumpScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
// Transform the narmal from tangent space to world space
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
fixed3 halfDir = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
渐变纹理
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "sample shader/RampTexture"
{
Properties
{
_RampTex("Ramp Texture", 2D) = "white" {}
_Color("Color Tint",Color)=(1,1,1,1)
_Specular("Spcular",Color)=(1,1,1,1)
_Gloss("Gloss",Range(8.0,256))=20
}
SubShader
{
Tags { "LightMode"="ForwardBase" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc" //_LightColor()变量
struct a2v
{
float4 vertex : POSITION;
float3 normal: NORMAL;
float4 texcoord:TEXCOORD0;
};
struct v2f
{
float worldNormal:TEXCOORD0;
float worldPos:TEXCOORD1;
float2 uv : TEXCOORD2;
float4 pos: SV_POSITION;
};
sampler2D _RampTex;
float4 _RampTex_ST;
float4 _Color;
float4 _Specular;
float _Gloss;
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, _RampTex);
//内置宏来计算经过平铺和偏移后的纹理坐标
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 worldNormal=normalize(i.worldNormal);
float3 worldDirLight=normalize(UnityWorldSpaceLightDir(i.worldPos));
//https://blog.csdn.net/JaMorant/article/details/125616269
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
//兰伯特定律,对于慢反射光来说,反射光线的强度与表面法线和光源之间的夹角余弦成正比
//c=c*m*max(0,n*l); 下面使用半兰特模型
fixed halfLambert=0.5*dot(worldNormal,worldDirLight)+0.5; //取法线在入射光方向的0.5+0.5
//这里tex2d对纹理采样,使用halfLambert来构建一个纹理坐标,_RampTex实际上是一个一维纹理,因此坐标轴都用uv
//把从渐进纹理采样得到的颜色和材质颜色相乘,得到漫反射的光
fixed3 diffuseColor=tex2D(_RampTex,fixed2(halfLambert,halfLambert)).rgb*_Color.rgb;
fixed3 diffuse=_LightColor0.rgb*diffuseColor;
//高光反射与反射角和法线的预先成gloss次放比
fixed3 viewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir=normalize(worldDirLight+viewDir);
fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(worldNormal,halfDir)),_Gloss);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
Fallback "Spcular"
}
需要把渐变纹理的Wrap Mode设置为Clamp模式,以防止对纹理进行采样的精度问题,Repeat中在高光区域有一些黑点,由于精度有问题,当我们用fixed2(halfLambert,halfLambert) 来采样的时候,虽然值在【0,1】之间但是会出现1.00001这样的如果是repeat模式就会舍弃整数数,变成0.00001,即为渐进图最左边的颜色黑色,高光区有黑点!!
遮罩纹理
保护某些区域防止修改,不希望所有点打的光都是一样的
一般用法:通过采样得到遮罩纹理额度纹素值,然后使用其中莫格通道的值来与某种表面属性进行相乘,这样,当通道值为0时可以保护表面属性不影响··
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 7/Mask Texture" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {}
_BumpScale("Bump Scale", Float) = 1.0
_SpecularMask ("Specular Mask", 2D) = "white" {} //遮罩纹理
_SpecularScale ("Specular Scale", Float) = 1.0 //系数
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float _BumpScale;
sampler2D _SpecularMask;
float _SpecularScale;
fixed4 _Specular;
float _Gloss;
/*
我们为主纹理,法线纹理,遮罩纹理,定义了他们共铜使用的纹理变量属性
_MainTex_ST 在材质主面板修改主纹理的平铺和偏移系数会影响三个纹理的采样
这种方式可以让我们节省需要存储的纹理坐标数目,为每一个纹理都定义 xxx_ST
那随着纹理的使用增加,我们会迅速沾满顶点着色器可以使用的插值寄存器。
但很多时候我们不需要对纹理平铺和位移,或者很多纹理平铺和位移操作相同
就使用同一个变换后的纹理坐标来采样
*/
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 lightDir: TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
TANGENT_SPACE_ROTATION;
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentViewDir = normalize(i.viewDir);
fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv));
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
// Get the mask value 对遮罩纹理采样选择分量r来计算掩码值
fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;
// Compute specular term with the specular mask
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss) * specularMask;
//现在通常会用一张纹理的RGBA四个通道存储不同的属性
//高光放R里,边缘光照放G里,高光反射指数放B里,自发光强度放A里
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
透明效果 Alpha
这章建议看原文写的很好
在实时渲染中要实现透明效果通常会使用控制它的透明通道,当一个物体被渲染到屏幕上时除了颜色和深度值之外还有透明度这个属性。
两种方法来实现透明效果
透明度测试和透明混合
Z-buffer:深度缓存,用于解决物体的可见性,它可以决定物体的那些部分会被渲染在前面,哪些会被遮挡,根据深度缓存的值来判断该片元距离摄像机的距离,当渲染一个片元的时候,需要把它的深度值和已经存在深度缓冲中的值进行比较,当渲染一个片元时,把它的深度和已经存在深度缓冲的值进行比较(如果开启了深度测试),如果它的值距离摄像机更远了说明有物体放在他前面挡住了不应该渲染,否则就应该覆盖掉此时颜色缓存中的像素值,并更新深度缓存(如果开启了深度写入),像是一下代码
void clip(float4 x){
if(any(x)<0) discard;
}
问题在于对于透明物体的渲染,当我使用透明度混合时,我们关闭了深度写入(Z-Write)不关闭后面
透明度测试(AlphaTest)和混合(Blend)的基础知识_alpha test-CSDN博客
循环重叠的不透明物体是十分难渲染的,深度缓冲是像素级别的,但是按物体不一定能够达到要求
unity提供了渲染队列来结局问题
Unity-Shader-渲染队列_unity renderqueue-CSDN博客
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 8/Alpha Test With Both Side" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
}
SubShader {
Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
Pass {
Tags { "LightMode"="ForwardBase" }
// Turn off culling
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _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 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
clip (texColor.a - _Cutoff);
/*
这里裁剪
*/
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
return fixed4(ambient + diffuse, 1.0);
}
ENDCG
}
}
FallBack "Transparent/Cutout/VertexLit"
}
透明度混合
双面渲染的透明效果
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 8/Alpha Blend With Both Side" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1
}
SubShader {
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
Pass {
Tags { "LightMode"="ForwardBase" }
// First pass renders only back faces
Cull Front
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
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 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
Pass {
Tags { "LightMode"="ForwardBase" }
// Second pass renders only front faces
Cull Back
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
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 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}