unity-shader相关

unity shader 相关记录


相关资料


感觉不错的仓库

尽管已经 star, 但还是记录一下哪些要看


在线画图工具, 数学公式


todo


View dir 和 eye vec 区别

只是方向不同


渲染管线

下图显示了 前向渲染管线 中各个阶段主要完成的工作,蓝色部分代表的是我们可以定义自己的着色器。

img

  在上图中,我们以数组的形式传递3个3D坐标作为渲染管线的输入,用它来表示一个三角形,这个数组叫做顶点数据(Vertex Data);这里顶点数据是几个顶点的集合。每个顶点是用顶点属性(vertex attributes)表示的,它可以包含任何我们希望用的数据,下面我们来看看渲染管线中各个阶段主要完成的工作:

  • 渲染管线的第一个部分是顶点着色器(vertex shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标(投影坐标),同时顶点着色器允许我们对顶点属性进行一些基本处理。
  • 图元组装(primitive assembly)阶段把顶点着色器的表示为基本图形的所有顶点作为输入,把所有点组装为特定的基本图形的形状;上图中是一个三角形。
  • 图元组装阶段的输出会传递给几何着色器(geometry shader)。几何着色器把基本图形形成的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其他的)基本图形来生成其他形状。
  • 细分着色器(tessellation shaders)拥有把给定基本图形细分为更多小基本图形的能力。这样我们就能在物体更接近玩家的时候通过创建更多的三角形的方式创建出更加平滑的视觉效果。
  • 细分着色器的输出会进入光栅化(rasterization)阶段,这里它会把基本图形映射为屏幕上相应的像素,生成供像素着色器(fragment shader)使用的fragment(OpenGL中的一个fragment是OpenGL渲染一个独立像素所需的所有数据。)。在像素着色器运行之前,会执行裁切(clipping)。裁切会丢弃超出你的视图以外的那些像素,来提升执行效率。
  • 像素着色器的主要目的是计算一个像素的最终颜色,这也是OpenGL高级效果产生的地方。通常,像素着色器包含用来计算像素最终颜色的3D场景的一些数据(比如光照、阴影、光的颜色等等)。
  • 在所有相应颜色值确定以后,最终它会传到另一个阶段,我们叫做alpha测试和混合(blending)阶段。这个阶段检测像素的相应的深度(和stencil)值,使用这些来检查这个像素是否在另一个物体的前面或后面,如此做到相应取舍。这个阶段也会查看alpha值(alpha值是一个物体的透明度值)和物体之间的混合(blend)。所以即使在像素着色器中计算出来了一个像素所输出的颜色,最后的像素颜色在渲染多个三角形的时候也可能完全不同。

  虽然渲染管线有多个阶段,每个阶段都需要对应的着色器,但其实对于大多数场合,我们必须做的只是顶点和像素着色器,几何着色器和细分着色器是可选的,通常使用默认的着色器就行了。现在的OpenGL中,我们必须定义至少一个顶点着色器和一个像素着色器(因为GPU中没有默认的顶点/像素着色器)。

img

Unity把每一frame绘制的事件进行了拆分,然后在其中定义了一些点,在这些点处,可以通过command buffer嵌入一些事件(比如设置RT,绘制一些物件等)。比如在延迟渲染中,可以当G-Buffer中绘制完毕后,往里面绘制一些额外物件。通过下图可以看到Unty的渲染顺序,并可以清晰的看到在绿色点标记的地方可以嵌入command buffer去执行自定义的命令:


RenderingMode 渲染模式

渲染模式总共有四种:

渲染模式 意思 适用对象举例 说明
Opaque 不透明 石头 适用于所有的不透明的物体
Cutout 镂空 破布 透明度不是0%就是100%,不存在半透明的区域。
Fade 隐现 物体隐去 与Transparent的区别为高光反射会随着透明度而消失。
Transparent 透明 玻璃 适用于像彩色玻璃一样的半透明物体,高光反射不会随透明而消失。

变量命名规则

以法线为例, 一般会以某个空间为前缀.

1
2
3
4
5
// 切线空间下的法线
fixed3 tangentNormal = UnpackNormal(packedNormal);

// 世界空间下的法线
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);

顶点法线


顶点->图元->像素


Wrap mode 循环模式

在贴图边界以设置贴图的重复模式的方式避免不真实的情

  • 使用强制贴图边界拉伸 TextureWrapMode.Clamp

  • 贴图重复平铺 TextureWrapMode.Repeat

    这种方式下, uv 超过 1 的时候, 又会从 0 开始, 假设这种模式下做的扫光特效, 会一直往复循环


UnityShader之空间变换解析


模板测试


混合

unity 内置的 Unlit-Texture,渲染类型 Opaque (不透明物体),所以不用开 Blend,默认也是关闭了混合( Blend Off)

屏幕上显示颜色计算规则

语法

Blend Off 不混合
Blend SrcFactor DstFactor SrcFactor是源系数,DstFactor是目标系数
最终颜色 = (Shader计算出的点颜色值 * 源系数)+(点累积颜色 * 目标系数)

属性(往SrcFactor,DstFactor 上填的值, 也就是 系数

解释
one 1
zero 0
SrcColor 源的RGB值,例如(0.5,0.4,1)
SrcAlpha 源的A值, 例如0.6
DstColor 混合目标的RGB值例如(0.5,0.4,1)
DstAlpha 混合目标的A值例如0.6
OneMinusSrcColor (1,1,1) - SrcColor
OneMinusSrcAlpha 1- SrcAlpha
OneMinusDstColor (1,1,1) - DstColor
OneMinusDstAlpha 1- DstAlpha

运算法则示例:

(注:r,g,b,a,x,y,z取值范围为[0,1])

计算方式
(r,g,) * a = (ra, ga, b*a)
(r,g,) * (x,y,z) = (rx, gy, b*z)
(r,g,) + (x,y,z) = (r+x, g+y, b+z)
(r,g,) - (x,y,z) = (r-x, g-y, b-z)

源颜色 为 该次绘制物体颜色, 目标颜色 为 缓冲区颜色

  • Blend SrcAlpha OneMinusSrcAlpha

    最终颜色 = 源颜色 * 源透明值 [SrcAlpha] + 目标颜色*(1 - 源透明值)[OneMinusSrcAlpha]

    结论:贴图alpha值越大,颜色越偏向贴图;alpha值越小,颜色越偏向混合目标


矩阵

http://ios.jobbole.com/89931/

在线 矩阵计算器 :http://www.yunsuanzi.com/matrixcomputations/solvematrixmultiplication.html

矩阵乘法

A的列 一定要等于 B的行 ( 不能使用交换律 )

正交矩阵

http://www.mashangxue123.com/%E7%BA%BF%E6%80%A7%E4%BB%A3%E6%95%B0/2077705030.html

先来看一下正交矩阵是如何定义的,若方阵M是正交的,则当且仅当M与他的转置矩阵M^T的乘积等于单位矩阵,那么就称矩阵M为正交矩阵.

$$
M^{T} M=I
$$
在矩阵的逆中我们知道,矩阵的逆和矩阵的乘积为单位矩阵I,由此推理,我们可以知道,如果该矩阵为正交矩阵,那么矩阵的逆和转置矩阵是相等的.

$$
M^{T}=M^{-1}
$$
那么正交矩阵存在的意义是什么呢?其实如果一个矩阵是正交矩阵,那么矩阵的逆和转置矩阵是相等的.转置矩阵是非常简单计算的,而计算矩阵的逆如果使用代数余子式计算是非常的麻烦,所以我们可以直接计算转置矩阵然后直接得到该矩阵的逆.

底下是一些重要的性质:


单位矩阵 I

矩阵的乘法中,有一种矩阵起着特殊的作用,如同数的乘法中的1,这种矩阵被称为单位矩阵。它是个方阵,从左上角到右下角的对角线(称为主对角线)上的元素均为1。除此以外全都为0。
$$
I_{1}=[1], I_{2}=\left[ \begin{array}{cc}{1} & {0} \ {0} & {1}\end{array}\right], I_{3}=\left[ \begin{array}{ccc}{1} & {0} & {0} \ {0} & {1} & {0} \ {0} & {0} & {1}\end{array}\right], \cdots, I_{n}=\left[ \begin{array}{cccc}{1} & {0} & {\cdots} & {0} \ {0} & {1} & {\cdots} & {0} \ {\vdots} & {\vdots} & {\ddots} & {\vdots} \ {0} & {0} & {\cdots} & {1}\end{array}\right]
$$


正交矩阵

但事实上,一个坐标系能用任意3个基向量定义,当然这三个基向量要线性无关(也就是不在同一平面上),

如果这三个基向量(归一化单位向量) 相互垂直 ,那么构成的矩阵是一个正交矩阵

正交矩阵 求 逆矩阵

1
2
// 求 TBN 矩阵的逆矩阵,因为 TBN 矩阵由三个互相垂直的单位向量组成,所以它是一个正交矩阵
// 正如前面所说,正交矩阵的逆矩阵等于它的转置,所以无需真的求逆矩阵

效果


切线空间

unity-shader-切线空间.md

使用 法线贴图 是因为在 低模 下想获得 高模 凹凸表面光照效果。(也就是面数不够,法线来凑,基于面的法线建立一个 虚拟坐标系 ,通过 法线贴图 在面上 加多点法线 )

由于需要将同一份法线纹理贴到不同模型的表面,或者同一个模型中不同角度的表面,那么当初生成这份法线纹理时所使用的面元角度(所谓模型local坐标系),就无法适用在其他角度的面元上。所以,人们不记录当时用模型坐标系生成的法线,而使用z轴与该点所在面元的法线平行的一个虚拟的相对坐标系来记录该点法线在该坐标系中的x/y/z的值,相当于记录了该点法线与所在平面法线之间的相对关系,而不是记录绝对坐标。由于点的法向量基本不会偏移面法向量太多,也就是凹凸程度一般不会太夸张,所以最终的法向量值中,z的值总是比x和y的大一些(也就是说 点法向量 与平面的 的夹角一般会大于45度),用(法向量值+1)/2转换为RGB后,就成了蓝色为主的色调了。

img

经过这样记录下来的法向量,贴到哪个模型表面,就按照当前表面的法向量,计算出该点法向量在世界坐标空间中的值,然后计算光照就可以了。

但是有个问题,看下图,竖轴同样是面法向量,但是有多种不同的x和y轴组合,每种组合生成的点法向量是不一致的,所以需要规定一套固定的x和y轴,大家遵守同样的规则。怎么规定呢?就用纹理的uv坐标来定。具体做法是,取该点所在的三角形的三个顶点P1.P2.P3的纹理U和V坐标,然后x轴的方向就是P3指向P1,又称T轴;y轴方向是P3指向P2,又称B轴。 (完)

img

参考资料

法线向量 使用

参考:https://blog.csdn.net/u013354943/article/details/52779991

1. 切线空间下使用 (常用,比较简单)
  • 直接使用 顶点的 法线normal切线tangent叉乘 算出 副切线binormal,然后三个向量 构建一个 切线空间 的 矩阵,然后将 世界空间 下的 光向量lightDir 和 观察向量viewDir。全过程都在 顶点着色器 中进行
2. 世界空间下使用
  • 先在 顶点着色器 中,算出 世界空间 下的顶点 法线worldNormal切线worldTangent , 叉乘算出 副切线worldBinormal ,然后 构建 切线空间到世界空间的变换矩阵,
  • 然后在 片段着色器 中,将 切线向量 变换 到 世界空间下

法线贴图


通过矩阵变换空间

参考 《Unity Shader入门精要》 4.8.1 和 4.9.2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
// 转换顶点 从 模型空间 到 剪裁空间
o.pos = UnityObjectToClipPos(v.vertex);

// Transform the normal from object space to world space
// 转换法线 从 模型空间 到 世界空间
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
// o.worldNormal = UnityObjectToWorldNormal(v.normal);
// o.worldNormal = mul((float3x3)IT_Object2World, v.normal);
// 法线是向量, 所以和顶点变换有所不同. 可以参考 : https://forum.unity.com/threads/_object2world-or-unity_matrix_it_mv.112446/

// 转换法线 从 世界空间 到 模型空间
float3 objNormal = mul((float3x3)unity_WorldToObject, o.worldNormal)
// objNormal 等价与 v.normal

// Transform the vertex from object spacet to world space
// 转换顶点 从 模型空间 到 世界空间
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

// 另一种变换 法线 从 模型空间 到 世界空间,其实就是 mul(v.normal, (float3x3)unity_WorldToObject); 的运算展开
// o.worldNormal = normalize(unity_WorldToObject[0].xyz * v.normal.x + unity_WorldToObject[1].xyz * v.normal.y + unity_WorldToObject[2].xyz * v.normal.z);
return o;
}

内置变量

这里写图片描述

unity 矩阵构建

unity 中的 xyz 坐标轴在矩阵中的排列为 横向 排列

所以构建矩阵或变换xyz分量时要要用 横向 数据

比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// csharp
Vector3 right = transform.TransformDirection(transform.right);
Vector3 up = transform.TransformDirection(transform.up);
Vector3 forward = transform.TransformDirection(transform.forward);

mMat.SetVector("_right", right);
mMat.SetVector("_up", up);
mMat.SetVector("_forward", forward);

// shader
// 构建世界矩阵
float4x4 worldMatrix = float4x4(_right, 0, _up, 0, _forward, 0, 0, 0, 0, 1); // 构建 世界空间矩阵
float4 worldPos = mul(worldMatrix, v.vertex);
float4 viewPos = mul(UNITY_MATRIX_V, worldPos);
o.vertex = mul(UNITY_MATRIX_P, viewPos);

变换某个分量

以变换 顶点模型空间 -> 剪裁空间 为例. 以下三种方式等价

1
2
3
4
5
6
7
8
9
10
11
12
13
14
v2f o;
// 方式一
// o.pos = UnityObjectToClipPos (v.vertex);

// 方式二
// float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
// o.pos = mul(UNITY_MATRIX_VP, worldPos);

// 方式三
float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
o.pos.x = mul(UNITY_MATRIX_VP[0], worldPos);
o.pos.y = mul(UNITY_MATRIX_VP[1], worldPos);
o.pos.z = mul(UNITY_MATRIX_VP[2], worldPos);
o.pos.w = mul(UNITY_MATRIX_VP[3], worldPos);

踩坑

  • 顶点 变换, 要用 float4 类型, 四个分量. 因为使用到平移, 用的矩阵是 float4x4.

    法线 变换, 用 float3 类型, 三个分量, 因为法线是向量, 不存在平移的概念, 所以用的矩阵是 float3x3.


坐标系


齐次坐标


标准化设备坐标 ( NDC )

OpenGL希望在所有顶点着色器运行后,所有我们可见的顶点都变为标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说,每个顶点的x,y,z坐标都应该在-1.0到1.0之间,超出这个坐标范围的顶点都将不可见。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标转换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),再将他们转换为屏幕上的二维坐标或像素。

将坐标转换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步,也就是类似于流水线那样子,实现的,在流水线里面我们在将对象转换到屏幕空间之前会先将其转换到多个坐标系统(Coordinate System)。将对象的坐标转换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中进行一些操作或运算更加方便和容易,这一点很快将会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统:

  • 局部空间(Local Space,或者称为物体空间(Object Space))
  • 世界空间(World Space)
  • 观察空间(View Space,或者称为视觉空间(Eye Space))
  • 裁剪空间(Clip Space)
  • 屏幕空间(Screen Space)

这些就是我们将所有顶点转换为片段之前,顶点需要处于的不同的状态。

标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形(忽略z轴, z轴有深度转化而来):

后处理中的ndc坐标构建

1
2
3
4
5
6
//使用宏和纹理坐标对深度纹理进行采样,得到深度值
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
//构建当前像素的NDC坐标,xy坐标由像素的纹理坐标映射而来,z坐标由深度值d映射而来
float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1); // 将屏幕坐标(0, 1) 转到ndc坐标(-1, 1)
// gl 的 Zndc 是 -1~1 之间, 所以 d*2-1
// dx 的 Zndc 是 0~1 之间, 所以直接用 d 即可

透视矩阵推导

https://zhuanlan.zhihu.com/p/56836628

  • 已知变量

    AE: 近剪裁面 用 n 代替这个已知量

    AD: 远剪裁面,用 u 来代替;

    ∠a:就是摄像机的张角,也就是FOV

$$
\theta=\frac{\angle a}{2}
$$

$$
k=\frac{F L}{F G}=\frac{h e i g h t}{\text {width}} (k是宽高的比例系数,已知条件 aspect)
$$

  • 最后推导出的透视矩阵
    $$
    \begin{array}{cccc}{\frac{k^{2}}{\tan (\theta)}} & {0} & {0} & {0} \ {0} & {\frac{e^{2}}{\tan (\theta)}} & {0} & {0} \ {0} & {0} & {-\frac{u+n}{u-n}} & {0} \ {0} & {0} & {-\frac{2 f n}{u-n}} & {0}\end{array}
    $$

将顶点从 view 坐标系转换到 NDC 下

这里面包含了两个步骤,将 view坐标系 下的顶点乘以透视矩阵,转换到 Clip坐标系,得到 Clip坐标,

$$
\left( \begin{array}{l}{x_{clip}} \ {y_{clip}} \ {z_{clip}}\end{array}\right)=M_{p r o j e c t i o n} \cdot \left( \begin{array}{c}{x_{e y e}} \ {y_{e y e}} \ {z_{c y e}} \ {w_{e y e}}\end{array}\right)
$$
然后统一除以w,得到NDC 坐标。
$$
\left( \begin{array}{l}{x_{n d c}} \ {y_{n d c}} \ {z_{n d c}}\end{array}\right)=\left( \begin{array}{l}{x_{c l i p} / w_{c l i p}} \ {y_{c l i p} / w_{c l i p}} \ {z_{c l i p} / w_{c l i p}}\end{array}\right)
$$


ddx, ddy 函数

可以参考:

ddx,ddy 只能在 Fragment Shader下使用, 因为它是用来计算相邻像素的某些属性值之差. ( 属性值比如 worldPos )

求出法线的实例. 利用 worldPos 的 x,y 之差查, 得到两个矢量, 通过 叉乘 得到垂直于这两个矢量组成面的 第三个矢量, 也就是法线.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
float3 dx = ddx(i.worldPos);
float3 dy = ddy(i.worldPos);
float3 normal = normalize(cross(dx, dy));

normal = normal * 0.5 + 0.5;
return fixed4(normal,1.0);
}

那么ddx,ddy 可以用在哪里呢。

  1. 计算出该像素的法线
  2. 计算mipmap level 计算当前纹理在屏幕分辨率下的大小是否小于某个mipmap等级的纹理大小,是的话就切换纹理
  3. 视差贴图 从视野方向计算像素挤出的位置的uv值

OpenGL中的坐标变换、矩阵变换

六种常见坐标系:

  1. Object or model coordinates(模型坐标系)
  2. World coordinates(世界坐标系)
  3. Eye (or Camera) coordinates(视坐标系)
  4. Clip coordinates(裁剪坐标系)
  5. Normalized device coordinates(归一化设备坐标系)
  6. Window (or screen) coordinates(屏幕坐标系)

剪裁坐标

在我们来解释一下这个坐标系为什么叫做“裁剪坐标系”,看看哪里来的“裁剪”二字:

需要特别注意的是,模型坐标系、世界坐标系、视点坐标系中的第四个分量w都是1,但是经过了投影变换之后的坐标的w分量不再为1,这时候就可以发挥w分量的作用了。在我们手动编程计算完gl_Position之后,进入GPU自身的流水管线,GPU会根据裁剪坐标gl_Position中 xyz分量 与 w分量 绝对值的大小进行比较进行裁剪。具体的过程是:GPU依次将gl_Position中x、y、z的绝对值与w的绝对值分别比较,只要有一个分量的绝对值大于w的绝对值 (也就是 Xclip/ Wclip 的值 >1 或 <-1 ) ,GPU就认为该点不在视景体内,就会被裁减掉,也就是说裁剪的过程是GPU自己进行的,没有被裁减掉的坐标 xyz分量 的绝对值都小于 w分量 的绝对值 (也就是 -1< Xclip/ Wclip 的值 <1 ) 。所以现在应该知道经过投影之后的坐标是为了让GPU进行裁剪用的,所以才叫做“裁剪坐标”。


线性空间与GAMMA校正

Gamma计算很简单,只是个power而已,也就是:
$$
\text { Color }{out}=\text { Color }{i n}^{\gamma}
$$
其中的γ就是用来校正的gamma值。

可惜,现实是残酷的,显示器的gamma为2.2,所以如果相机仍然是线性的,那么结果就会变成:

这样在显示器上看到的就会有明显的色彩失真。解决方法是把相机的gamma设成1/2.2,这样两次调整之后又能得到真实场景的色彩了:

对渲染的意义

前面讲的输入是对相机拍的照片而言。而对渲染来说,情况又如何呢?渲染中用到的光照都是在线性空间的。因为在设计光照的时候都是认为1的亮度是0.5的2倍。光照如此,texture又如何呢?渲染中用到的 texture一般有两个来源,一个是照片,一个是artist手工画的。前文提到了,照片是gamma = 1/2.2的。一般图象处理软件也都是在gamma空间工作的,所以artist画的图一般也可以认为是gamma = 1/2.2的。所以,我们在pixel shader常可以见到这样的代码:

1
2
float4 diff = tex2D(diffuse_texture, uv);
return diff \* max(0, dot(light_dir, normal));

这样的代码对吗?不对也对。

说其不对,是因为这里没显式地做gamma校正。做校正的话应该是这样的:

1
2
float4 diff = pow(tex2D(diffuse_texture, uv), 2.2f);
return pow(diff \* max(0, dot(light_dir, normal)), 1 / 2.2f);

也就是说,gamma校正的过程就是把输入的texture都转换到线性空间,并把输出的调整到gamma = 1/2.2的空间。

总结

总之,计算 都要发生在 线性空间,所以输入和输出需要进行gamma校正。最佳选择是采用sRGB格式,这样pow是 硬件内自动实现 ,速度更快,代码也简单。鉴于目前很多texture的数据是gamma = 1/2.2的,而纹理格式却被错误地标记成没有sRGB的,所以需要修改它们的格式标记,并重新建立mipmap。

贴图格式是sRGB, 也就是该贴图是gamma空间的.


HDR


泛光 bloom

下面这几步就是泛光后处理特效的过程,它总结了实现泛光所需的步骤。

首先我们需要根据一定的阈限提取所有明亮的颜色。我们先来做这件事。


GPU和GLSL并不擅长优化循环和分支

然而事实上,你的GPU和GLSL并不擅长优化循环和分支。这一缺陷的原因是GPU中着色器的运行是高度并行的,大部分的架构要求对于一个大的线程集合,GPU需要对它运行完全一样的着色器代码从而获得高效率


注意问题

矩阵行列优先级

在 CG 中, 对 float4x4 等类型的变量是 按行有限 的方式进行填充。(入门精要 4.9.2)

单位精度

类型 精度
float 最高精度。32位存储
half 中等精度。16位存储,范围 -60 000 ~ +60 000
fixed 最低精度。11位存储,范围 -2.0 ~ +2.0 (常用来存储颜色值(0~1之间))

参考 : Cg/HLSL中的数据类型 - https://zhuanlan.zhihu.com/p/48530294

  • float

    高精度类型,32位,通常用于世界坐标下的位置,纹理UV,或涉及复杂函数的标量计算,如三角函数、幂运算等。

  • half

    中精度类型,16位,数值范围为[-60000,+60000],通常用于本地坐标下的位置、方向向量、HDR颜色等。

  • fixed

    低精度类型,11位,数值范围为[-2,+2],通常用于常规的颜色与贴图,以及低精度间的一些运算变量等。

在PC平台不管你Shader中写的是half还是fixed,统统都会被当作float来处理。half与fixed仅在一些移动设备上有效。
比较常用的一个规则是,除了位置和坐标用float以外,其余的全部用half。主要原因也是因为大部分的现代GPU只支持32位与16位,也就是说只支持float和half,不支持fixed。


技巧

替代 if else 判断

可以参考: unity-shader-GPU优化_step函数替代if-else - https://blog.csdn.net/yangxuan0261/article/details/89852627

GPU 中尽量减少使用 if else 判断逻辑,执行消耗非常大,可以使用以下方式替代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fixed4 frag(v2f i) : SV_Target
{
float factor = i.objPos.x - _DissolveThreshold;

fixed3 color = tex2D(_MainTex, i.uv).rgb;
fixed4 retClr;
//方式a, 方式a 与 方式b 等价, 但 方式b 性能更好
//if (i.objPos.x > _DissolveThreshold) {
// retClr = _DissolveColor;
//} else {
// retClr = _DissolveColor;
//}

//方式b
fixed lerpFactor = saturate(sign(i.objPos.x - _DissolveThreshold));
//fixed lerpFactor = step(_DissolveThreshold, i.objPos.x); // 与上一行等价
retClr = lerpFactor * _DissolveColor + (1 - lerpFactor) * fixed4(color, 1);
//retClr = lerp(fixed4(color, 1), _DissolveColor, lerpFactor); // 与上一行代码等价

return retClr;
}

sign 约束 值 只能是 -1, 0, 1

saturate 约束值 只能是 [0, 1]

step(a, b) 表示 如果 a <= b,返回 1 ;否则,返回 0 。

所以只要 i.objPos.x > _DissolveThreshold,就取 颜色 _DissolveColor


pass 复用

参考:

  1. 定义 pass 逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    Shader "test/PassNameBase"
    {
    Properties
    {
    _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
    Tags { "RenderType"="Opaque" }
    LOD 100

    Pass
    {
    Name "PassNameBase01"
    CGPROGRAM
    // 逻辑 1
    ENDCG
    }

    Pass
    {
    Name "PassNameBase02"
    CGPROGRAM
    // 逻辑 2
    ENDCG
    }
    }
    }

  2. 复用 pass 逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Shader "test/PassNameTest01"
    {
    Properties
    {
    _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
    Tags { "RenderType"="Opaque" "IgnoreProjector"="True"}
    LOD 100
    UsePass "test/PassNameBase/PassNameBase01"
    // UsePass "test/PassNameBase/PassNameBase02"
    }
    }

    然后为该 shader 创建材质球就可以使用了.


几何意义解析

  • 点乘(又名 点积,dot):dot(a, b) 求出 向量 a、b夹角的 余弦值,值越大,夹角越小
  • 叉乘(又名 叉积,cross): cross(a, b) 求出 与向量 a、b组成平面 垂直的 c 向量
  • 变换,mul(A, n) 用 矩阵A 将 n 向量变换 到 A空间 下。(A是一个坐标系,可能是 3x3坐标系, 可能是 4x4 齐次坐标系) ,等价与 mul(n, A的逆矩阵) (矩阵相乘不满足交换律)

专栏


_Time 的单位

名称 类型 说明
_Time float4 t 是自该场景加载开始所经过的时间,4个分量分别是 (t/20, t, t2, t3)
_SinTime float4 t 是时间的正弦值,4个分量分别是 (t/8, t/4, t/2, t)
_CosTime float4 t 是时间的余弦值,4个分量分别是 (t/8, t/4, t/2, t)
unity_DeltaTime float4 dt 是时间增量,4个分量的值分别是(dt, 1/dt, smoothDt, 1/smoothDt)

XX_TexelSize

XX纹理的像素相关大小width,height对应纹理的分辨率,x = 1/width, y = 1/height, z = width, w = height

也就是 : Vector4(1 / width, 1 / height, width, height)


后处理

后处理 需要注意贴图的平台差异

1
2
3
4
5
//dx中纹理从左上角为初始坐标,需要反向(在写rt的时候需要注意)
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv2.y = 1 - o.uv2.y;
#endif

默认传 _MainTex 给 shader

要注意写法

1
2
3
4
5
6
7
8
9
Shader "test/PostProcCircle"  {
Properties {
_MainTex("Base (RGB)", 2D) = "white" {} // 1. 要声明一个 暴露 的参数, 不然会是一个全灰色的图
}

CGINCLUDE
#include "UnityCG.cginc"

sampler2D _MainTex; // 2. 声明 uniform 变量

运动模糊

相关资料

运动模糊是个经常会用到的效果,常见的实现步骤是:

  1. 对深度纹理进行采样,取得当前片元的深度信息
  2. 根据深度信息建立当前片元的NDC空间的坐标curNDCPos
  3. 把curNDCPos乘以当前VP矩阵的逆矩阵(即View*Projection)-1,得到当前片元的世界空间坐标WorldPos
  4. 把WorldPos乘以上一帧的VP矩阵(即View*Projection),得到上一帧在裁切空间中的位置 lastClipPos
  5. 把lastClipPos除以其w分量,得到NDC空间位置lastNDCPos
  6. 用当前片元NDC空间位置 减去 上一帧NDC空间位置(即 curNDCPos-lastClipPos),得到速度的方向speed
  7. 沿speed方向进行多次采样,求出平均值作为当前片元的颜色

法线的变换

参考 :

官方的解释

  • MV transforms points from object to eye space
  • IT_MV rotates normals from object to eye space

And similarly:

  • Object2World transforms points from object to world space
  • IT_Object2World (which, as you point out, is the transpose of World2Object) rotates normals from object to world space

If it is orthogonal, the upper-left 3x3 of Object2World will be equal to that of IT_Object2World, and so will also rotate normals from object to world space.

上面这里很好的描述了UNITY_MATRIX_IT_MV的使用场景,专门针对法线进行变换。但是为什么法线的变换和定点不一样呢?让我们来看一篇推导的文章。

注:之所以法线不能直接使用UNITY_MATRIX_MV进行变换,是因为法线是向量,具有 方向,在进行空间变换的时候,如果发生非等比缩放,方向会发生偏移。为什么呢?拿上面的例子来说,我们可以简单的把法线和切线当成三角形的两条边,显然,三角形在空间变换的时候,不管是平移,还是旋转,或者是等比缩放,都不会变形,但是如果非等比缩放,就会发生拉伸。所以法线和切线的夹角也就会发生变化。(而切线在变换前后,方向总是正确的,所以法线方向就不正确了)。

结论 : 法线的变换需要用 xxx_IT

shader示例1

1
2
3
4
5
6
7
8
9
10
11
12
13
// 变换 法线 从 模型空间 -> 观察空间
// 方式一
// float3 worldNorm = UnityObjectToWorldNormal(v.normal).xyz;
// float3 viewNormal = mul((float3x3)UNITY_MATRIX_V, worldNorm); // 将法线转到观察空间下, 因为matcap贴图是摄像机看到的贴图
// o.uv.zw = viewNormal.xy; // 转换法线值为贴图值

// 方式二
float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal.xyz);
o.uv.zw = viewNormal.xy;

// 方式三
// o.uv.z = mul(UNITY_MATRIX_IT_MV[0], v.normal);
// o.uv.w = mul(UNITY_MATRIX_IT_MV[1], v.normal);

shader示例2 - 切线空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
v2f2 vert2 (appdata2 v)
{
v2f2 o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);

float3 worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
o.lightDir = UnityWorldSpaceLightDir(worldPos);
o.viewDir = UnityWorldSpaceViewDir(worldPos);

// 构建 法线 从 切线空间 到 世界空间 的 变换矩阵(三个向量)
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;

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 frag2 (v2f2 i) : SV_Target
{
//获得世界空间中的坐标
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
//计算光照和视角方向在世界坐标系中
fixed3 worldLightDir = normalize(i.lightDir);
fixed3 worldViewDir = normalize(i.viewDir);

fixed4 packedNormal = tex2D(_NormalTex, i.uv);
fixed4 tex = tex2D(_MainTex, i.uv);

// 法线图 解包 后 的 切线空间的法线向量
fixed3 tangentNormal = UnpackNormal(packedNormal);

// 方式一
// 转换 法线向量 从 切线空间 到 世界空间, 等价于 下面注释部分
// fixed3 worldNormal = normalize(half3(dot(i.TtoW0.xyz, tangentNormal), dot(i.TtoW1.xyz, tangentNormal), dot(i.TtoW2.xyz, tangentNormal)));

// 方式二
fixed3 worldNormal = normalize(half3(mul(i.TtoW0.xyz, tangentNormal), mul(i.TtoW1.xyz, tangentNormal), mul(i.TtoW2.xyz, tangentNormal)));

// 方式三
// 构建 转换矩阵
// float3x3 worldNormalMatrix = float3x3(i.TtoW0.xyz, i.TtoW1.xyz, i.TtoW2.xyz);
// fixed3 worldNormal = normalize(mul(worldNormalMatrix, tangentNormal));

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * tex.rgb;

//漫反射
fixed3 diffuse = _LightColor0.rgb * tex.rgb * saturate(dot(worldNormal, worldLightDir));

//Blinn-Phong高光光照模型,相对于普通的Phong高光模型,会更加光
fixed3 halfDir = normalize(worldLightDir + worldViewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);

//边缘颜色,对于法线和观察方向,只要在同一坐标系下即可
fixed dotProduct = 1 - saturate(dot(worldNormal, worldViewDir));
fixed3 rim = _RimColor.rgb * pow(dotProduct, 1 / _RimPower);

fixed4 maskCol = tex2D(_MaskTex, i.uv + float2(0, _Time.y * _MoveDir));
return fixed4(ambient + diffuse + specular + rim * maskCol.rgb, 1);
// return fixed4(ambient + diffuse, 1);
}

法线混合算法

相关文章

引擎实现

  • unity

    unity 的 shader graph 的节点 Normal Blend Node

  • ue4


ComputeScreenPos 屏幕空间位置

参考:

获取像素在屏幕空间的位置

  • 内置 ComputeScreenPos 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    inline float4 ComputeNonStereoScreenPos(float4 pos) {
    float4 o = pos * 0.5f;
    o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
    o.zw = pos.zw;
    return o;
    }

    inline float4 ComputeScreenPos(float4 pos) {
    float4 o = ComputeNonStereoScreenPos(pos);
    #if defined(UNITY_SINGLE_PASS_STEREO)
    o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
    #endif
    return o;
    }
  • 内置 tex2Dproj 实现

    1
    half4 tex2Dproj(sampler2D s, in half4 t)        { return tex2D(s, t.xy / t.w); }

简单的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.projPos = ComputeScreenPos(o.vertex); // 参数是剪裁空间下的坐标
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}

fixed4 frag (v2f i) : SV_Target {
fixed4 col = tex2D(_MainTex, i.projPos.xy / i.projPos.w);
// fixed4 col = tex2Dproj(_MainTex, i.projPos);
return col;
}

一般采样贴图是都是顶点中的uv值, 是个固定值的, 所以不会随着顶点位置的变化而变化, 就会造成拉伸.

可以做图片不被拉伸的效果, 因为采样的 uv 值是 0-1, 所以只用当四个顶点完后只能覆盖屏幕四个角落时, 才会显示出完整的图片.
因为 mesh 的所有的顶点在屏幕空间下的值都会在 (0, 0) 到 (1, 1) 区间内.


表面着色器

参考:

1
2
3
4
5
6
7
8
# 指定 表面函数 和 光照函数 
#pragma surface surf Toon
struct Input {
float2 uv_MainTex;
float2 uv_Bump;
};
void surf (Input IN, inout SurfaceOutput o) {...}
half4 LightingToon(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten) {...}

雾效使用

unity 中使用雾效的姿势. 主要是四个地方, 加下面注释处.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Pass
{
CGPROGRAM

#pragma multi_compile_fog // 雾效编译指令

struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1) // 雾效数据字段, 1 为寄存器语义 TEXCOORD1, 完整展开 - float fogCoord : TEXCOORD1
float4 vertex : SV_POSITION;
};



v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex); // 将 剪裁空间 下 顶点 的 z 值 存进 fogCoord 字段中
return o;
}

fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
UNITY_APPLY_FOG(i.fogCoord, col); // 最终颜色加上雾效
return col;
}
}

shader 可视化编辑

比较热门的两款 可视化 插件 Amplify Shader vs Shader Forge

Amplify Shader vs Shader Forge

Shader Forge 貌似更新太慢等很多抱怨

Amplify Shader 的可视化 和 ue4 的材质编辑器 很相似,编辑出来的是 surface着色器 的源码

Shader Forge 编辑出来的是 vertex着色器与fragment着色器 的源码


关于 geometry shader

Shader Model 4 supports a new pipeline stage—the geometry-shader stage—which can be used to create or modify existing geometry.

要 sm4.0 才支持, 所以手机上还还不能使用

unity Shader Compilation Target Levels - https://docs.unity3d.com/Manual/SL-ShaderCompileTargets.html

sm2.0 才能支持移动平台


GI shader中的使用方式

可以参考这个仓库: https://github.com/keijiro/AdamPlaneReflection


GLSL 转 UnityShader


着色器数据类型和精度

Unity 中的标准着色器语言为 HLSL,支持一般 HLSL 数据类型。但是,Unity 对 HLSL 类型有一些补充,特别是为了在移动平台上提供更好的支持。

着色器中的大多数计算是对浮点数(在 C# 等常规编程语言中为 float)进行的。浮点类型有几种变体:floathalffixed(以及它们的矢量/矩阵变体,比如 half3float4x4)。这些类型的精度不同(因此性能或功耗也不同):

高精度:float

最高精度浮点值;一般是 32 位(就像常规编程语言中的 float)。

完整的 float 精度通常用于世界空间位置、纹理坐标或涉及复杂函数(如三角函数或幂/取幂)的标量计算。

中等精度:half

中等精度浮点值;通常为 16 位(范围为 –60000 至 +60000,精度约为 3 位小数)。

半精度对于短矢量、方向、对象空间位置、高动态范围颜色非常有用。

低精度:fixed

最低精度的定点值。通常是 11 位,范围从 –2.0 到 +2.0,精度为 1/256。

固定精度对于常规颜色(通常存储在常规纹理中)以及对它们执行简单运算非常有用。

整数数据类型

整数(int 数据类型)通常用作循环计数器或数组索引。为此,它们通常可以在各种平台上正常工作。

根据平台的不同,GPU 可能不支持整数类型。例如,Direct3D 9 和 OpenGL ES 2.0 GPU 仅对浮点数据进行运算,并且可以使用相当复杂的浮点数学指令来模拟简单的整数表达式(涉及位运算或逻辑运算)。

Direct3D 11、OpenGL ES 3、Metal 和其他现代平台都对整数数据类型有适当的支持,因此使用位移位和位屏蔽可以按预期工作。


Queue

Queue渲染队列,用来指定当前shader作用的对象的渲染顺序:
Unity中的几种内置的渲染队列,按照渲染顺序,从先到后进行排序,队列数越小的,越先渲染,队列数越大的,越后渲染。

  • Background(1000) 最早被渲染的物体的队列。
  • Geometry (2000) 不透明物体的渲染队列。大多数物体都应该使用该队列进行渲染,也是Unity Shader中默认的渲染队列。
  • AlphaTest (2450) 有透明通道,需要进行Alpha Test的物体的队列,比在Geomerty中更有效。
  • Transparent(3000) 半透物体的渲染队列。一般是不写深度的物体,Alpha Blend等的在该队列渲染。
  • Overlay (4000) 最后被渲染的物体的队列,一般是覆盖效果,比如镜头光晕,屏幕贴片之类的

3d 场景透明贴图渲染合批优化

  • 渲染队列中, Transparent (透明, 默认: 3000) 渲染队列不能 gpu instancing,遮挡关系因为 go 的顺序间隔打断 instance

    Geometry (不透明, 默认: 2000), 就不会因为 顺序打断,不透明物体的遮挡关系取决于深度

    想要渲染 透明物体,又要使用 instance,就需要把 透明物体 的队列设置到 2000-2450 之间, 如: "Queue"="Geometry+20", 让 unity 识别到要在 不透明渲染队列在中渲染, 同时不要写入深度,因为如果 物体x 的渲染队列更高, 同时深度又小于这个 透明物体 的话, 物体x 就不会渲染出来


RenderType

  • a

URP/SRP


常见问题

(1) TRANSFORM_TEX是做什么的

(2)float4 _MainTex_ST 中的_MainTex_ST变量也没有用到,为啥非要声明一下?

答:

(1)简单来说,TRANSFORM_TEX主要作用是拿顶点的uv去和材质球的tiling和offset作运算, 确保材质球里的缩放和偏移设置是正确的。 (v.texcoord就是顶点的uv)

下面这两个函数是等价的。

o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);

o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;

所以使用他有两个前提:

\1. #include “UnityCG.cginc”

\2. 定义name##_ST

(2)而_MainTex_ST的ST是应该是SamplerTexture的意思 ,就是声明_MainTex是一张采样图,也就是会进行UV运算。 如果没有这句话,是不能进行TRANSFORM_TEX的运算的。_MainTex_ST.xy为 下图中的Tiling,zw为下图中的offset.

如果Tiling 和Offset你留的是默认值,即Tiling为(1,1) Offset为(0,0)的时候,可以不用

o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);

换成o.uv = v.texcoord.xy;也是能正常显示的;相当于Tiling 为(1,1)Offset为(0,0),但是如下图自己填的Tiling值和Offset值就不起作用了

【风宇冲】Unity3D教程宝典之Shader篇:特别讲 常见问题解答

clip 函数

1
2
// alpha test
clip (o.Alpha - _CutOff);

clip(x) : 如果输入向量中的任何元素小于0,则丢弃当前像素。

也就是说,是通过 Alpha 值 减去 指定的 _CutOff ,也就是,Alpha 值 低于 _CutOff 的片段都会被丢弃。

根据书上所说,这次学习的 透明裁剪着色器,因为要逐像素判断 Alpha 是否小于 _CutOff ,所以是比较消耗 GPU的,所以在手机并不推荐

但是在工作中,会发现,其实很大程度上都是一个度的问题。

像这中溶解效果,用在副本里面,不可能几十个怪同时死亡而同时显示溶解的效果的。

所以并没有什么在手机上就不能用这种事情。想用就用。

SV_Target、SV_POSITION

SV其实是system-value(系统数值)的意思,需要注意的是SV开头的语义考虑到扩平台的问题了,所以能用SV开头的语义的尽量使用SV开头的语义,免去跨平台的烦恼

http://blog.csdn.net/a958832776/article/details/70847033

http://blog.csdn.net/zhao_92221/article/details/46797969

Q: Duplicated input semantics can’t change type, size or layout. (TEXCOORD1)

重复使用了 TEXCOORD1 语义,可能类型 UNITY_FOG_COORDS(1) 这样的unity宏里使用了,所以改成 TEXCOORD2\3\4\5 即可

Q: 法线图偏蓝紫色 (rgb(0.5,0.5,1))

法线贴图每个像素存的都是每点(当然这些点是离散化的)的局部法向量坐标,所以没有扰动法向量的时候,图片全部像素应该是rgb(0.5,0.5,1) 大概就是蓝紫色那样 (假设z轴就是法向)。因为单位法向量的xy分量取值在(-1,1)但是颜色分量取值在(0,1),所以把这个区间映射一下就会发现未被扰动的向量(0,0,1)会被映射成(0.5,0.5,1)这个偏蓝紫色的颜色

映射公式: pixel = (normal + 1) / 2

通常情况下,美工会做出一张使用的RGB三个通道用来存放法线的XYZ三个轴向的坐标的法线贴图。但是Unity在导入法线贴图的时候会自动将法线贴图压缩成 DXT5nm 格式,这个格式的好处是 它只使用AG(透明和绿色)两个通道来存放两个轴向的坐标值。而Z轴向的坐标值由于是 单位坐标 可以通过 1 减去另外前两个轴向坐标的 平方和 来得到(已知2个分量,可求第三个),从而可以以同样的容量存放更大尺寸的法线贴图,我们都知道图片的颜色通道存的都是非负数(法线贴图生成的时候已经把[-1,1]压缩为[0-1]),而我们的三维空间是[-1,1],所以我们要把它解析放大一下,方法就是对对应的颜色通道值乘以2再减1。这就能存储游戏物体所有的法线信息了。例如,一个RGB值为(0.5,0.5,1)或#8080ff(16进制)的颜色向量,它所存储法线向量值为(0,0,1),代表该图向上的法向量,即模型没有凹凸现象。对比本页面之前的模型平面区域,您就会发现这种颜色基调。

参考 : 法线贴图详解之三—-凹凸,法线以及高度图的区别及法线贴图蓝紫色的原因 - http://manew.com/thread-90210-1-1.html

因此, 如果切线空间下的法线 和 顶点 ( 插值后的) 法线 重合 (平行) , 则这该点像素值为 ( 0.5, 0.5, 1), 也就是对应的法线值 ( 0, 0, 1), 也就是切线空间坐标系的z轴, 也就是顶点的法线.

Q: worldNorm.xy * 0.5 + 0.5 是几个意思

等价与 上面那个问题的 映射公式,都是将 法线值转为 像素值

会使用到这个公式 一般都是 使用了 fixed3 normals = UnpackNormal(tex2D(_BumpMap, i.uv_bump)); 将法线值 从 法线图 解包后

1
2
3
4
5
6
pixel = (normal + 1) / 2
//等价与
pixel = (normal * 0.5 + 1 * 0.5

//推导出
normal = pixel * 2 - 1

Q: unity_ObjectToWorld 和 unity_WorldToObject 是互逆矩阵?

对,所以用法可以互换,

1
2
3
// Transform the normal from object space to world space
// 转换法线 从 模型空间 到 世界空间
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject); // 内置 UnityObjectToWorldNormal 的实现, 相当于 mul(unity_ObjectToWorld的逆转矩阵, v.normal);

此方式只能用在向量上,不能用在顶点上(v.vertex),用在顶点上会得到不一样的结果. 因为 法向量是方向向量, 所以不管怎么平移都是一样的, 所以才使用 float3x3 矩阵 (去掉了平移)

推到公式可以参考: 法向量矩阵 - https://www.qiujiawei.com/linear-algebra-18/, 他也是参考 3D游戏与计算机图形学中的数学方法 这本书的.


变体 shader 丢失

表现上 editor 正常, 移动端 丢失, 经测试是 移动端变体丢失

解决办法参考: unity-shader变体ShaderVariant.md


Shader Forge shader 材质移动端显示黑色 (丢失)

将对应的 shader 里面 only_renderers 等指定 渲染硬件的 注释掉即可

1
2
// #pragma only_renderers d3d9 d3d11 glcore gles
// #pragma exclude_renderers d3d9 d3d11 glcore gles

参考: Shader Forge shader材质移动端显示黑色bug修复 - https://blog.csdn.net/u014670984/article/details/114364246